feat: implement usage tracker + quotas

This commit is contained in:
Ruben Fiszel
2022-12-11 18:47:39 +01:00
parent 7bc0c2b0c4
commit 039e1fa1e6
32 changed files with 860 additions and 642 deletions

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,8 @@
-- Add up migration script here
CREATE TABLE usage (
id VARCHAR(50) NOT NULL,
is_workspace BOOLEAN NOT NULL,
month_ INTEGER NOT NULL,
usage INTEGER NOT NULL,
PRIMARY KEY (id, is_workspace, month_)
);

View File

@@ -0,0 +1,5 @@
-- Add down migration script here
ALTER TABLE queue DROP COLUMN email;
ALTER TABLE workspace_settings DROP COLUMN slack_email;
ALTER TABLE schedule DROP COLUMN email;
ALTER TABLE schedule DROP COLUMN error;

View File

@@ -0,0 +1,6 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT 'missing@email.xyz';
ALTER TABLE workspace_settings ADD COLUMN slack_email VARCHAR(50) NOT NULL DEFAULT 'missing@email.xyz';;
ALTER TABLE schedule ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT 'missing@email.xyz';
ALTER TABLE schedule ADD COLUMN error TEXT;

View File

@@ -1,30 +1,5 @@
{
"db": "PostgreSQL",
"02cf26c6e81ce21fa84fdc2a431663aa2db7efea3596d7eec984dd707b8ad010": {
"describe": {
"columns": [
{
"name": "expiration",
"ordinal": 0,
"type_info": "Timestamptz"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Text",
"Bool"
]
}
},
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6) RETURNING expiration"
},
"0355b53b1d45955ca56b2829372ce9c656d7f0ad7b8d0709161047f0d8cdc4f4": {
"describe": {
"columns": [
@@ -258,73 +233,6 @@
},
"query": "DELETE FROM account WHERE id = $1 AND workspace_id = $2"
},
"0eea65f84168d4425d20d24b321cdee98b69dbf1e8a75e916af6d37abc4aee92": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Bool",
"Uuid",
"Varchar",
"Varchar",
"Timestamptz",
"Int8",
"Varchar",
"Text",
"Text",
"Jsonb",
{
"Custom": {
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies"
]
},
"name": "job_kind"
}
},
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
"Bool",
"Text"
]
}
},
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20) RETURNING id"
},
"11b1586acdfc180c5a077861ee1f7201fcbcec9d0ebada464f9d952c9c3e400d": {
"describe": {
"columns": [],
@@ -472,13 +380,19 @@
"name": "slack_command_script",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "slack_email",
"ordinal": 4,
"type_info": "Varchar"
}
],
"nullable": [
false,
true,
true,
true
true,
false
],
"parameters": {
"Left": [
@@ -594,6 +508,74 @@
},
"query": "UPDATE queue SET suspend = $1 WHERE id = $2"
},
"207b9cca0240dd6eea3f3b95c093ead9d7bb48fae2484e1a4086636396c0dcb7": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Bool",
"Uuid",
"Varchar",
"Varchar",
"Timestamptz",
"Int8",
"Varchar",
"Text",
"Text",
"Jsonb",
{
"Custom": {
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies"
]
},
"name": "job_kind"
}
},
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
"Bool",
"Text",
"Varchar"
]
}
},
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21) RETURNING id"
},
"20e8a8343adc4a3a28a1c4908a13047cddafc4480cf028d3455fd17347cace73": {
"describe": {
"columns": [
@@ -754,20 +736,6 @@
},
"query": "SELECT now()"
},
"282655dc568d000379196fae0dee32438a824d93f4ad2fcc155269e76f4ca20d": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar"
]
}
},
"query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name)\n VALUES ($1, $2, $3) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3"
},
"28c042adef65c3055edc324fbbd2f267285d3566cbec58404983323d410ace27": {
"describe": {
"columns": [
@@ -918,6 +886,20 @@
},
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description, account, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6, $7)"
},
"2ee6d24b95cdda151585dcff19f8e7c931785fc21f7bbe9c3a82671943ced0ea": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
}
},
"query": "UPDATE schedule SET enabled = false, error = $1 WHERE workspace_id = $2 AND path = $3"
},
"355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": {
"describe": {
"columns": [
@@ -1050,6 +1032,26 @@
},
"query": "UPDATE app SET versions = array_append(versions, $1) WHERE id = $2"
},
"3e785610aa9f63241c16affc1f4a20c52f2aeb2aed9fc9e3b8c24874c87b14dc": {
"describe": {
"columns": [
{
"name": "usage",
"ordinal": 0,
"type_info": "Int4"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Varchar"
]
}
},
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, true, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage"
},
"3fabb3857c9cf2d057814b54ee54a95d01b6a7d9e89bea239b832a9d70f0044b": {
"describe": {
"columns": [],
@@ -1251,6 +1253,16 @@
"name": "is_flow",
"ordinal": 10,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"type_info": "Text"
}
],
"nullable": [
@@ -1264,7 +1276,9 @@
false,
true,
false,
false
false,
false,
true
],
"parameters": {
"Left": [
@@ -1360,6 +1374,23 @@
},
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(\n JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2),\n ARRAY['step'], $3)\n WHERE id = $4\n "
},
"53f2836a652d34aafe6ffb266a092d7390212bc5532dcc226529a61a53deec42": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Text",
"Bool"
]
}
},
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6)"
},
"541ebd3bac65431237cf3b882dfdcd61ca97c253d9754d05bba59fda89841067": {
"describe": {
"columns": [
@@ -1414,13 +1445,19 @@
"name": "slack_command_script",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "slack_email",
"ordinal": 4,
"type_info": "Varchar"
}
],
"nullable": [
false,
true,
true,
true
true,
false
],
"parameters": {
"Left": [
@@ -1515,6 +1552,19 @@
},
"query": "SELECT app.id, app.path, app.summary, app.versions, app.policy,\n app.extra_perms, app_version.value, \n app_version.created_at, app_version.created_by from app, app_version \n WHERE app_version.id = $1 AND app.id = app_version.flow_id AND app.workspace_id = $2"
},
"576b00c515ee7cbf628b1881596d8a03c1c506b309e39a0048a3a8fe18f37070": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "UPDATE schedule SET error = NULL WHERE workspace_id = $1 AND path = $2"
},
"5850e0c1f7dee98455027a4c0db6ccea0af2c58aee656ef43964b515de054888": {
"describe": {
"columns": [
@@ -1575,27 +1625,6 @@
},
"query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter')"
},
"5b7a1d16d8109a65479ab33d411c60d14ea91d870fdff8606d7aa4ad39f0ba00": {
"describe": {
"columns": [
{
"name": "email",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT email FROM usr WHERE username = $1 AND workspace_id = $2"
},
"5b9b58612ca0f703a5d154a76fab82ac2329aef965fa937bfab2810b6e1336a4": {
"describe": {
"columns": [],
@@ -1876,26 +1905,6 @@
},
"query": "SELECT policy from app WHERE path = $1 AND workspace_id = $2"
},
"6eacfedfc1ab2431c318996d2ff480a65d55fc43d8fa95aa4e2e8430722dc82d": {
"describe": {
"columns": [
{
"name": "email",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT email FROM usr WHERE username = $1"
},
"6fc2cfae9df83eb24ea33e4c9567740100f4dd2285afc3ef474fc70041b0567b": {
"describe": {
"columns": [
@@ -2461,6 +2470,16 @@
"name": "is_flow",
"ordinal": 10,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"type_info": "Text"
}
],
"nullable": [
@@ -2474,7 +2493,9 @@
false,
true,
false,
false
false,
false,
true
],
"parameters": {
"Left": [
@@ -2721,27 +2742,6 @@
},
"query": "INSERT INTO app_version\n (flow_id, value, created_by)\n VALUES ($1, $2, $3) RETURNING id"
},
"92ee6274b2287bc4d368bfdb47d6452df75bd265cdd891f55112df2795294dac": {
"describe": {
"columns": [
{
"name": "count",
"ordinal": 0,
"type_info": "Int8"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT COUNT(id) FROM queue WHERE permissioned_as = $1 AND workspace_id = $2"
},
"9490a4388f43e45e32911f1129e623f2d73ce2da7a948fe134bea1c87cdbefd1": {
"describe": {
"columns": [
@@ -2784,6 +2784,101 @@
},
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'branchall', 'branch'], ((flow_status->'modules'->$1::int->'branchall'->>'branch')::int + 1)::text::jsonb)\n WHERE id = $2\n RETURNING (flow_status->'modules'->$1::int->'branchall'->>'branch')::int\n "
},
"96dc1cd308f3c21cfb50b88048054dc03f93e261d25969d66aa48e9d0502960f": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "edited_by",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "edited_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "schedule",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true
],
"parameters": {
"Left": [
"Bool",
"Varchar",
"Text",
"Text"
]
}
},
"query": "UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *"
},
"97e364c703bdcdfb5345810659cbe0477a28b8199ef0b297f9a22c88a43b6b5c": {
"describe": {
"columns": [
@@ -3008,6 +3103,16 @@
"name": "is_flow",
"ordinal": 10,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"type_info": "Text"
}
],
"nullable": [
@@ -3021,7 +3126,9 @@
false,
true,
false,
false
false,
false,
true
],
"parameters": {
"Left": [
@@ -3032,6 +3139,19 @@
},
"query": "SELECT * FROM schedule WHERE path = $1 AND workspace_id = $2"
},
"ab690bfb3fd84eb0a833818352501e1560d22fd47b9a676571ea6efdad1ad300": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
}
},
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2"
},
"abc9f034e62ac224894173356aa69e09f3647a45d176e253e4fc8f7206f6a18d": {
"describe": {
"columns": [
@@ -3181,6 +3301,26 @@
},
"query": "UPDATE usr SET is_admin = $1 WHERE username = $2 AND workspace_id = $3"
},
"b2e14f4fe46b82fe333eb76cd75fdac0ab205a14999becb394b09c6e51af284f": {
"describe": {
"columns": [
{
"name": "usage",
"ordinal": 0,
"type_info": "Int4"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Varchar"
]
}
},
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage"
},
"b3b80de52d0931a2fdb5d38b7603a2d69cc25ab1cda413228c363a5ffd777113": {
"describe": {
"columns": [
@@ -3464,88 +3604,6 @@
},
"query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6, dependency_job = NULL WHERE path = $7 AND workspace_id = $8"
},
"c213427a32e8903fff649a3e7aa8392d2215665ed04f5a8f2178861e9dba298a": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "edited_by",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "edited_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "schedule",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"type_info": "Bool"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false
],
"parameters": {
"Left": [
"Bool",
"Text",
"Text"
]
}
},
"query": "UPDATE schedule SET enabled = $1 WHERE path = $2 AND workspace_id = $3 RETURNING *"
},
"c2849e67b9fea0dc46e6d7000f5a0c9dab89ae80a183d9255f8fdb356b4bc61c": {
"describe": {
"columns": [
@@ -4107,26 +4165,106 @@
},
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'iterator', 'index'], ((flow_status->'modules'->$1::int->'iterator'->>'index')::int + 1)::text::jsonb)\n WHERE id = $2\n RETURNING (flow_status->'modules'->$1::int->'iterator'->>'index')::int\n "
},
"e69142610bd94f6c65022cca5ccf7458b7c5bb77b3ed0278715222dd920d8644": {
"e5ba82116000d2ae71e2099f417b39121178349fda9f8438bdd5556ff632ceb2": {
"describe": {
"columns": [
{
"name": "sum",
"name": "workspace_id",
"ordinal": 0,
"type_info": "Int8"
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "edited_by",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "edited_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "schedule",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"type_info": "Text"
}
],
"nullable": [
null
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true
],
"parameters": {
"Left": [
"Text",
"Text"
"Varchar",
"Varchar",
"Varchar",
"Int4",
"Varchar",
"Varchar",
"Bool",
"Jsonb",
"Bool",
"Varchar"
]
}
},
"query": "\n SELECT SUM(duration_ms)\n FROM completed_job\n WHERE permissioned_as = $1\n AND created_at > NOW() - INTERVAL '1200 seconds'\n AND workspace_id = $2"
"query": "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *"
},
"e838b15953b567af2a2da6a4e3a04add5ec2cacabd93171a97c5fc978a64f8aa": {
"describe": {
@@ -4156,6 +4294,21 @@
},
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3"
},
"eaa6e9dc4c0d3d6cc8152515019befa880bb3b69ff340d337edd14b65e74e2a3": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
}
},
"query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name, slack_email)\n VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3, slack_email = $4"
},
"ed400dbf4a1fa597bc984240cfe9069369a8a265b127116187d78c0ba93ae076": {
"describe": {
"columns": [
@@ -4296,94 +4449,6 @@
},
"query": "\n SELECT payload\n FROM capture\n WHERE workspace_id = $1\n AND path = $2\n "
},
"f4a1bb4dfeafa3f28385c19d10730f2f49b4ebe5bba631cee46c760cee2e6949": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "edited_by",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "edited_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "schedule",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"type_info": "Bool"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Int4",
"Varchar",
"Varchar",
"Bool",
"Jsonb",
"Bool"
]
}
},
"query": "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *"
},
"f516ca558816c2cbab3c8ae865ef8f764aa4686e7df9c53d30d49a3dbbf36af4": {
"describe": {
"columns": [
@@ -4404,6 +4469,19 @@
},
"query": "SELECT SUM(duration_ms) as duration FROM completed_job WHERE id = ANY($1)"
},
"f67ba6ca3ed802ca41e49a310a4161ea8d81f79021f11f3a9f2b65e93ce6312d": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
}
},
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, true, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2"
},
"f7906298e4204ad55ec84021bb2461f369386493519637279f1188227230c580": {
"describe": {
"columns": [
@@ -4571,6 +4649,24 @@
},
"query": "SELECT * FROM resource WHERE workspace_id = $1"
},
"fa32fb9e4676dd91e20226b2a88672aed65ef33c39be7cdf63c733020a46e212": {
"describe": {
"columns": [
{
"name": "?column?",
"ordinal": 0,
"type_info": "Int4"
}
],
"nullable": [
null
],
"parameters": {
"Left": []
}
},
"query": "\n SELECT usage.usage + 1 FROM usage \n WHERE is_workspace = false AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)"
},
"fa567c205929f41799a64aefac97c4504c7993b22478530e9345b9bc117e92e9": {
"describe": {
"columns": [

View File

@@ -823,6 +823,7 @@ impl RunJob {
payload,
args,
/* user */ "test-user",
/* email */ "test@windmill.dev",
/* permissioned_as */ "u/admin".to_string(),
/* scheduled_for_o */ None,
/* schedule_path */ None,

View File

@@ -4041,6 +4041,8 @@ components:
language:
type: string
enum: [python3, deno, go, bash]
email:
type: string
required:
- id
- running
@@ -4048,6 +4050,7 @@ components:
- job_kind
- permissioned_as
- is_flow_step
- email
CompletedJob:
type: object
@@ -4633,6 +4636,10 @@ components:
type: object
additionalProperties:
type: boolean
email:
type: string
error:
type: string
required:
- path
- edited_by
@@ -4643,6 +4650,7 @@ components:
- extra_perms
- is_flow
- enabled
- email
NewSchedule:
type: object

View File

@@ -94,6 +94,7 @@ pub enum ExecutionMode {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Policy {
pub on_behalf_of: Option<String>,
pub on_behalf_of_email: Option<String>,
//paths:
// - script/<path>
// - flow/<path>
@@ -457,49 +458,40 @@ async fn execute_component(
static_fields,
);
}
Policy { execution_mode: ExecutionMode::Viewer, triggerables: hm, on_behalf_of: None }
Policy {
execution_mode: ExecutionMode::Viewer,
triggerables: hm,
on_behalf_of: None,
on_behalf_of_email: None,
}
} else {
policy
};
let (username, permissioned_as) = match policy.execution_mode {
let (username, permissioned_as, email) = match policy.execution_mode {
ExecutionMode::Anonymous => {
let username = opt_authed
.map(|a| a.username)
.unwrap_or_else(|| "anonymous".to_string());
let permissioned_as = policy
.on_behalf_of
.as_ref()
.ok_or_else(|| {
Error::BadRequest(
"on_behalf_of is missing in the app policy and is required for anonymous execution"
.to_string(),
)
})?
.to_string();
(username, permissioned_as)
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
(username, permissioned_as, email)
}
ExecutionMode::Publisher => {
let username = opt_authed.map(|a| a.username).ok_or_else(|| {
Error::BadRequest("publisher execution mode requires authentication".to_string())
})?;
let permissioned_as = policy
.on_behalf_of
.as_ref()
.ok_or_else(|| {
Error::BadRequest(
"on_behalf_of is missing in the app policy and is required for publisher execution"
.to_string(),
)
})?
.to_string();
(username, permissioned_as)
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
(username, permissioned_as, email)
}
ExecutionMode::Viewer => {
let username = opt_authed
.map(|a| a.username)
.ok_or_else(|| Error::BadRequest("".to_string()))?;
(username.clone(), owner_to_token_owner(&username, false))
let (username, email) = opt_authed.map(|a| (a.username, a.email)).ok_or_else(|| {
Error::BadRequest("Required to be authed in viewer mode".to_string())
})?;
(
username.clone(),
owner_to_token_owner(&username, false),
email,
)
}
};
@@ -535,6 +527,7 @@ async fn execute_component(
job_payload,
args,
&username,
&email,
permissioned_as,
None,
None,
@@ -549,6 +542,30 @@ async fn execute_component(
Ok(uuid.to_string())
}
fn get_on_behalf_of(policy: &Policy) -> Result<(String, String)> {
let permissioned_as = policy
.on_behalf_of
.as_ref()
.ok_or_else(|| {
Error::BadRequest(
"on_behalf_of is missing in the app policy and is required for anonymous execution"
.to_string(),
)
})?
.to_string();
let email = policy
.on_behalf_of_email
.as_ref()
.ok_or_else(|| {
Error::BadRequest(
"on_behalf_of is missing in the app policy and is required for anonymous execution"
.to_string(),
)
})?
.to_string();
Ok((permissioned_as, email))
}
async fn exists_app(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,

View File

@@ -11,7 +11,7 @@ use reqwest::Client;
use sql_builder::prelude::*;
use axum::{
extract::{Extension, Host, Path, Query},
extract::{Extension, Path, Query},
routing::{get, post},
Json, Router,
};
@@ -108,33 +108,27 @@ async fn list_flows(
}
async fn list_hub_flows(
Authed { email, username, .. }: Authed,
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> JsonResult<serde_json::Value> {
let flows = list_elems_from_hub(
http_client,
"https://hub.windmill.dev/searchFlowData?approved=true",
email,
username,
host,
&email,
)
.await?;
Ok(Json(flows))
}
pub async fn get_hub_flow_by_id(
Authed { email, username, .. }: Authed,
Authed { email, .. }: Authed,
Path(id): Path<i32>,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> JsonResult<serde_json::Value> {
let value = http_get_from_hub(
http_client,
&format!("https://hub.windmill.dev/flows/{id}/json"),
email,
username,
host,
&email,
false,
)
.await?
@@ -194,6 +188,7 @@ async fn create_flow(
JobPayload::FlowDependencies { path: nf.path.clone() },
serde_json::Map::new(),
&authed.username,
&authed.email,
windmill_common::users::owner_to_token_owner(&authed.username, false),
None,
None,
@@ -299,6 +294,7 @@ async fn update_flow(
JobPayload::FlowDependencies { path: nf.path.clone() },
serde_json::Map::new(),
&authed.username,
&authed.email,
windmill_common::users::owner_to_token_owner(&authed.username, false),
None,
None,

View File

@@ -868,6 +868,7 @@ impl From<UnifiedJob> for Job {
language: uj.language,
same_worker: false,
pre_run_error: None,
email: "".to_string(),
}),
t => panic!("job type {} not valid", t),
}
@@ -957,6 +958,7 @@ pub async fn run_flow_by_path(
JobPayload::Flow(flow_path.to_string()),
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
scheduled_for,
None,
@@ -990,6 +992,7 @@ pub async fn run_job_by_path(
job_payload,
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
scheduled_for,
None,
@@ -1056,6 +1059,7 @@ pub async fn run_wait_result_job_by_path(
job_payload,
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
scheduled_for,
None,
@@ -1090,6 +1094,7 @@ pub async fn run_wait_result_job_by_hash(
JobPayload::ScriptHash { hash: ScriptHash(hash), path },
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
scheduled_for,
None,
@@ -1142,6 +1147,7 @@ async fn run_preview_job(
}),
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
scheduled_for,
None,
@@ -1173,6 +1179,7 @@ async fn run_preview_flow_job(
JobPayload::RawFlow { value: raw_flow.value, path: raw_flow.path },
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
scheduled_for,
None,
@@ -1206,6 +1213,7 @@ pub async fn run_job_by_hash(
JobPayload::ScriptHash { hash: ScriptHash(hash), path },
args,
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
scheduled_for,
None,

View File

@@ -644,12 +644,13 @@ async fn connect_slack_callback(
sqlx::query!(
"INSERT INTO workspace_settings
(workspace_id, slack_team_id, slack_name)
VALUES ($1, $2, $3) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, \
slack_name = $3",
(workspace_id, slack_team_id, slack_name, slack_email)
VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, \
slack_name = $3, slack_email = $4",
&w_id,
token.team_id,
token.team_name
token.team_name,
authed.email
)
.execute(&mut tx)
.await?;
@@ -796,6 +797,7 @@ async fn slack_command(
payload,
map,
&form.user_name,
&settings.slack_email,
"g/slack".to_string(),
None,
None,

View File

@@ -47,7 +47,9 @@ async fn create_schedule(
Json(ns): Json<NewSchedule>,
) -> Result<String> {
let tx = user_db.begin(&authed).await?;
let res = windmill_queue::schedule::create_schedule(tx, w_id, ns, &authed.username).await?;
let res =
windmill_queue::schedule::create_schedule(tx, w_id, ns, &authed.username, &authed.email)
.await?;
Ok(res)
}
@@ -110,8 +112,15 @@ pub async fn set_enabled(
Json(payload): Json<SetEnabled>,
) -> Result<String> {
let tx = user_db.begin(&authed).await?;
let res =
windmill_queue::schedule::set_enabled(tx, w_id, path, payload, &authed.username).await?;
let res = windmill_queue::schedule::set_enabled(
tx,
w_id,
path,
payload,
&authed.username,
&authed.email,
)
.await?;
Ok(res)
}

View File

@@ -12,10 +12,10 @@ use windmill_audit::{audit_log, ActionKind};
use crate::{
db::{UserDB, DB},
users::{truncate_token, Authed, Tokened},
users::Authed,
};
use axum::{
extract::{Extension, Host, Path, Query},
extract::{Extension, Path, Query},
routing::{get, post},
Json, Router,
};
@@ -160,16 +160,13 @@ async fn list_scripts(
}
async fn list_hub_scripts(
Authed { email, username, .. }: Authed,
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> JsonResult<serde_json::Value> {
let asks = list_elems_from_hub(
http_client,
"https://hub.windmill.dev/searchData?approved=true",
email,
username,
host,
&email,
)
.await?;
Ok(Json(asks))
@@ -182,7 +179,6 @@ fn hash_script(ns: &NewScript) -> i64 {
}
async fn create_script(
authed: Authed,
Tokened { token }: Tokened,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
@@ -293,7 +289,7 @@ async fn create_script(
.map(|v| v.1.clone())
.unwrap_or(json!({}));
let lock = if ns.language == ScriptLang::Bash {
let lock = if ns.language == ScriptLang::Bash || ns.language == ScriptLang::Deno {
Some(String::new())
} else {
ns.lock
@@ -339,6 +335,7 @@ async fn create_script(
windmill_queue::JobPayload::Dependencies { hash, dependencies, language: ns.language },
serde_json::Map::new(),
&authed.username,
&authed.email,
owner_to_token_owner(&authed.username, false),
None,
None,
@@ -361,13 +358,7 @@ async fn create_script(
ActionKind::Update,
&w_id,
Some(&ns.path),
Some(
[
("hash", hash.to_string().as_str()),
("token", &truncate_token(&token)),
]
.into(),
),
Some([("hash", hash.to_string().as_str())].into()),
)
.await?;
} else {
@@ -382,7 +373,6 @@ async fn create_script(
[
("workspace", w_id.as_str()),
("hash", hash.to_string().as_str()),
("token", &truncate_token(&token)),
]
.into(),
),
@@ -399,33 +389,17 @@ pub async fn get_hub_script_by_path(
authed: Authed,
Path(path): Path<StripPath>,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> Result<String> {
windmill_common::scripts::get_hub_script_by_path(
authed.email,
authed.username,
path,
http_client,
host,
)
.await
windmill_common::scripts::get_hub_script_by_path(&authed.email, path, http_client).await
}
pub async fn get_full_hub_script_by_path(
Authed { username, email, .. }: Authed,
Authed { email, .. }: Authed,
Path(path): Path<StripPath>,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> JsonResult<HubScript> {
Ok(Json(
windmill_common::scripts::get_full_hub_script_by_path(
email,
username,
path,
http_client,
host,
)
.await?,
windmill_common::scripts::get_full_hub_script_by_path(&email, path, http_client).await?,
))
}

View File

@@ -11,15 +11,12 @@ use windmill_common::error::{self, Error};
pub async fn require_super_admin<'c>(
db: &mut Transaction<'c, Postgres>,
email: Option<String>,
email: &str,
) -> error::Result<()> {
let is_admin = sqlx::query_scalar!(
"SELECT super_admin FROM password WHERE email = $1",
email.as_ref()
)
.fetch_one(db)
.await
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))?;
let is_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
.fetch_one(db)
.await
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))?;
if !is_admin {
Err(Error::NotAuthorized(
"This endpoint require caller to be a super admin".to_owned(),

View File

@@ -60,7 +60,7 @@ async fn list_contextual_variables(
get_reserved_variables(
&w_id,
"q1A0qcPuO00yxioll7iph76N9CJDqn",
&email.unwrap_or_else(|| "no email".to_string()),
&email,
&username,
"017e0ad5-f499-73b6-5488-92a61c5196dd",
format!("u/{username}").as_str(),

View File

@@ -46,6 +46,7 @@ pub fn workspaced_service() -> Router {
.route("/get_settings", get(get_settings))
.route("/edit_slack_command", post(edit_slack_command))
.route("/tarball", get(tarball_workspace))
.route("/usage", get(get_usage))
}
pub fn global_service() -> Router {
@@ -74,6 +75,16 @@ pub struct WorkspaceSettings {
pub slack_team_id: Option<String>,
pub slack_name: Option<String>,
pub slack_command_script: Option<String>,
pub slack_email: String,
}
#[derive(FromRow, Serialize, Debug)]
pub struct Usage {
pub workspace_id: String,
pub slack_team_id: Option<String>,
pub slack_name: Option<String>,
pub slack_command_script: Option<String>,
pub slack_email: String,
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug)]
@@ -175,7 +186,7 @@ async fn list_workspaces(
Workspace,
"SELECT workspace.* FROM workspace, usr WHERE usr.workspace_id = workspace.id AND \
usr.email = $1 AND deleted = false",
authed.email.as_ref()
authed.email
)
.fetch_all(&mut tx)
.await?;
@@ -202,6 +213,25 @@ async fn get_settings(
Ok(Json(settings))
}
async fn get_usage(
authed: Authed,
Path(w_id): Path<String>,
Extension(user_db): Extension<UserDB>,
) -> JsonResult<WorkspaceSettings> {
let mut tx = user_db.begin(&authed).await?;
let settings = sqlx::query_as!(
WorkspaceSettings,
"SELECT * FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("getting settings: {e}")))?;
tx.commit().await?;
Ok(Json(settings))
}
async fn edit_slack_command(
authed: Authed,
Extension(db): Extension<DB>,
@@ -225,7 +255,7 @@ async fn edit_slack_command(
"workspaces.edit_command_script",
ActionKind::Update,
&w_id,
Some(&authed.email.unwrap()),
Some(&authed.email),
Some(
[(
"script",
@@ -249,7 +279,7 @@ async fn list_workspaces_as_super_admin(
Authed { email, .. }: Authed,
) -> JsonResult<Vec<Workspace>> {
let mut tx = user_db.begin(&authed).await?;
require_super_admin(&mut tx, email).await?;
require_super_admin(&mut tx, &email).await?;
let (per_page, offset) = paginate(pagination);
let workspaces = sqlx::query_as!(
@@ -268,9 +298,6 @@ async fn user_workspaces(
Extension(db): Extension<DB>,
Authed { email, .. }: Authed,
) -> JsonResult<WorkspaceList> {
let email = email
.ok_or("not a personal token")
.map_err(|x| Error::NotAuthorized(x.to_string()))?;
let mut tx = db.begin().await?;
let workspaces = sqlx::query_as!(
UserWorkspace,
@@ -406,7 +433,7 @@ async fn edit_workspace(
"workspaces.update",
ActionKind::Update,
&w_id,
Some(&authed.email.unwrap()),
Some(&authed.email),
Some(
[(
"domain",
@@ -447,7 +474,7 @@ async fn delete_workspace(
"workspaces.delete",
ActionKind::Update,
&w_id,
Some(&email.unwrap_or("noemail".to_string())),
Some(&email),
None,
)
.await?;

View File

@@ -208,11 +208,9 @@ pub fn to_hex_string(i: &i64) -> String {
#[cfg(feature = "reqwest")]
pub async fn get_hub_script_by_path(
email: Option<String>,
username: String,
email: &str,
path: StripPath,
http_client: reqwest::Client,
host: String,
) -> crate::error::Result<String> {
use crate::{
error::{to_anyhow, Error},
@@ -228,8 +226,6 @@ pub async fn get_hub_script_by_path(
http_client,
&format!("https://hub.windmill.dev/raw/{path}.ts"),
email,
username,
host,
true,
)
.await?
@@ -241,11 +237,9 @@ pub async fn get_hub_script_by_path(
#[cfg(feature = "reqwest")]
pub async fn get_full_hub_script_by_path(
email: Option<String>,
username: String,
email: &str,
path: StripPath,
http_client: reqwest::Client,
host: String,
) -> crate::error::Result<HubScript> {
use crate::{
error::{to_anyhow, Error},
@@ -261,8 +255,6 @@ pub async fn get_full_hub_script_by_path(
http_client,
&format!("https://hub.windmill.dev/raw2/{path}"),
email,
username,
host,
true,
)
.await?

View File

@@ -80,11 +80,9 @@ pub fn get_owner_from_path(path: &str) -> String {
pub async fn list_elems_from_hub(
http_client: reqwest::Client,
url: &str,
email: Option<String>,
username: String,
host: String,
email: &str,
) -> Result<serde_json::Value> {
let rows = http_get_from_hub(http_client, url, email, username, host, false)
let rows = http_get_from_hub(http_client, url, email, false)
.await?
.json::<serde_json::Value>()
.await
@@ -96,9 +94,7 @@ pub async fn list_elems_from_hub(
pub async fn http_get_from_hub(
http_client: reqwest::Client,
url: &str,
email: Option<String>,
username: String,
host: String,
email: &str,
plain: bool,
) -> Result<reqwest::Response> {
let response = http_client
@@ -111,9 +107,7 @@ pub async fn http_get_from_hub(
"application/json"
},
)
.header("X-email", email.unwrap_or_else(|| "".to_string()))
.header("X-username", username)
.header("X-hostname", host)
.header("X-email", email)
.send()
.await
.map_err(crate::error::to_anyhow)?;

View File

@@ -10,7 +10,7 @@ use std::{collections::HashMap, str::FromStr};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres, Transaction};
use tracing::instrument;
use tracing::{instrument, Instrument};
use ulid::Ulid;
use uuid::Uuid;
use windmill_audit::{audit_log, ActionKind};
@@ -39,10 +39,11 @@ lazy_static::lazy_static! {
"Total number of jobs pulled from the queue."
)
.unwrap();
pub static ref CLOUD_HOSTED: bool = std::env::var("CLOUD_HOSTED").is_ok();
}
const MAX_NB_OF_JOBS_IN_Q_PER_USER: i64 = 10;
const MAX_DURATION_LAST_1200: std::time::Duration = std::time::Duration::from_secs(900);
const MAX_FREE_EXECS: i32 = 1000;
pub async fn cancel_job<'c>(
username: &str,
@@ -245,6 +246,7 @@ pub async fn push<'c>(
job_payload: JobPayload,
args: serde_json::Map<String, serde_json::Value>,
user: &str,
email: &str,
permissioned_as: String,
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
schedule_path: Option<String>,
@@ -257,54 +259,60 @@ pub async fn push<'c>(
let args_json = serde_json::Value::Object(args);
let job_id: Uuid = Ulid::new().into();
let premium_workspace =
sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", workspace_id)
// we track only non flow steps
let usage = if !matches!(
job_payload,
JobPayload::Flow(_) | JobPayload::RawFlow { .. }
) {
sqlx::query_scalar!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1
RETURNING usage.usage",
email)
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("updating usage: {e}")))?
} else {
sqlx::query_scalar!("
SELECT usage.usage + 1 FROM usage
WHERE is_workspace = false AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)")
.fetch_optional(&mut tx)
.await?
.flatten()
.unwrap_or(0)
};
if *CLOUD_HOSTED {
let premium_workspace =
sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", workspace_id)
.fetch_one(&mut tx)
.await
.map_err(|e| {
Error::InternalErr(format!("fetching if {workspace_id} is premium: {e}"))
})?;
if !premium_workspace {
let is_super_admin =
sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
.fetch_optional(&mut tx)
.await?
.unwrap_or(false);
if !is_super_admin && usage > MAX_FREE_EXECS {
return Err(error::Error::BadRequest(format!(
"User {email} has exceeded the free usage limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
)));
}
} else {
sqlx::query_scalar!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, true, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1
RETURNING usage.usage",
workspace_id)
.fetch_one(&mut tx)
.await
.map_err(|e| {
Error::InternalErr(format!("fetching if {workspace_id} is premium: {e}"))
})?;
if !premium_workspace && std::env::var("CLOUD_HOSTED").is_ok() {
let rate_limiting_queue = sqlx::query_scalar!(
"SELECT COUNT(id) FROM queue WHERE permissioned_as = $1 AND workspace_id = $2",
permissioned_as,
workspace_id
)
.fetch_one(&mut tx)
.await?;
if let Some(nb_jobs) = rate_limiting_queue {
if nb_jobs > MAX_NB_OF_JOBS_IN_Q_PER_USER {
return Err(error::Error::ExecutionErr(format!(
"You have exceeded the number of authorized elements of queue at any given \
time: {}",
MAX_NB_OF_JOBS_IN_Q_PER_USER
)));
}
}
let rate_limiting_duration_ms = sqlx::query_scalar!(
"
SELECT SUM(duration_ms)
FROM completed_job
WHERE permissioned_as = $1
AND created_at > NOW() - INTERVAL '1200 seconds'
AND workspace_id = $2",
permissioned_as,
workspace_id
)
.fetch_one(&mut tx)
.await?;
if let Some(sum_duration_ms) = rate_limiting_duration_ms {
if sum_duration_ms as u128 > MAX_DURATION_LAST_1200.as_millis() {
return Err(error::Error::ExecutionErr(format!(
"You have exceeded the scripts cumulative duration limit over the last 20m \
which is: {} seconds",
MAX_DURATION_LAST_1200.as_secs()
)));
}
.map_err(|e| Error::InternalErr(format!("updating usage: {e}")))?;
}
}
@@ -334,14 +342,7 @@ pub async fn push<'c>(
)
}
JobPayload::ScriptHub { path } => {
let email = sqlx::query_scalar!(
"SELECT email FROM usr WHERE username = $1 AND workspace_id = $2",
user,
workspace_id
)
.fetch_optional(&mut tx)
.await?;
let script = get_hub_script(path.clone(), email, user).await?;
let script = get_hub_script(path.clone(), email).await?;
(
None,
Some(path),
@@ -466,8 +467,8 @@ pub async fn push<'c>(
"INSERT INTO queue
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20) \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21) \
RETURNING id",
workspace_id,
job_id,
@@ -488,7 +489,8 @@ pub async fn push<'c>(
is_flow_step,
language: ScriptLang,
same_worker,
pre_run_error.map(|e| e.to_string())
pre_run_error.map(|e| e.to_string()),
email
)
.fetch_one(&mut tx)
.await
@@ -526,6 +528,7 @@ pub async fn push<'c>(
script_path.as_ref().map(|x| x.as_str()),
Some(hm),
)
.instrument(tracing::info_span!("job_run", email = &email))
.await?;
}
Ok((uuid, tx))
@@ -540,20 +543,14 @@ pub fn canceled_job_to_result(job: &QueuedJob) -> String {
format!("Job canceled: {reason} by {canceler}")
}
pub async fn get_hub_script(
path: String,
email: Option<String>,
user: &str,
) -> error::Result<HubScript> {
pub async fn get_hub_script(path: String, email: &str) -> error::Result<HubScript> {
get_full_hub_script_by_path(
email,
user.to_string(),
StripPath(path),
reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.build()
.map_err(to_anyhow)?,
std::env::var("BASE_URL").unwrap_or_else(|_| "".to_string()),
)
.await
.map(|e| e)
@@ -588,6 +585,7 @@ pub struct QueuedJob {
pub language: Option<ScriptLang>,
pub same_worker: bool,
pub pre_run_error: Option<String>,
pub email: String,
}
impl QueuedJob {

View File

@@ -32,6 +32,8 @@ pub struct Schedule {
pub is_flow: bool,
pub args: Option<serde_json::Value>,
pub extra_perms: serde_json::Value,
pub email: String,
pub error: Option<String>,
}
#[derive(Deserialize)]
@@ -100,21 +102,29 @@ pub async fn push_scheduled_job<'c>(
}
};
let (_, tx) = push(
let (_, mut tx) = push(
tx,
&schedule.workspace_id,
payload,
args,
&schedule_to_user(&schedule.path),
&schedule.email,
get_owner_from_path(&schedule.path),
Some(next),
Some(schedule.path),
Some(schedule.path.clone()),
None,
false,
false,
None,
)
.await?;
sqlx::query!(
"UPDATE schedule SET error = NULL WHERE workspace_id = $1 AND path = $2",
&schedule.workspace_id,
&schedule.path
)
.execute(&mut tx)
.await?;
Ok(tx)
}
@@ -123,6 +133,7 @@ pub async fn create_schedule(
w_id: String,
ns: NewSchedule,
username: &str,
email: &str,
) -> Result<String> {
cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
@@ -130,7 +141,7 @@ pub async fn create_schedule(
let schedule = sqlx::query_as!(
Schedule,
"INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, \
is_flow, args, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *",
is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *",
w_id,
ns.path,
ns.schedule,
@@ -140,6 +151,7 @@ pub async fn create_schedule(
ns.is_flow,
ns.args,
ns.enabled.unwrap_or(false),
email
)
.fetch_one(&mut tx)
.await
@@ -360,12 +372,14 @@ pub async fn set_enabled(
path: StripPath,
SetEnabled { enabled }: SetEnabled,
username: &str,
email: &str,
) -> Result<String> {
let path = path.to_path();
let schedule_o = sqlx::query_as!(
Schedule,
"UPDATE schedule SET enabled = $1 WHERE path = $2 AND workspace_id = $3 RETURNING *",
"UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *",
enabled,
email,
path,
w_id
)

View File

@@ -7,11 +7,11 @@
*/
use serde_json::{Map, Value};
use sqlx::{Pool, Postgres, Transaction};
use sqlx::{Pool, Postgres};
use tracing::instrument;
use uuid::Uuid;
use windmill_common::{error::Error, flow_status::FlowStatusModule};
use windmill_queue::{delete_job, schedule::get_schedule_opt, JobKind, QueuedJob};
use windmill_queue::{delete_job, schedule::get_schedule_opt, JobKind, QueuedJob, CLOUD_HOSTED};
#[instrument(level = "trace", skip_all)]
pub async fn add_completed_job_error<E: ToString + std::fmt::Debug>(
@@ -46,6 +46,24 @@ pub fn error_to_result<E: ToString + std::fmt::Debug>(
);
}
fn flatten_jobs(modules: Vec<FlowStatusModule>) -> Vec<Uuid> {
modules
.into_iter()
.filter_map(|m| match m {
FlowStatusModule::Success { job, flow_jobs, .. }
| FlowStatusModule::Failure { job, flow_jobs, .. } => {
if let Some(flow_jobs) = flow_jobs {
Some(flow_jobs)
} else {
Some(vec![job])
}
}
_ => None,
})
.flatten()
.collect::<Vec<_>>()
}
#[instrument(level = "trace", skip_all)]
pub async fn add_completed_job(
db: &Pool<Postgres>,
@@ -60,14 +78,7 @@ pub async fn add_completed_job(
let jobs = queued_job.parse_flow_status().map(|s| {
let mut modules = s.modules;
modules.extend([s.failure_module.module_status]);
modules
.into_iter()
.filter_map(|m| match m {
FlowStatusModule::Success { job, .. }
| FlowStatusModule::Failure { job, .. } => Some(job),
_ => None,
})
.collect::<Vec<_>>()
flatten_jobs(modules)
});
if let Some(jobs) = jobs {
sqlx::query_scalar!(
@@ -85,6 +96,7 @@ pub async fn add_completed_job(
} else {
None
};
let mut tx = db.begin().await?;
let job_id = queued_job.id.clone();
sqlx::query!(
@@ -149,32 +161,69 @@ pub async fn add_completed_job(
.await
.map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?;
let _ = delete_job(db, &queued_job.workspace_id, job_id).await?;
tx.commit().await?;
if duration.unwrap_or(0) > 1000 {
let additional_usage = (duration.unwrap() as i32 / 1000) - 1;
sqlx::query!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
queued_job.email,
additional_usage)
.execute(db)
.await
.map_err(|e| Error::InternalErr(format!("updating usage: {e}")))?;
if *CLOUD_HOSTED {
let w_id = &queued_job.workspace_id;
let premium_workspace =
sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", w_id)
.fetch_one(db)
.await
.map_err(|e| {
Error::InternalErr(format!("fetching if {w_id} is premium: {e}"))
})?;
if premium_workspace {
let _ = sqlx::query!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, true, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
w_id,
additional_usage)
.execute(db)
.await
.map_err(|e| Error::InternalErr(format!("updating usage: {e}")));
}
}
}
if !queued_job.is_flow_step
&& queued_job.job_kind != JobKind::Flow
&& queued_job.job_kind != JobKind::FlowPreview
&& queued_job.schedule_path.is_some()
&& queued_job.script_path.is_some()
{
tx = schedule_again_if_scheduled(
tx,
schedule_again_if_scheduled(
db,
queued_job.schedule_path.as_ref().unwrap(),
queued_job.script_path.as_ref().unwrap(),
&queued_job.workspace_id,
)
.await?;
}
tx.commit().await?;
tracing::debug!("Added completed job {}", queued_job.id);
Ok(queued_job.id)
}
#[instrument(level = "trace", skip_all)]
pub async fn schedule_again_if_scheduled<'c>(
mut tx: Transaction<'c, Postgres>,
pub async fn schedule_again_if_scheduled(
db: &Pool<Postgres>,
schedule_path: &str,
script_path: &str,
w_id: &str,
) -> windmill_common::error::Result<Transaction<'c, Postgres>> {
) -> windmill_common::error::Result<()> {
let mut tx = db.begin().await?;
let schedule = get_schedule_opt(&mut tx, w_id, schedule_path)
.await?
.ok_or_else(|| {
@@ -184,11 +233,11 @@ pub async fn schedule_again_if_scheduled<'c>(
))
})?;
if schedule.enabled && script_path == schedule.script_path {
tx = windmill_queue::schedule::push_scheduled_job(
let res = windmill_queue::schedule::push_scheduled_job(
tx,
windmill_queue::schedule::Schedule {
workspace_id: w_id.to_owned(),
path: schedule.path,
path: schedule.path.clone(),
edited_by: schedule.edited_by,
edited_at: schedule.edited_at,
schedule: schedule.schedule,
@@ -200,10 +249,25 @@ pub async fn schedule_again_if_scheduled<'c>(
.args
.and_then(|e| serde_json::to_value(e).map_or(None, |v| Some(v))),
extra_perms: serde_json::to_value(schedule.extra_perms).expect("hashmap -> json"),
email: schedule.email,
error: None,
},
)
.await?;
.await;
match res {
Ok(tx) => tx.commit().await?,
Err(e) => {
sqlx::query!(
"UPDATE schedule SET enabled = false, error = $1 WHERE workspace_id = $2 AND path = $3",
e.to_string(),
&schedule.workspace_id,
&schedule.path
)
.execute(db)
.await?;
tracing::warn!("Could not schedule job for {}: {}", schedule_path, e);
}
}
}
Ok(tx)
Ok(())
}

View File

@@ -242,10 +242,10 @@ pub async fn create_token_for_owner<'c>(
.await?
.unwrap_or(false);
let expiration = sqlx::query_scalar!(
sqlx::query_scalar!(
"INSERT INTO token
(workspace_id, token, owner, label, expiration, super_admin)
VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6) RETURNING expiration",
VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6)",
&w_id,
token,
owner,
@@ -253,32 +253,7 @@ pub async fn create_token_for_owner<'c>(
expires_in.to_string(),
is_super_admin
)
.fetch_one(&mut tx)
.await?;
let mut truncated_token = token[..10].to_owned();
truncated_token.push_str("*****");
windmill_audit::audit_log(
&mut tx,
&username,
"users.token.create",
windmill_audit::ActionKind::Create,
w_id,
Some(&truncated_token),
Some(
[
Some(("label", label)),
expiration
.map(|x| x.to_string())
.as_ref()
.map(|exp| ("expiration", &exp[..])),
]
.into_iter()
.flatten()
.collect(),
),
)
.execute(&mut tx)
.await?;
Ok((tx, token))
}
@@ -646,6 +621,17 @@ async fn handle_job_error(
keep_job_dir: bool,
base_internal_url: &str,
) {
add_completed_job_error(
db,
&job,
format!("Unexpected error during job execution:\n{err}"),
&err,
metrics.clone(),
)
.await
.map(|(_, m)| m)
.unwrap_or_else(|_| Map::new());
if job.is_flow_step || job.job_kind == JobKind::FlowPreview || job.job_kind == JobKind::Flow {
let (flow, job_status_to_update) = if let Some(parent_job_id) = job.parent_job {
(parent_job_id, job.id)
@@ -693,17 +679,6 @@ async fn handle_job_error(
}
}
}
add_completed_job_error(
db,
&job,
format!("Unexpected error during job execution:\n{err}"),
&err,
metrics,
)
.await
.map(|(_, m)| m)
.unwrap_or_else(|_| Map::new());
tracing::error!(job_id = %job.id, err = err.alt(), "error handling job: {} {} {}", job.id, job.workspace_id, job.created_by);
}
@@ -787,7 +762,7 @@ async fn handle_queued_job(
.await?;
}
tracing::info!(
tracing::debug!(
worker = %worker_name,
job_id = %job.id,
workspace_id = %job.workspace_id,
@@ -1005,7 +980,7 @@ async fn handle_code_execution_job(
.map(|x| format!("{x:?}"))
.unwrap_or_else(|| "NO_LANG".to_string());
tracing::info!(
tracing::debug!(
worker_name = %worker_name,
job_id = %job.id,
workspace_id = %job.workspace_id,
@@ -2103,17 +2078,6 @@ async fn gen_go_mymod(code: &str, job_dir: &str) -> error::Result<()> {
Ok(())
}
// TODO: this really shouldn't be here
pub async fn get_email_from_username(
username: &String,
db: &Pool<Postgres>,
) -> error::Result<Option<String>> {
let email = sqlx::query_scalar!("SELECT email FROM usr WHERE username = $1", username)
.fetch_optional(db)
.await?;
Ok(email)
}
#[tracing::instrument(level = "trace", skip_all)]
async fn get_reserved_variables(
job: &QueuedJob,
@@ -2133,9 +2097,7 @@ async fn get_reserved_variables(
let variables = variables::get_reserved_variables(
&job.workspace_id,
token,
&get_email_from_username(&job.created_by, db)
.await?
.unwrap_or_else(|| "nosuitable@email.xyz".to_string()),
&job.email,
&job.created_by,
&job.id.to_string(),
&job.permissioned_as,

View File

@@ -407,13 +407,15 @@ pub async fn update_flow_status_after_job_completion(
false => false,
};
tx.commit().await?;
if old_status.step == 0
&& !flow_job.is_flow_step
&& flow_job.schedule_path.is_some()
&& flow_job.script_path.is_some()
{
tx = schedule_again_if_scheduled(
tx,
schedule_again_if_scheduled(
db,
flow_job.schedule_path.as_ref().unwrap(),
flow_job.script_path.as_ref().unwrap(),
&w_id,
@@ -421,8 +423,6 @@ pub async fn update_flow_status_after_job_completion(
.await?;
}
tx.commit().await?;
let done = if !should_continue_flow {
let logs = if flow_job.canceled {
"Flow job canceled".to_string()
@@ -1233,6 +1233,7 @@ async fn push_next_flow_job(
payload,
ok.unwrap_or_else(|| Map::new()),
&flow_job.created_by,
&flow_job.email,
flow_job.permissioned_as.to_owned(),
scheduled_for_o,
flow_job.schedule_path.clone(),

View File

@@ -77,7 +77,7 @@
<div class="flex flex-row justify-between">The next runs will be scheduled at:</div>
<ul class="list-disc mx-12">
{#each preview.slice(0, limit) as p}
<li class="mx-2 text-gray-700 text-sm">{displayDate(p)}</li>
<li class="mx-2 text-gray-700 text-sm">{displayDate(p, true)}</li>
{/each}
<li class="text-sm mx-2">...</li>
{#if limit != 10}

View File

@@ -36,7 +36,7 @@
dispatch('click', { item: item?.eventName })
}
}}
class="block whitespace-nowrap hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30 px-4 py-2 text-sm text-gray-700 text-left{item.separatorTop
class="block w-full whitespace-nowrap hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30 px-4 py-2 text-sm text-gray-700 text-left {item.separatorTop
? 'border-t'
: ''} {item.separatorBottom ? 'border-b' : ''} {item.type == 'delete'
? 'text-red-500'
@@ -64,7 +64,7 @@
close()
}
}}
class="block px-4 py-2 text-sm text-gray-700 hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30"
class="block w-full px-4 py-2 text-sm text-gray-700 hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30"
role="menuitem"
tabindex="-1"
id="user-menu-item-{name}-{i}}"
@@ -81,7 +81,7 @@
</a>
{:else}
<span
class="block px-4 py-2 text-sm text-gray-700"
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"
id="user-menu-item-{name}-{i}}"

View File

@@ -28,13 +28,13 @@
fill: #ffffff;
}
.st2 {
fill: #999;
fill: #bcd4fc;
}
.st3 {
fill: black;
fill: #3b82f6;
}
.st4 {
fill: #999;
fill: #b3b3b3;
}
.st5 {
fill: url(#SVGID_1_);

View File

@@ -8,6 +8,7 @@ export interface UserExt {
email: string;
username: string;
is_admin: boolean;
is_super_admin: boolean;
created_at: string;
groups: string[];
pgroups: string[];

View File

@@ -4,7 +4,7 @@ import { FlowService, Script, ScriptService, type Flow, type FlowModule, type Us
import { toast } from '@zerodevx/svelte-toast'
import { get } from 'svelte/store'
import type { Schema, SupportedLanguage } from './common'
import { hubScripts, superadmin, workspaceStore, type UserExt } from './stores'
import { hubScripts, workspaceStore, type UserExt } from './stores'
export function validateUsername(username: string): string {
if (username != '' && !/^\w+$/.test(username)) {
@@ -179,7 +179,7 @@ export function canWrite(
if (!user) {
return false
}
if (user.is_admin || get(superadmin)) {
if (user.is_admin || user.is_super_admin) {
return true
}
let userOwner = `u/${user.username}`

View File

@@ -1,14 +1,13 @@
<script context="module">
export function load({ params }) {
return {
stuff: { title: `Script ${params.hash}` }
stuff: { title: `App ${params.path}` }
}
}
</script>
<script lang="ts">
import { page } from '$app/stores'
import AppEditor from '$lib/components/apps/editor/AppEditor.svelte'
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import CenteredPage from '$lib/components/CenteredPage.svelte'

View File

@@ -17,6 +17,7 @@
import ShareModal from '$lib/components/ShareModal.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import {
faCircle,
faEdit,
faPlus,
faShare,
@@ -28,6 +29,8 @@
import CenteredPage from '$lib/components/CenteredPage.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { Badge, Button, Skeleton } from '$lib/components/common'
import Popover from '$lib/components/Popover.svelte'
import { Icon } from 'svelte-awesome'
type ScheduleW = Schedule & { canWrite: boolean }
@@ -51,7 +54,7 @@
})
loadSchedules()
} catch (err) {
sendUserToast(`Cannot ` + (enabled ? 'disable' : 'enable') + ` schedule: ${err}`, true)
sendUserToast(`Cannot ` + (enabled ? 'enable' : 'disable') + ` schedule: ${err.body}`, true)
loadSchedules()
}
}
@@ -79,13 +82,14 @@
<th>Schedule</th>
<th>Script or Flow</th>
<th>schedule</th>
<th />
<th>off/on</th>
<th>timezone</th>
<th>last edit</th>
<th />
</tr>
<tbody slot="body">
{#each schedules as { path, edited_by, edited_at, schedule, offset_, enabled, script_path, is_flow, extra_perms, canWrite }}
{#each schedules as { path, error, edited_by, edited_at, schedule, offset_, enabled, script_path, is_flow, extra_perms, canWrite }}
<tr class={enabled ? '' : 'bg-gray-50'}>
<td class="max-w-sm"
><a class="break-words text-sm" href="/schedule/add?edit={path}&isFlow={is_flow}"
@@ -101,6 +105,32 @@
></div
>
</td><td><Badge color="blue">{schedule}</Badge></td>
<td>
<div class="w-10">
{#if error}
<Popover notClickable>
<span class="flex h-4 w-4">
<Icon
class="text-red-600 animate-ping absolute inline-flex "
data={faCircle}
scale={0.7}
label="Error during last job scheduling"
/>
<Icon
class="text-red-600 relative inline-flex"
data={faCircle}
scale={0.7}
label="Error during last job scheduling"
/>
</span>
<div slot="text">
The schedule disabled itself because there was an error scheduling the next
job: {error}
</div>
</Popover>
{/if}
</div></td
>
<td>
<Toggle
checked={enabled}
@@ -117,8 +147,8 @@
<td
><span class="text-2xs">By {edited_by} <br />the {displayDate(edited_at)}</span></td
>
<td
><Dropdown
<td>
<Dropdown
placement="bottom-end"
dropdownItems={[
{

View File

@@ -72,7 +72,7 @@
sendUserToast(`Job <a href='/run/${run}?workspace=${$workspaceStore}'>${run}</a> started`)
goto('/run/' + run + '?workspace=' + $workspaceStore)
} catch (err) {
sendUserToast(`Could not create job: ${err}`, true)
sendUserToast(`Could not create job: ${err.body}`, true)
}
}

View File

@@ -229,12 +229,20 @@
<div class="w-10">
{#if refresh_error}
<Popover notClickable>
<Icon
class="text-red-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Error during exchange of the refresh token"
/>
<span class="flex h-4 w-4">
<Icon
class="text-red-600 animate-ping absolute inline-flex "
data={faCircle}
scale={0.7}
label="Error during exchange of the refresh token"
/>
<Icon
class="text-red-600 relative inline-flex"
data={faCircle}
scale={0.7}
label="Error during exchange of the refresh token"
/>
</span>
<div slot="text">
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>