mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-26 00:01:37 +00:00
cargo fmt
This commit is contained in:
@@ -1,7 +1,2 @@
|
||||
imports_granularity = "Crate"
|
||||
max_width = 100
|
||||
use_small_heuristics = "Default"
|
||||
indent_style = "Block"
|
||||
fn_single_line = false
|
||||
force_multiline_blocks = true
|
||||
format_strings = true
|
||||
|
||||
+10
-5
@@ -228,7 +228,8 @@ async fn create_flow(
|
||||
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::text::json)",
|
||||
"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, \
|
||||
schema) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::text::json)",
|
||||
w_id,
|
||||
nf.path,
|
||||
nf.summary,
|
||||
@@ -267,7 +268,8 @@ async fn check_schedule_conflict<'c>(
|
||||
path: &str,
|
||||
) -> error::Result<()> {
|
||||
let exists_flow = sqlx::query_scalar!(
|
||||
"SELECT EXISTS (SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2 AND path != script_path)",
|
||||
"SELECT EXISTS (SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2 AND path != \
|
||||
script_path)",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
@@ -276,7 +278,8 @@ async fn check_schedule_conflict<'c>(
|
||||
.unwrap_or(false);
|
||||
if exists_flow {
|
||||
return Err(error::Error::BadConfig(format!(
|
||||
"A flow cannot have the same path as a schedule if the schedule does not trigger that same flow: {path}",
|
||||
"A flow cannot have the same path as a schedule if the schedule does not trigger that \
|
||||
same flow: {path}",
|
||||
)));
|
||||
};
|
||||
Ok(())
|
||||
@@ -295,7 +298,8 @@ async fn update_flow(
|
||||
|
||||
let schema = nf.schema.map(|x| x.0);
|
||||
let flow = sqlx::query_scalar!(
|
||||
"UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = $6, schema = $7 WHERE path = $8 AND workspace_id = $9 RETURNING path",
|
||||
"UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, \
|
||||
edited_at = $6, schema = $7 WHERE path = $8 AND workspace_id = $9 RETURNING path",
|
||||
nf.path,
|
||||
nf.summary,
|
||||
nf.description,
|
||||
@@ -358,7 +362,8 @@ async fn exists_flow_by_path(
|
||||
let path = path.to_path();
|
||||
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter'))",
|
||||
"SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id \
|
||||
= 'starter'))",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
|
||||
@@ -46,7 +46,8 @@ async fn add_granular_acl(
|
||||
|
||||
let identifier = if kind == "group_" { "name" } else { "path" };
|
||||
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
|
||||
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), true) WHERE {identifier} = $2 AND workspace_id = $3 RETURNING extra_perms"
|
||||
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \
|
||||
true) WHERE {identifier} = $2 AND workspace_id = $3 RETURNING extra_perms"
|
||||
))
|
||||
.bind(write.unwrap_or(false))
|
||||
.bind(path)
|
||||
@@ -74,7 +75,8 @@ async fn remove_granular_acl(
|
||||
|
||||
let identifier = if kind == "group_" { "name" } else { "path" };
|
||||
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
|
||||
"UPDATE {kind} SET extra_perms = extra_perms - $1 WHERE {identifier} = $2 AND workspace_id = $3 RETURNING extra_perms"
|
||||
"UPDATE {kind} SET extra_perms = extra_perms - $1 WHERE {identifier} = $2 AND \
|
||||
workspace_id = $3 RETURNING extra_perms"
|
||||
))
|
||||
.bind(owner)
|
||||
.bind(path)
|
||||
|
||||
+56
-36
@@ -13,19 +13,17 @@ use sqlx::{query_scalar, Postgres, Transaction};
|
||||
use std::collections::HashMap;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::error::to_anyhow;
|
||||
use crate::scripts::{get_hub_script_by_path, ScriptLang};
|
||||
use crate::worker_flow::init_flow_status;
|
||||
use crate::{
|
||||
audit::{audit_log, ActionKind},
|
||||
db::{UserDB, DB},
|
||||
error,
|
||||
error::Error,
|
||||
error::{to_anyhow, Error},
|
||||
flows::FlowValue,
|
||||
schedule::get_schedule_opt,
|
||||
scripts::ScriptHash,
|
||||
scripts::{get_hub_script_by_path, ScriptHash, ScriptLang},
|
||||
users::{owner_to_token_owner, Authed},
|
||||
utils::{require_admin, Pagination, StripPath},
|
||||
worker_flow::init_flow_status,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -269,8 +267,10 @@ pub async fn get_latest_hash_for_path<'c>(
|
||||
script_path: &str,
|
||||
) -> error::Result<ScriptHash> {
|
||||
let script_hash_o = sqlx::query_scalar!(
|
||||
"select hash from script where path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND
|
||||
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')) AND
|
||||
"select hash from script where path = $1 AND (workspace_id = $2 OR workspace_id = \
|
||||
'starter') AND
|
||||
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR \
|
||||
workspace_id = 'starter')) AND
|
||||
deleted = false",
|
||||
script_path,
|
||||
w_id
|
||||
@@ -319,7 +319,8 @@ pub async fn get_path_for_hash<'c>(
|
||||
hash: i64,
|
||||
) -> error::Result<String> {
|
||||
let path = sqlx::query_scalar!(
|
||||
"select path from script where hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
|
||||
"select path from script where hash = $1 AND (workspace_id = $2 OR workspace_id = \
|
||||
'starter')",
|
||||
hash,
|
||||
w_id
|
||||
)
|
||||
@@ -703,9 +704,8 @@ async fn cancel_job(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let job_option = sqlx::query_scalar!(
|
||||
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 \
|
||||
WHERE id = $3 AND schedule_path IS NULL AND workspace_id = $4\
|
||||
RETURNING id",
|
||||
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 WHERE id = $3 \
|
||||
AND schedule_path IS NULL AND workspace_id = $4RETURNING id",
|
||||
&authed.username,
|
||||
reason,
|
||||
id,
|
||||
@@ -756,7 +756,8 @@ async fn delete_completed_job(
|
||||
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let job_o = sqlx::query_as::<_, CompletedJob>(
|
||||
"UPDATE completed_job SET logs = '', deleted = true WHERE id = $1 AND workspace_id = $2 RETURNING *",
|
||||
"UPDATE completed_job SET logs = '', deleted = true WHERE id = $1 AND workspace_id = $2 \
|
||||
RETURNING *",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(&w_id)
|
||||
@@ -821,7 +822,8 @@ async fn get_job_update(
|
||||
}))
|
||||
} else {
|
||||
let logs = query_scalar!(
|
||||
"SELECT substr(logs, $1) as logs FROM completed_job WHERE workspace_id = $2 AND id = $3",
|
||||
"SELECT substr(logs, $1) as logs FROM completed_job WHERE workspace_id = $2 AND id = \
|
||||
$3",
|
||||
log_offset,
|
||||
&w_id,
|
||||
&id
|
||||
@@ -1075,29 +1077,41 @@ pub async fn push<'c>(
|
||||
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)));
|
||||
"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 = sqlx::query_scalar!(
|
||||
"SELECT SUM(duration) FROM completed_job WHERE created_by = $1 AND created_at > NOW() - INTERVAL '1200 seconds' AND workspace_id = $2",
|
||||
user,
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
"SELECT SUM(duration) FROM completed_job WHERE created_by = $1 AND created_at > NOW() \
|
||||
- INTERVAL '1200 seconds' AND workspace_id = $2",
|
||||
user,
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
|
||||
if let Some(sum_duration) = rate_limiting_duration {
|
||||
if sum_duration > MAX_DURATION_LAST_1200 {
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"You have exceeded the scripts cumulative duration limit over the last 20m which is: {}", MAX_DURATION_LAST_1200)));
|
||||
"You have exceeded the scripts cumulative duration limit over the last 20m \
|
||||
which is: {}",
|
||||
MAX_DURATION_LAST_1200
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (script_hash, script_path, raw_code, job_kind, raw_flow, language) = match job_payload {
|
||||
JobPayload::ScriptHash { hash, path } => {
|
||||
let language = sqlx::query_scalar!("SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", hash.0, workspace_id)
|
||||
let language = sqlx::query_scalar!(
|
||||
"SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND \
|
||||
(workspace_id = $2 OR workspace_id = 'starter')",
|
||||
hash.0,
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
(
|
||||
@@ -1168,11 +1182,15 @@ pub async fn push<'c>(
|
||||
(None, path, None, JobKind::FlowPreview, Some(value), None)
|
||||
}
|
||||
JobPayload::Flow(flow) => {
|
||||
let value_json = sqlx::query_scalar!("SELECT value FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
|
||||
flow, workspace_id)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.ok_or_else(|| Error::InternalErr(format!("not found flow at path {:?}", flow)))?;
|
||||
let value_json = sqlx::query_scalar!(
|
||||
"SELECT value FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = \
|
||||
'starter')",
|
||||
flow,
|
||||
workspace_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.ok_or_else(|| Error::InternalErr(format!("not found flow at path {:?}", flow)))?;
|
||||
let value = serde_json::from_value::<FlowValue>(value_json).map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"could not convert json to flow for {flow}: {err:?}"
|
||||
@@ -1186,8 +1204,10 @@ pub async fn push<'c>(
|
||||
let uuid = sqlx::query_scalar!(
|
||||
"INSERT INTO queue
|
||||
(workspace_id, id, parent_job, created_by, permissioned_as, scheduled_for,
|
||||
script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id",
|
||||
script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, \
|
||||
flow_status, is_flow_step, language)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) \
|
||||
RETURNING id",
|
||||
workspace_id,
|
||||
job_id,
|
||||
parent_job,
|
||||
@@ -1284,13 +1304,13 @@ pub async fn add_completed_job(
|
||||
.num_seconds() as i32;
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO completed_job as cj
|
||||
(workspace_id, id, parent_job, created_by, created_at, duration, success, script_hash, script_path, \
|
||||
args, result, logs,
|
||||
raw_code, canceled, canceled_by, canceled_reason, job_kind, schedule_path, permissioned_as, flow_status, raw_flow, \
|
||||
is_flow_step, is_skipped)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) \
|
||||
ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12) \
|
||||
RETURNING id",
|
||||
(workspace_id, id, parent_job, created_by, created_at, duration, success, script_hash, \
|
||||
script_path, args, result, logs,
|
||||
raw_code, canceled, canceled_by, canceled_reason, job_kind, schedule_path, \
|
||||
permissioned_as, flow_status, raw_flow, is_flow_step, is_skipped)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, \
|
||||
$18, $19, $20, $21, $22, $23) ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, \
|
||||
logs = concat(cj.logs, $12) RETURNING id",
|
||||
queued_job.workspace_id,
|
||||
queued_job.id,
|
||||
queued_job.parent_job,
|
||||
|
||||
+10
-16
@@ -5,21 +5,13 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use deno_core::op;
|
||||
use deno_core::serde_v8;
|
||||
use deno_core::v8;
|
||||
use deno_core::v8::IsolateHandle;
|
||||
use deno_core::Extension;
|
||||
use deno_core::JsRuntime;
|
||||
use deno_core::RuntimeOptions;
|
||||
use deno_core::{op, serde_v8, v8, v8::IsolateHandle, Extension, JsRuntime, RuntimeOptions};
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::timeout;
|
||||
use tokio::{sync::oneshot, time::timeout};
|
||||
|
||||
use crate::client;
|
||||
use crate::error::Error;
|
||||
use crate::{client, error::Error};
|
||||
|
||||
pub struct EvalCreds {
|
||||
pub workspace: String,
|
||||
@@ -193,11 +185,13 @@ async function resource(path) {{
|
||||
}})()
|
||||
"#,
|
||||
env.into_iter()
|
||||
.map(|(a, b)| format!(
|
||||
"let {a} = {};\n",
|
||||
serde_json::to_string(&b)
|
||||
.unwrap_or_else(|_| "\"error serializing value\"".to_string())
|
||||
))
|
||||
.map(|(a, b)| {
|
||||
format!(
|
||||
"let {a} = {};\n",
|
||||
serde_json::to_string(&b)
|
||||
.unwrap_or_else(|_| "\"error serializing value\"".to_string())
|
||||
)
|
||||
})
|
||||
.join(""),
|
||||
);
|
||||
tracing::debug!("{}", code);
|
||||
|
||||
+3
-1
@@ -76,7 +76,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
.unwrap_or(false);
|
||||
|
||||
tracing::info!(
|
||||
"DISABLE_NSJAIL: {disable_nsjail}, DISABLE_NUSER: {disable_nuser}, BASE_URL: {base_url}, SLEEP_QUEUE: {sleep_queue}, NUM_WORKERS: {num_workers}, TIMEOUT: {timeout}"
|
||||
"DISABLE_NSJAIL: {disable_nsjail}, DISABLE_NUSER: {disable_nuser}, BASE_URL: \
|
||||
{base_url}, SLEEP_QUEUE: {sleep_queue}, NUM_WORKERS: {num_workers}, TIMEOUT: \
|
||||
{timeout}"
|
||||
);
|
||||
windmill::run_workers(
|
||||
db.clone(),
|
||||
|
||||
+41
-30
@@ -1,37 +1,39 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::{collections::HashMap, fmt::Debug};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::Bytes;
|
||||
use axum::extract::{Extension, FromRequest, Path, Query, RequestParts};
|
||||
use axum::response::Redirect;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{async_trait, Json, Router};
|
||||
use axum::{
|
||||
async_trait,
|
||||
body::Bytes,
|
||||
extract::{Extension, FromRequest, Path, Query, RequestParts},
|
||||
response::Redirect,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use chrono::{Duration, Utc};
|
||||
use hyper::StatusCode;
|
||||
use itertools::Itertools;
|
||||
|
||||
use oauth2::{Client as OClient, *};
|
||||
use reqwest::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use slack_http_verifier::SlackVerifier;
|
||||
use sqlx::{Postgres, Transaction};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::{fs::File, io::AsyncReadExt};
|
||||
use tower_cookies::{Cookie, Cookies};
|
||||
|
||||
use crate::audit::{audit_log, ActionKind};
|
||||
use crate::db::{UserDB, DB};
|
||||
use crate::error::{self, to_anyhow, Error, Result};
|
||||
use crate::jobs;
|
||||
use crate::jobs::{get_latest_hash_for_path, JobPayload};
|
||||
use crate::users::Authed;
|
||||
use crate::utils::not_found_if_none;
|
||||
use crate::variables::{build_crypt, encrypt};
|
||||
use crate::workspaces::WorkspaceSettings;
|
||||
use crate::BaseUrl;
|
||||
use crate::{
|
||||
audit::{audit_log, ActionKind},
|
||||
db::{UserDB, DB},
|
||||
error::{self, to_anyhow, Error, Result},
|
||||
jobs,
|
||||
jobs::{get_latest_hash_for_path, JobPayload},
|
||||
users::Authed,
|
||||
utils::not_found_if_none,
|
||||
variables::{build_crypt, encrypt},
|
||||
workspaces::WorkspaceSettings,
|
||||
BaseUrl,
|
||||
};
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
@@ -278,7 +280,8 @@ async fn create_account(
|
||||
|
||||
let expires_at = chrono::Utc::now() + Duration::seconds(payload.expires_in);
|
||||
let id = sqlx::query_scalar!(
|
||||
"INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, $2, $3, $4, $5) RETURNING id",
|
||||
"INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, \
|
||||
$2, $3, $4, $5) RETURNING id",
|
||||
w_id,
|
||||
payload.client,
|
||||
payload.owner,
|
||||
@@ -469,8 +472,12 @@ pub async fn _refresh_token<'c>(
|
||||
.unwrap(),
|
||||
);
|
||||
sqlx::query!(
|
||||
"UPDATE account SET refresh_token = $1, expires_at = $2 WHERE workspace_id = $3 AND id = $4",
|
||||
token.refresh_token.map(|x| x.to_string()).unwrap_or(account.refresh_token),
|
||||
"UPDATE account SET refresh_token = $1, expires_at = $2 WHERE workspace_id = $3 AND id = \
|
||||
$4",
|
||||
token
|
||||
.refresh_token
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or(account.refresh_token),
|
||||
expires_at,
|
||||
w_id,
|
||||
id
|
||||
@@ -548,7 +555,8 @@ async fn set_workspace_slack(
|
||||
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",
|
||||
VALUES ($1, $2, $3) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, \
|
||||
slack_name = $3",
|
||||
&w_id,
|
||||
token.team_id,
|
||||
token.team_name
|
||||
@@ -720,15 +728,18 @@ async fn login_callback(
|
||||
crate::users::create_session_token(&email, super_admin, &mut tx, cookies).await?;
|
||||
} else {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"an user with the email associated to this login exists but with a different login type {login_type}")
|
||||
));
|
||||
"an user with the email associated to this login exists but with a different \
|
||||
login type {login_type}"
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
let user = get_user_info(&http_client, &client_name, &token).await?;
|
||||
|
||||
sqlx::query(
|
||||
&format!("INSERT INTO password (email, name, company, login_type, verified) VALUES ($1, $2, $3, '{}', true)", &client_name)
|
||||
)
|
||||
sqlx::query(&format!(
|
||||
"INSERT INTO password (email, name, company, login_type, verified) VALUES ($1, \
|
||||
$2, $3, '{}', true)",
|
||||
&client_name
|
||||
))
|
||||
.bind(&email)
|
||||
.bind(&user.name)
|
||||
.bind(user.company)
|
||||
|
||||
+63
-60
@@ -127,8 +127,7 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
|
||||
}
|
||||
}
|
||||
|
||||
use swc_common::sync::Lrc;
|
||||
use swc_common::{FileName, SourceMap};
|
||||
use swc_common::{sync::Lrc, FileName, SourceMap};
|
||||
use swc_ecma_ast::{
|
||||
AssignPat, BindingIdent, Decl, ExportDecl, FnDecl, Ident, ModuleDecl, ModuleItem, Pat,
|
||||
TsArrayType, TsEntityName, TsKeywordTypeKind, TsType, TsTypeRef,
|
||||
@@ -240,70 +239,74 @@ fn binding_ident_to_arg(
|
||||
id.sym.to_string(),
|
||||
type_ann
|
||||
.as_ref()
|
||||
.map(|x| match &*x.type_ann {
|
||||
TsType::TsKeywordType(t) => match t.kind {
|
||||
TsKeywordTypeKind::TsObjectKeyword => Typ::Dict,
|
||||
TsKeywordTypeKind::TsBooleanKeyword => Typ::Bool,
|
||||
TsKeywordTypeKind::TsBigIntKeyword => Typ::Int,
|
||||
TsKeywordTypeKind::TsNumberKeyword => Typ::Float,
|
||||
TsKeywordTypeKind::TsStringKeyword => Typ::Str,
|
||||
_ => Typ::Unknown,
|
||||
},
|
||||
// TODO: we can do better here and extract the inner type of array
|
||||
TsType::TsArrayType(TsArrayType { span: _, elem_type }) => {
|
||||
match &**elem_type {
|
||||
TsType::TsTypeRef(TsTypeRef {
|
||||
span: _,
|
||||
type_name:
|
||||
TsEntityName::Ident(Ident {
|
||||
span: _,
|
||||
sym,
|
||||
optional: _,
|
||||
}),
|
||||
type_params: _,
|
||||
}) => match sym.to_string().as_str() {
|
||||
"Base64" => Typ::List(InnerTyp::Bytes),
|
||||
"Email" => Typ::List(InnerTyp::Email),
|
||||
"bigint" => Typ::List(InnerTyp::Int),
|
||||
"number" => Typ::List(InnerTyp::Float),
|
||||
.map(|x| {
|
||||
match &*x.type_ann {
|
||||
TsType::TsKeywordType(t) => match t.kind {
|
||||
TsKeywordTypeKind::TsObjectKeyword => Typ::Dict,
|
||||
TsKeywordTypeKind::TsBooleanKeyword => Typ::Bool,
|
||||
TsKeywordTypeKind::TsBigIntKeyword => Typ::Int,
|
||||
TsKeywordTypeKind::TsNumberKeyword => Typ::Float,
|
||||
TsKeywordTypeKind::TsStringKeyword => Typ::Str,
|
||||
_ => Typ::Unknown,
|
||||
},
|
||||
// TODO: we can do better here and extract the inner type of array
|
||||
TsType::TsArrayType(TsArrayType { span: _, elem_type }) => {
|
||||
match &**elem_type {
|
||||
TsType::TsTypeRef(TsTypeRef {
|
||||
span: _,
|
||||
type_name:
|
||||
TsEntityName::Ident(Ident {
|
||||
span: _,
|
||||
sym,
|
||||
optional: _,
|
||||
}),
|
||||
type_params: _,
|
||||
}) => match sym.to_string().as_str() {
|
||||
"Base64" => Typ::List(InnerTyp::Bytes),
|
||||
"Email" => Typ::List(InnerTyp::Email),
|
||||
"bigint" => Typ::List(InnerTyp::Int),
|
||||
"number" => Typ::List(InnerTyp::Float),
|
||||
_ => Typ::List(InnerTyp::Str),
|
||||
},
|
||||
//TsType::TsKeywordType(())
|
||||
_ => Typ::List(InnerTyp::Str),
|
||||
},
|
||||
//TsType::TsKeywordType(())
|
||||
_ => Typ::List(InnerTyp::Str),
|
||||
}
|
||||
}
|
||||
}
|
||||
TsType::TsTypeRef(TsTypeRef {
|
||||
span: _,
|
||||
type_name,
|
||||
type_params,
|
||||
}) => {
|
||||
let sym = match type_name {
|
||||
TsEntityName::Ident(Ident {
|
||||
span: _,
|
||||
sym,
|
||||
optional: _,
|
||||
}) => sym,
|
||||
TsEntityName::TsQualifiedName(p) => &*p.right.sym,
|
||||
};
|
||||
match sym.to_string().as_str() {
|
||||
"Resource" => Typ::Resource(
|
||||
type_params
|
||||
.as_ref()
|
||||
.and_then(|x| {
|
||||
x.params.get(0).and_then(|y| {
|
||||
y.as_ts_lit_type().and_then(|z| {
|
||||
z.lit.as_str().map(|a| a.to_owned().value.to_string())
|
||||
TsType::TsTypeRef(TsTypeRef {
|
||||
span: _,
|
||||
type_name,
|
||||
type_params,
|
||||
}) => {
|
||||
let sym = match type_name {
|
||||
TsEntityName::Ident(Ident {
|
||||
span: _,
|
||||
sym,
|
||||
optional: _,
|
||||
}) => sym,
|
||||
TsEntityName::TsQualifiedName(p) => &*p.right.sym,
|
||||
};
|
||||
match sym.to_string().as_str() {
|
||||
"Resource" => Typ::Resource(
|
||||
type_params
|
||||
.as_ref()
|
||||
.and_then(|x| {
|
||||
x.params.get(0).and_then(|y| {
|
||||
y.as_ts_lit_type().and_then(|z| {
|
||||
z.lit
|
||||
.as_str()
|
||||
.map(|a| a.to_owned().value.to_string())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
),
|
||||
"Base64" => Typ::Bytes,
|
||||
"Email" => Typ::Email,
|
||||
_ => Typ::Unknown,
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
),
|
||||
"Base64" => Typ::Bytes,
|
||||
"Email" => Typ::Email,
|
||||
_ => Typ::Unknown,
|
||||
}
|
||||
}
|
||||
_ => Typ::Unknown,
|
||||
}
|
||||
_ => Typ::Unknown,
|
||||
})
|
||||
.unwrap_or(Typ::Unknown),
|
||||
))
|
||||
|
||||
@@ -140,7 +140,8 @@ async fn get_resource(
|
||||
|
||||
let resource_o = sqlx::query_as!(
|
||||
Resource,
|
||||
"SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
|
||||
"SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = \
|
||||
'starter')",
|
||||
path.to_owned(),
|
||||
&w_id
|
||||
)
|
||||
@@ -179,7 +180,8 @@ async fn get_resource_value(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let value_o = sqlx::query_scalar!(
|
||||
"SELECT value from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
|
||||
"SELECT value from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = \
|
||||
'starter')",
|
||||
path.to_owned(),
|
||||
&w_id
|
||||
)
|
||||
@@ -312,9 +314,14 @@ async fn list_resource_types(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<ResourceType>> {
|
||||
let rows = sqlx::query_as!(ResourceType, "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name", &w_id)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
let rows = sqlx::query_as!(
|
||||
ResourceType,
|
||||
"SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER \
|
||||
BY name",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
@@ -323,9 +330,13 @@ async fn list_resource_types_names(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<String>> {
|
||||
let rows = sqlx::query_scalar!("SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name", &w_id)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
let rows = sqlx::query_scalar!(
|
||||
"SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') \
|
||||
ORDER BY name",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
@@ -339,7 +350,8 @@ async fn get_resource_type(
|
||||
|
||||
let resource_type_o = sqlx::query_as!(
|
||||
ResourceType,
|
||||
"SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
|
||||
"SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = \
|
||||
'starter')",
|
||||
&name,
|
||||
&w_id
|
||||
)
|
||||
|
||||
+11
-6
@@ -133,8 +133,10 @@ async fn create_schedule(
|
||||
|
||||
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
|
||||
|
||||
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 *",
|
||||
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 *",
|
||||
w_id,
|
||||
ns.path,
|
||||
ns.schedule,
|
||||
@@ -195,9 +197,10 @@ async fn check_flow_conflict<'c>(
|
||||
.unwrap_or(false);
|
||||
if exists_flow {
|
||||
return Err(error::Error::BadConfig(format!(
|
||||
"If a schedule has the same path as or a flow, it must be its primary schedule and hence can only trigger it.
|
||||
"If a schedule has the same path as or a flow, it must be its primary schedule \
|
||||
and hence can only trigger it.
|
||||
However the provided path is: {script_path} and is_flow is: {is_flow}",
|
||||
)));
|
||||
)));
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
@@ -233,8 +236,10 @@ async fn edit_schedule(
|
||||
check_flow_conflict(&mut tx, &w_id, &path, es.is_flow, &es.script_path).await?;
|
||||
|
||||
clear_schedule(&mut tx, path).await?;
|
||||
let schedule = sqlx::query_as!(Schedule,
|
||||
"UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path = $5 AND workspace_id = $6 RETURNING *",
|
||||
let schedule = sqlx::query_as!(
|
||||
Schedule,
|
||||
"UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path \
|
||||
= $5 AND workspace_id = $6 RETURNING *",
|
||||
es.schedule,
|
||||
es.script_path,
|
||||
es.is_flow,
|
||||
|
||||
+20
-13
@@ -342,7 +342,7 @@ async fn create_script(
|
||||
if let Some(clashing_hash) = clashing_hash_o {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"A script with hash {} with same parent_hash has been found. However, the \
|
||||
lineage must be linear: no 2 scripts can have the same parent",
|
||||
lineage must be linear: no 2 scripts can have the same parent",
|
||||
ScriptHash(clashing_hash)
|
||||
)));
|
||||
};
|
||||
@@ -395,9 +395,9 @@ async fn create_script(
|
||||
};
|
||||
//::text::json is to ensure we use serde_json with preserve order
|
||||
sqlx::query!(
|
||||
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, \
|
||||
created_by, schema, is_template, extra_perms, lock, language, is_trigger) VALUES \
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14)",
|
||||
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, \
|
||||
content, created_by, schema, is_template, extra_perms, lock, language, is_trigger) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14)",
|
||||
&w_id,
|
||||
&hash.0,
|
||||
ns.path,
|
||||
@@ -515,8 +515,10 @@ async fn get_script_by_path(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let script_o = sqlx::query_as::<_, Script>(
|
||||
"SELECT * FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND
|
||||
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND (workspace_id = $2 OR workspace_id = 'starter'))",
|
||||
"SELECT * FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') \
|
||||
AND
|
||||
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND \
|
||||
(workspace_id = $2 OR workspace_id = 'starter'))",
|
||||
)
|
||||
.bind(path)
|
||||
.bind(w_id)
|
||||
@@ -535,9 +537,12 @@ async fn exists_script_by_path(
|
||||
let path = path.to_path();
|
||||
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND
|
||||
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')))",
|
||||
path, w_id
|
||||
"SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR \
|
||||
workspace_id = 'starter') AND
|
||||
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 \
|
||||
OR workspace_id = 'starter')))",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
@@ -586,8 +591,10 @@ async fn get_deployment_status(
|
||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||
) -> JsonResult<DeploymentStatus> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let status_o: Option<DeploymentStatus> = sqlx::query_as!(DeploymentStatus,
|
||||
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
|
||||
let status_o: Option<DeploymentStatus> = sqlx::query_as!(
|
||||
DeploymentStatus,
|
||||
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND (workspace_id = $2 OR \
|
||||
workspace_id = 'starter')",
|
||||
hash.0,
|
||||
w_id,
|
||||
)
|
||||
@@ -669,8 +676,8 @@ async fn delete_script_by_hash(
|
||||
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let script = sqlx::query_as::<_, Script>(
|
||||
"UPDATE script SET content = '', archived = true, deleted = true WHERE hash = $1 AND workspace_id = $2\
|
||||
RETURNING *",
|
||||
"UPDATE script SET content = '', archived = true, deleted = true WHERE hash = $1 AND \
|
||||
workspace_id = $2RETURNING *",
|
||||
)
|
||||
.bind(&hash.0)
|
||||
.bind(&w_id)
|
||||
|
||||
+237
-183
@@ -10,13 +10,13 @@ use std::{sync::Arc, time::Duration};
|
||||
use crate::{
|
||||
audit::{audit_log, ActionKind},
|
||||
db::{UserDB, DB},
|
||||
error::{Error, JsonResult, Result, self},
|
||||
utils::{require_admin, require_super_admin, Pagination}
|
||||
error::{self, Error, JsonResult, Result},
|
||||
utils::{require_admin, require_super_admin, Pagination},
|
||||
};
|
||||
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{Extension, FromRequest, Path, RequestParts, Query},
|
||||
extract::{Extension, FromRequest, Path, Query, RequestParts},
|
||||
http,
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
@@ -25,7 +25,7 @@ use hyper::StatusCode;
|
||||
use rand::rngs::OsRng;
|
||||
use retainer::Cache;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow};
|
||||
use sqlx::FromRow;
|
||||
use time::OffsetDateTime;
|
||||
use tower_cookies::{Cookie, Cookies};
|
||||
use tracing::Span;
|
||||
@@ -48,8 +48,6 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/leave", post(leave_workspace))
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
.route("/email", get(get_email))
|
||||
@@ -65,13 +63,11 @@ pub fn global_service() -> Router {
|
||||
.route("/tokens/create", post(create_token))
|
||||
.route("/tokens/delete/:token_prefix", delete(delete_token))
|
||||
.route("/tokens/list", get(list_tokens))
|
||||
// .route("/list_invite_codes", get(list_invite_codes))
|
||||
// .route("/create_invite_code", post(create_invite_code))
|
||||
// .route("/signup", post(signup))
|
||||
// .route("/lost_password", post(lost_password))
|
||||
// .route("/use_magic_link", get(use_magic_link))
|
||||
|
||||
|
||||
// .route("/list_invite_codes", get(list_invite_codes))
|
||||
// .route("/create_invite_code", post(create_invite_code))
|
||||
// .route("/signup", post(signup))
|
||||
// .route("/lost_password", post(lost_password))
|
||||
// .route("/use_magic_link", get(use_magic_link))
|
||||
}
|
||||
|
||||
pub fn make_unauthed_service() -> Router {
|
||||
@@ -92,15 +88,17 @@ impl AuthCache {
|
||||
}
|
||||
|
||||
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<Authed> {
|
||||
let key = (w_id.as_ref().unwrap_or(&"".to_string()).to_string(), token.to_string());
|
||||
let key = (
|
||||
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
|
||||
token.to_string(),
|
||||
);
|
||||
let s = self.cache.get(&key).await.map(|c| c.to_owned());
|
||||
match s {
|
||||
a @ Some(_) => {
|
||||
a
|
||||
},
|
||||
a @ Some(_) => a,
|
||||
None => {
|
||||
let user_o = sqlx::query_as::<_, (Option<String>, Option<String>, bool)>(
|
||||
"UPDATE token SET last_used_at = $1 WHERE token = $2 AND (expiration > NOW() OR expiration IS NULL) RETURNING owner, email, super_admin",
|
||||
"UPDATE token SET last_used_at = $1 WHERE token = $2 AND (expiration > NOW() \
|
||||
OR expiration IS NULL) RETURNING owner, email, super_admin",
|
||||
)
|
||||
.bind(chrono::Utc::now())
|
||||
.bind(token)
|
||||
@@ -114,40 +112,44 @@ impl AuthCache {
|
||||
match user {
|
||||
(_, Some(email), super_admin) => {
|
||||
if w_id.is_some() {
|
||||
let row_o =
|
||||
sqlx::query_as::<_, (String, bool)>(
|
||||
"SELECT username, is_admin FROM usr where email = $1 AND workspace_id = $2",
|
||||
)
|
||||
.bind(&email)
|
||||
.bind(&w_id.as_ref().unwrap())
|
||||
.fetch_optional(&self.db)
|
||||
.await
|
||||
.unwrap_or(Some(("error".to_string(), false)));
|
||||
let row_o = sqlx::query_as::<_, (String, bool)>(
|
||||
"SELECT username, is_admin FROM usr where email = $1 AND \
|
||||
workspace_id = $2",
|
||||
)
|
||||
.bind(&email)
|
||||
.bind(&w_id.as_ref().unwrap())
|
||||
.fetch_optional(&self.db)
|
||||
.await
|
||||
.unwrap_or(Some(("error".to_string(), false)));
|
||||
|
||||
match row_o {
|
||||
Some((username, is_admin)) => {
|
||||
let groups = get_groups_for_user(&w_id.as_ref().unwrap(),
|
||||
&username, &self.db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
match row_o {
|
||||
Some((username, is_admin)) => {
|
||||
let groups = get_groups_for_user(
|
||||
&w_id.as_ref().unwrap(),
|
||||
&username,
|
||||
&self.db,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(Authed {
|
||||
email: Some(email),
|
||||
username,
|
||||
is_admin: is_admin || super_admin,
|
||||
groups,
|
||||
})
|
||||
},
|
||||
None if super_admin || w_id.unwrap() == "starter" => Some(Authed {
|
||||
email: Some(email.to_string()),
|
||||
username: email,
|
||||
is_admin: super_admin,
|
||||
groups: vec![],
|
||||
}),
|
||||
None => None
|
||||
|
||||
}
|
||||
Some(Authed {
|
||||
email: Some(email),
|
||||
username,
|
||||
is_admin: is_admin || super_admin,
|
||||
groups,
|
||||
})
|
||||
}
|
||||
None if super_admin || w_id.unwrap() == "starter" => {
|
||||
Some(Authed {
|
||||
email: Some(email.to_string()),
|
||||
username: email,
|
||||
is_admin: super_admin,
|
||||
groups: vec![],
|
||||
})
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
} else {
|
||||
Some(Authed {
|
||||
email: Some(email.to_string()),
|
||||
@@ -160,21 +162,23 @@ impl AuthCache {
|
||||
(Some(owner), _, super_admin) if w_id.is_some() => {
|
||||
if let Some((prefix, name)) = owner.split_once('/') {
|
||||
if prefix == "u" {
|
||||
|
||||
let is_admin = super_admin || sqlx::query_scalar!(
|
||||
"SELECT is_admin FROM usr where username = $1 AND workspace_id = $2",
|
||||
name,
|
||||
&w_id.as_ref().unwrap()
|
||||
)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or(false);
|
||||
|
||||
let groups = get_groups_for_user(&w_id.unwrap(), &name, &self.db)
|
||||
let is_admin = super_admin
|
||||
|| sqlx::query_scalar!(
|
||||
"SELECT is_admin FROM usr where username = $1 AND \
|
||||
workspace_id = $2",
|
||||
name,
|
||||
&w_id.as_ref().unwrap()
|
||||
)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
.unwrap_or(false);
|
||||
|
||||
let groups =
|
||||
get_groups_for_user(&w_id.unwrap(), &name, &self.db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(Authed {
|
||||
email: None,
|
||||
@@ -217,7 +221,8 @@ impl AuthCache {
|
||||
|
||||
async fn extract_token<B: Send>(req: &mut RequestParts<B>) -> Option<String> {
|
||||
let auth_header = req
|
||||
.headers().get(http::header::AUTHORIZATION)
|
||||
.headers()
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|s| s.strip_prefix("Bearer "));
|
||||
|
||||
@@ -248,14 +253,14 @@ where
|
||||
Ok(tokened.clone())
|
||||
} else {
|
||||
let token_o = extract_token(req).await;
|
||||
if let Some(token) = token_o {
|
||||
let tokened = Self { token };
|
||||
req.extensions_mut().insert(tokened.clone());
|
||||
Ok(tokened)
|
||||
} else {
|
||||
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
|
||||
}
|
||||
if let Some(token) = token_o {
|
||||
let tokened = Self { token };
|
||||
req.extensions_mut().insert(tokened.clone());
|
||||
Ok(tokened)
|
||||
} else {
|
||||
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,10 +325,9 @@ pub struct User {
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub operator: bool,
|
||||
pub disabled: bool,
|
||||
pub role: Option<String>
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
pub struct GlobalUserInfo {
|
||||
email: String,
|
||||
@@ -334,7 +338,6 @@ pub struct GlobalUserInfo {
|
||||
company: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UserInfo {
|
||||
pub workspace_id: String,
|
||||
@@ -346,7 +349,7 @@ pub struct UserInfo {
|
||||
pub groups: Vec<String>,
|
||||
pub operator: bool,
|
||||
pub disabled: bool,
|
||||
pub role: Option<String>
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
@@ -388,7 +391,7 @@ pub struct NewUser {
|
||||
pub password: String,
|
||||
pub super_admin: bool,
|
||||
pub name: Option<String>,
|
||||
pub company: Option<String>
|
||||
pub company: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -439,7 +442,6 @@ pub struct Login {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Signup {
|
||||
pub email: String,
|
||||
@@ -458,20 +460,21 @@ struct WorkspaceUsername {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
|
||||
async fn exists_username(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(WorkspaceUsername { username }): Json<WorkspaceUsername>
|
||||
Json(WorkspaceUsername { username }): Json<WorkspaceUsername>,
|
||||
) -> JsonResult<bool> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)",
|
||||
&w_id, &username)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
"SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)",
|
||||
&w_id,
|
||||
&username
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
tx.commit().await?;
|
||||
Ok(Json(exists))
|
||||
}
|
||||
@@ -479,7 +482,7 @@ async fn exists_username(
|
||||
async fn list_users(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<User>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let rows = sqlx::query_as!(User, "SELECT * from usr WHERE workspace_id = $1", &w_id)
|
||||
@@ -492,20 +495,25 @@ async fn list_users(
|
||||
async fn list_users_as_super_admin(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Query(pagination): Query<Pagination>
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<GlobalUserInfo>> {
|
||||
let mut tx = db.begin().await?;
|
||||
require_super_admin(&mut tx, authed.email).await?;
|
||||
let (per_page, offset) = crate::utils::paginate(pagination);
|
||||
|
||||
let rows = sqlx::query_as!(GlobalUserInfo, "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2", per_page as i32, offset as i32)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
let rows = sqlx::query_as!(
|
||||
GlobalUserInfo,
|
||||
"SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT \
|
||||
$1 OFFSET $2",
|
||||
per_page as i32,
|
||||
offset as i32
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
|
||||
// async fn list_invite_codes(
|
||||
// authed: Authed,
|
||||
// Extension(db): Extension<DB>,
|
||||
@@ -522,11 +530,10 @@ async fn list_users_as_super_admin(
|
||||
// Ok(Json(rows))
|
||||
// }
|
||||
|
||||
|
||||
async fn list_usernames(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<String>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let rows = sqlx::query_scalar!("SELECT username from usr WHERE workspace_id = $1", &w_id)
|
||||
@@ -541,14 +548,17 @@ async fn list_invites(
|
||||
Extension(db): Extension<DB>,
|
||||
) -> JsonResult<Vec<WorkspaceInvite>> {
|
||||
let mut tx = db.begin().await?;
|
||||
let rows = sqlx::query_as!(WorkspaceInvite, "SELECT * from workspace_invite WHERE email = $1", authed.email)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
let rows = sqlx::query_as!(
|
||||
WorkspaceInvite,
|
||||
"SELECT * from workspace_invite WHERE email = $1",
|
||||
authed.email
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
|
||||
async fn logout(
|
||||
Tokened { token }: Tokened,
|
||||
cookies: Cookies,
|
||||
@@ -577,7 +587,12 @@ async fn logout(
|
||||
async fn whoami(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Authed { username, email, is_admin, groups }: Authed,
|
||||
Authed {
|
||||
username,
|
||||
email,
|
||||
is_admin,
|
||||
groups,
|
||||
}: Authed,
|
||||
) -> JsonResult<UserInfo> {
|
||||
let user = get_user(&w_id, &username, &db).await?;
|
||||
if let Some(user) = user {
|
||||
@@ -603,27 +618,40 @@ async fn global_whoami(
|
||||
Extension(db): Extension<DB>,
|
||||
Authed { email, .. }: Authed,
|
||||
) -> JsonResult<GlobalUserInfo> {
|
||||
let user: GlobalUserInfo = sqlx::query_as!(GlobalUserInfo, "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1", email)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
let user: GlobalUserInfo = sqlx::query_as!(
|
||||
GlobalUserInfo,
|
||||
"SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE \
|
||||
email = $1",
|
||||
email
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
async fn get_email(
|
||||
Authed { email, .. }: Authed,
|
||||
) -> Result<String> {
|
||||
let email = email.ok_or(Error::BadRequest("current session does not correspond to an user with email".to_string()))?;
|
||||
async fn get_email(Authed { email, .. }: Authed) -> Result<String> {
|
||||
let email = email.ok_or(Error::BadRequest(
|
||||
"current session does not correspond to an user with email".to_string(),
|
||||
))?;
|
||||
Ok(email)
|
||||
}
|
||||
|
||||
async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo>> {
|
||||
let user = sqlx::query_as!(User, "SELECT * FROM usr where username = $1 AND workspace_id = $2", username, w_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
let is_super_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", user.as_ref().map(|x| &x.email))
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
let user = sqlx::query_as!(
|
||||
User,
|
||||
"SELECT * FROM usr where username = $1 AND workspace_id = $2",
|
||||
username,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
let is_super_admin = sqlx::query_scalar!(
|
||||
"SELECT super_admin FROM password WHERE email = $1",
|
||||
user.as_ref().map(|x| &x.email)
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
let groups = get_groups_for_user(&w_id, username, db).await?;
|
||||
Ok(user.map(|usr| UserInfo {
|
||||
groups,
|
||||
@@ -635,33 +663,36 @@ async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo
|
||||
created_at: usr.created_at,
|
||||
operator: usr.operator,
|
||||
disabled: usr.disabled,
|
||||
role: usr.role
|
||||
role: usr.role,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result<Vec<String>> {
|
||||
let groups = sqlx::query_scalar!("SELECT group_ FROM usr_to_group where usr = $1 AND workspace_id = $2", username, w_id)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
let groups = sqlx::query_scalar!(
|
||||
"SELECT group_ FROM usr_to_group where usr = $1 AND workspace_id = $2",
|
||||
username,
|
||||
w_id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
async fn whois(Extension(db): Extension<DB>, Path((w_id, username)): Path<(String, String)>) -> JsonResult<UserInfo> {
|
||||
async fn whois(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, username)): Path<(String, String)>,
|
||||
) -> JsonResult<UserInfo> {
|
||||
let user_o = get_user(&w_id, &username, &db).await?;
|
||||
let user = crate::utils::not_found_if_none(user_o, "User", username)?;
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// async fn create_invite_code(
|
||||
// Authed { email, .. }: Authed,
|
||||
// Extension(db): Extension<DB>,
|
||||
// Json(nu): Json<NewInviteCode>,
|
||||
// ) -> Result<(StatusCode, String)> {
|
||||
|
||||
|
||||
// let mut tx = db.begin().await?;
|
||||
// require_super_admin(&mut tx, email).await?;
|
||||
|
||||
@@ -688,7 +719,6 @@ async fn decline_invite(
|
||||
Extension(db): Extension<DB>,
|
||||
Json(nu): Json<DeclineInvite>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let email = email.unwrap_or("".to_string());
|
||||
@@ -701,8 +731,8 @@ async fn decline_invite(
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&email,
|
||||
&mut tx,
|
||||
&email,
|
||||
"users.decline_invite",
|
||||
ActionKind::Delete,
|
||||
&nu.workspace_id,
|
||||
@@ -715,7 +745,10 @@ async fn decline_invite(
|
||||
if is_admin.is_some() {
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
format!("user {} declined invite to workspace {}", &email, nu.workspace_id),
|
||||
format!(
|
||||
"user {} declined invite to workspace {}",
|
||||
&email, nu.workspace_id
|
||||
),
|
||||
))
|
||||
} else {
|
||||
Err(Error::NotFound(format!("invite for {email} not found")))
|
||||
@@ -728,7 +761,7 @@ async fn accept_invite(
|
||||
Json(nu): Json<AcceptInvite>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
if &nu.username == "bot" {
|
||||
return Err(Error::BadRequest("bot is a reserved username".to_string()))
|
||||
return Err(Error::BadRequest("bot is a reserved username".to_string()));
|
||||
}
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
@@ -746,7 +779,7 @@ async fn accept_invite(
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&mut tx,
|
||||
&nu.username,
|
||||
"users.accept_invite",
|
||||
ActionKind::Create,
|
||||
@@ -760,14 +793,23 @@ async fn accept_invite(
|
||||
if is_admin.is_some() {
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
format!("user {} accepted invite to workspace {}", &email, nu.workspace_id),
|
||||
format!(
|
||||
"user {} accepted invite to workspace {}",
|
||||
&email, nu.workspace_id
|
||||
),
|
||||
))
|
||||
} else {
|
||||
Err(Error::NotFound(format!("invite for {email} not found")))
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_user_to_workspace<'c>(w_id: &str, email: &str, username: &str, is_admin: bool, mut tx: sqlx::Transaction<'c, sqlx::Postgres>) -> error::Result<sqlx::Transaction<'c, sqlx::Postgres>> {
|
||||
async fn add_user_to_workspace<'c>(
|
||||
w_id: &str,
|
||||
email: &str,
|
||||
username: &str,
|
||||
is_admin: bool,
|
||||
mut tx: sqlx::Transaction<'c, sqlx::Postgres>,
|
||||
) -> error::Result<sqlx::Transaction<'c, sqlx::Postgres>> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO usr
|
||||
(workspace_id, email, username, is_admin)
|
||||
@@ -789,7 +831,7 @@ async fn add_user_to_workspace<'c>(w_id: &str, email: &str, username: &str, is_a
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&mut tx,
|
||||
username,
|
||||
"users.add_to_workspace",
|
||||
ActionKind::Create,
|
||||
@@ -802,7 +844,9 @@ async fn add_user_to_workspace<'c>(w_id: &str, email: &str, username: &str, is_a
|
||||
}
|
||||
|
||||
async fn update_workspace_user(
|
||||
Authed { username, is_admin, .. }: Authed,
|
||||
Authed {
|
||||
username, is_admin, ..
|
||||
}: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, username_to_update)): Path<(String, String)>,
|
||||
Json(eu): Json<EditWorkspaceUser>,
|
||||
@@ -812,7 +856,7 @@ async fn update_workspace_user(
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
if let Some(a) = eu.is_admin {
|
||||
sqlx::query_scalar!(
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE usr SET is_admin = $1 WHERE username = $2 AND workspace_id = $3",
|
||||
a,
|
||||
&username_to_update,
|
||||
@@ -847,7 +891,7 @@ async fn update_user(
|
||||
require_super_admin(&mut tx, email.clone()).await?;
|
||||
|
||||
if let Some(sa) = eu.is_super_admin {
|
||||
sqlx::query_scalar!(
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE password SET super_admin = $1 WHERE email = $2",
|
||||
sa,
|
||||
&email_to_update
|
||||
@@ -881,7 +925,8 @@ async fn create_user(
|
||||
require_super_admin(&mut tx, email.clone()).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)
|
||||
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, \
|
||||
company)
|
||||
VALUES ($1, $2, $3, 'password', $4, $5, $6)",
|
||||
&nu.email,
|
||||
true,
|
||||
@@ -893,7 +938,6 @@ async fn create_user(
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&email.unwrap(),
|
||||
@@ -908,14 +952,15 @@ async fn create_user(
|
||||
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
|
||||
}
|
||||
|
||||
|
||||
pub fn owner_to_token_owner(user: &str, is_group: bool) -> String {
|
||||
let prefix = if is_group { 'g' } else { 'u' };
|
||||
format!("{}/{}", prefix, user)
|
||||
}
|
||||
|
||||
async fn delete_user(
|
||||
Authed { username, is_admin, .. }: Authed,
|
||||
Authed {
|
||||
username, is_admin, ..
|
||||
}: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, username_to_delete)): Path<(String, String)>,
|
||||
) -> Result<String> {
|
||||
@@ -923,7 +968,6 @@ async fn delete_user(
|
||||
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
|
||||
let email_to_delete_o = sqlx::query_scalar!(
|
||||
"SELECT email FROM usr where username = $1 AND workspace_id = $2",
|
||||
username_to_delete,
|
||||
@@ -956,22 +1000,29 @@ async fn delete_user(
|
||||
async fn set_password(
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(argon2): Extension<Arc<Argon2<'_>>>,
|
||||
Authed { username, email, .. }: Authed,
|
||||
Authed {
|
||||
username, email, ..
|
||||
}: Authed,
|
||||
Json(EditPassword { password }): Json<EditPassword>,
|
||||
) -> Result<String> {
|
||||
let mut tx = db.begin().await?;
|
||||
let email = email.ok_or("no_email").map_err(|e| Error::NotAuthorized(e.to_string()))?;
|
||||
let email = email
|
||||
.ok_or("no_email")
|
||||
.map_err(|e| Error::NotAuthorized(e.to_string()))?;
|
||||
|
||||
let custom_type = sqlx::query_scalar!(
|
||||
"SELECT login_type::TEXT FROM password WHERE email = $1",
|
||||
&email)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or("".to_string());
|
||||
|
||||
"SELECT login_type::TEXT FROM password WHERE email = $1",
|
||||
&email
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or("".to_string());
|
||||
|
||||
if custom_type != "password".to_string() {
|
||||
return Err(Error::BadRequest(format!("login type for {email} is of type {custom_type}. Cannot set password.")))
|
||||
}
|
||||
return Err(Error::BadRequest(format!(
|
||||
"login type for {email} is of type {custom_type}. Cannot set password."
|
||||
)));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE password SET password_hash = $1 WHERE email = $2",
|
||||
@@ -1012,7 +1063,6 @@ pub fn hash_password(argon2: Arc<Argon2>, password: String) -> Result<String> {
|
||||
Ok(password_hash)
|
||||
}
|
||||
|
||||
|
||||
// async fn lost_password(
|
||||
// Extension(db): Extension<DB>,
|
||||
// Extension(es): Extension<Arc<EmailSender>>,
|
||||
@@ -1032,7 +1082,7 @@ pub fn hash_password(argon2: Arc<Argon2>, password: String) -> Result<String> {
|
||||
|
||||
// if !exists {
|
||||
// return Err(Error::NotFound(format!("no user found at email {email}")))
|
||||
// }
|
||||
// }
|
||||
|
||||
// let already = sqlx::query_scalar!(
|
||||
// "SELECT EXISTS(SELECT 1 FROM magic_link WHERE email = $1)",
|
||||
@@ -1082,7 +1132,6 @@ pub fn hash_password(argon2: Arc<Argon2>, password: String) -> Result<String> {
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// async fn signup(
|
||||
// TypedHeader(host): TypedHeader<headers::Host>,
|
||||
// Extension(db): Extension<DB>,
|
||||
@@ -1097,14 +1146,12 @@ pub fn hash_password(argon2: Arc<Argon2>, password: String) -> Result<String> {
|
||||
// ) -> Result<(StatusCode, String)> {
|
||||
// let mut tx = db.begin().await?;
|
||||
|
||||
|
||||
// let email = sqlx::query_scalar!(
|
||||
// "INSERT INTO password (email, password_hash, name, company) VALUES ($1, $2, $3, $4) RETURNING email",
|
||||
// &email, &hash_password(argon2, password)?, name, company)
|
||||
// .fetch_optional(&mut tx)
|
||||
// .await?;
|
||||
|
||||
|
||||
// if let Some(email) = email {
|
||||
// let tx = create_magic_link(&host.hostname(), &email, &es, tx).await?;
|
||||
// tx.commit().await?;
|
||||
@@ -1118,7 +1165,6 @@ pub fn hash_password(argon2: Arc<Argon2>, password: String) -> Result<String> {
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// async fn create_magic_link<'c>(host: &str, email: &str, es: &EmailSender, mut tx: sqlx::Transaction<'c, sqlx::Postgres>) -> error::Result<sqlx::Transaction<'c, sqlx::Postgres>> {
|
||||
// let token = gen_token();
|
||||
|
||||
@@ -1131,7 +1177,7 @@ pub fn hash_password(argon2: Arc<Argon2>, password: String) -> Result<String> {
|
||||
// )
|
||||
// .execute(&mut tx)
|
||||
// .await?;
|
||||
|
||||
|
||||
// let encoded_token = urlencoding::encode(&token);
|
||||
// let encoded_email = urlencoding::encode(email);
|
||||
// es.send_email(Message::builder()
|
||||
@@ -1157,20 +1203,17 @@ async fn login(
|
||||
cookies: Cookies,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(argon2): Extension<Arc<Argon2<'_>>>,
|
||||
Json(Login {
|
||||
email,
|
||||
password,
|
||||
}): Json<Login>,
|
||||
Json(Login { email, password }): Json<Login>,
|
||||
) -> Result<String> {
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
|
||||
let email_w_h: Option<(String, String, bool)> = sqlx::query_as(
|
||||
"SELECT email, password_hash, super_admin FROM password WHERE email = $1 AND login_type = 'password'",
|
||||
)
|
||||
.bind(&email)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
let email_w_h: Option<(String, String, bool)> = sqlx::query_as(
|
||||
"SELECT email, password_hash, super_admin FROM password WHERE email = $1 AND login_type = \
|
||||
'password'",
|
||||
)
|
||||
.bind(&email)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
|
||||
if let Some((email, hash, super_admin)) = email_w_h {
|
||||
let parsed_hash =
|
||||
@@ -1190,7 +1233,12 @@ async fn login(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_session_token<'c>(email: &str, super_admin: bool, tx: &mut sqlx::Transaction<'c, sqlx::Postgres>, cookies: Cookies) -> Result<String> {
|
||||
pub async fn create_session_token<'c>(
|
||||
email: &str,
|
||||
super_admin: bool,
|
||||
tx: &mut sqlx::Transaction<'c, sqlx::Postgres>,
|
||||
cookies: Cookies,
|
||||
) -> Result<String> {
|
||||
let token = gen_token();
|
||||
sqlx::query!(
|
||||
"INSERT INTO token
|
||||
@@ -1238,8 +1286,11 @@ pub async fn create_token_for_owner(
|
||||
.map(char::from)
|
||||
.collect();
|
||||
let mut tx = db.begin().await?;
|
||||
let is_super_admin = username.contains('@') && sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1",
|
||||
owner.split_once('/').map(|x| x.1).unwrap_or(""))
|
||||
let is_super_admin = username.contains('@')
|
||||
&& sqlx::query_scalar!(
|
||||
"SELECT super_admin FROM password WHERE email = $1",
|
||||
owner.split_once('/').map(|x| x.1).unwrap_or("")
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
@@ -1284,16 +1335,19 @@ pub async fn create_token_for_owner(
|
||||
|
||||
async fn create_token(
|
||||
Extension(db): Extension<DB>,
|
||||
Authed { email,.. }: Authed,
|
||||
Authed { email, .. }: Authed,
|
||||
Json(new_token): Json<NewToken>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
let token = gen_token();
|
||||
let mut tx = db.begin().await?;
|
||||
let email = email.ok_or_else(|| error::Error::BadRequest(format!("Only users with email can create tokens")))?;
|
||||
let is_super_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
let email = email.ok_or_else(|| {
|
||||
error::Error::BadRequest(format!("Only users with email can create tokens"))
|
||||
})?;
|
||||
let is_super_admin =
|
||||
sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
sqlx::query!(
|
||||
"INSERT INTO token
|
||||
(token, email, label, expiration, super_admin)
|
||||
@@ -1327,8 +1381,8 @@ async fn list_tokens(
|
||||
) -> JsonResult<Vec<TruncatedToken>> {
|
||||
let rows = sqlx::query_as!(
|
||||
TruncatedToken,
|
||||
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at FROM token \
|
||||
WHERE email = $1",
|
||||
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, \
|
||||
last_used_at FROM token WHERE email = $1",
|
||||
email,
|
||||
)
|
||||
.fetch_all(&db)
|
||||
@@ -1342,7 +1396,9 @@ async fn delete_token(
|
||||
Path(token_prefix): Path<String>,
|
||||
) -> Result<String> {
|
||||
let mut tx = db.begin().await?;
|
||||
let email = email.ok_or_else(|| error::Error::BadRequest(format!("Only users with email can create tokens")))?;
|
||||
let email = email.ok_or_else(|| {
|
||||
error::Error::BadRequest(format!("Only users with email can create tokens"))
|
||||
})?;
|
||||
let tokens_deleted: Vec<String> = sqlx::query_scalar(
|
||||
"DELETE FROM token WHERE email = $1 AND
|
||||
token LIKE concat($3, '%') RETURNING concat(substring(token for 10), '*****')",
|
||||
@@ -1387,13 +1443,13 @@ async fn leave_workspace(
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&mut tx,
|
||||
&username,
|
||||
"users.leave_workspace",
|
||||
ActionKind::Delete,
|
||||
&w_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -1401,7 +1457,6 @@ None,
|
||||
Ok(format!("left workspace {w_id}"))
|
||||
}
|
||||
|
||||
|
||||
pub async fn delete_expired_items_perdiodically(
|
||||
db: &DB,
|
||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||
@@ -1415,12 +1470,11 @@ pub async fn delete_expired_items_perdiodically(
|
||||
.fetch_all(db)
|
||||
.await;
|
||||
|
||||
|
||||
match tokens_deleted_r {
|
||||
Ok(tokens) => tracing::debug!("deleted {} tokens: {:?}", tokens.len(), tokens),
|
||||
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
|
||||
}
|
||||
|
||||
|
||||
let magic_links_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
|
||||
"DELETE FROM magic_link WHERE expiration <= $1
|
||||
RETURNING concat(substring(token for 10), '*****')",
|
||||
|
||||
+20
-12
@@ -93,47 +93,51 @@ pub fn get_reserved_variables(
|
||||
ContextualVariable {
|
||||
name: "WM_WORKSPACE".to_string(),
|
||||
value: w_id.to_string(),
|
||||
description: "Workspace id of the current script".to_string()
|
||||
description: "Workspace id of the current script".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_TOKEN".to_string(),
|
||||
value: token.to_string(),
|
||||
description: "Token ephemeral to the current script with equal permission to the permission of the run (Usable as a bearer token)".to_string()
|
||||
description: "Token ephemeral to the current script with equal permission to the \
|
||||
permission of the run (Usable as a bearer token)"
|
||||
.to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_EMAIL".to_string(),
|
||||
value: email.to_string(),
|
||||
description: "Email of the user that executed the current script".to_string()
|
||||
description: "Email of the user that executed the current script".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_USERNAME".to_string(),
|
||||
value: username.to_string(),
|
||||
description: "Username of the user that executed the current script".to_string()
|
||||
description: "Username of the user that executed the current script".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_JOB_ID".to_string(),
|
||||
value: job_id.to_string(),
|
||||
description: "Job id of the current script".to_string()
|
||||
description: "Job id of the current script".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_JOB_PATH".to_string(),
|
||||
value: path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the script or flow being run if any".to_string()
|
||||
description: "Path of the script or flow being run if any".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_FLOW_PATH".to_string(),
|
||||
value: flow_path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the encapsulating flow if the job is a flow step".to_string()
|
||||
description: "Path of the encapsulating flow if the job is a flow step".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_SCHEDULE_PATH".to_string(),
|
||||
value: schedule_path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the schedule if the job of the step or encapsulating step has been triggered by a schedule".to_string()
|
||||
description: "Path of the schedule if the job of the step or encapsulating step has \
|
||||
been triggered by a schedule"
|
||||
.to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_PERMISSIONED_AS".to_string(),
|
||||
value: permissioned_as.to_string(),
|
||||
description: "Fully Qualified (u/g) owner name of executor of the job".to_string()
|
||||
description: "Fully Qualified (u/g) owner name of executor of the job".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -168,8 +172,11 @@ async fn list_variables(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let rows = sqlx::query_as::<_, ListableVariable>(
|
||||
"SELECT workspace_id, path, CASE WHEN is_secret IS TRUE THEN null ELSE value::text END as value, is_secret, description, extra_perms, account, is_oauth, false as is_expired from variable
|
||||
WHERE (workspace_id = $1 OR (is_secret IS NOT TRUE AND workspace_id = 'starter')) ORDER BY path",
|
||||
"SELECT workspace_id, path, CASE WHEN is_secret IS TRUE THEN null ELSE value::text END as \
|
||||
value, is_secret, description, extra_perms, account, is_oauth, false as is_expired from \
|
||||
variable
|
||||
WHERE (workspace_id = $1 OR (is_secret IS NOT TRUE AND workspace_id = 'starter')) ORDER \
|
||||
BY path",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&mut tx)
|
||||
@@ -198,7 +205,8 @@ async fn get_variable(
|
||||
let variable_o = sqlx::query_as::<_, ListableVariable>(
|
||||
"SELECT variable.*, (now() > account.expires_at) as is_expired from variable
|
||||
LEFT JOIN account ON variable.account = account.id
|
||||
WHERE variable.path = $1 AND (variable.workspace_id = $2 OR (is_secret IS NOT TRUE AND variable.workspace_id = 'starter'))
|
||||
WHERE variable.path = $1 AND (variable.workspace_id = $2 OR (is_secret IS NOT TRUE AND \
|
||||
variable.workspace_id = 'starter'))
|
||||
LIMIT 1",
|
||||
)
|
||||
.bind(&path)
|
||||
|
||||
+27
-6
@@ -404,7 +404,10 @@ async fn handle_nondep_job(
|
||||
};
|
||||
(code, reqs, job.language.to_owned())
|
||||
} else {
|
||||
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>)>("SELECT content, lock, language FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')")
|
||||
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>)>(
|
||||
"SELECT content, lock, language FROM script WHERE hash = $1 AND (workspace_id = $2 OR \
|
||||
workspace_id = 'starter')",
|
||||
)
|
||||
.bind(&job.script_hash.unwrap_or(ScriptHash(0)).0)
|
||||
.bind(&job.workspace_id)
|
||||
.fetch_optional(db)
|
||||
@@ -474,11 +477,29 @@ async fn handle_nondep_job(
|
||||
let _ = write_file(job_dir, "inner.py", &inner_content).await?;
|
||||
|
||||
let sig = crate::parser::parse_python_signature(&inner_content)?;
|
||||
let transforms = sig.args.into_iter().map(|x| match x.typ {
|
||||
Typ::Bytes => format!("if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n kwargs[\"{}\"] = base64.b64decode(kwargs[\"{}\"])\n", x.name, x.name, x.name, x.name),
|
||||
Typ::Datetime => format!("if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n kwargs[\"{}\"] = datetime.strptime(kwargs[\"{}\"], '%Y-%m-%dT%H:%M')\n", x.name, x.name, x.name, x.name),
|
||||
_ => "".to_string()
|
||||
}).collect::<Vec<String>>().join("");
|
||||
let transforms = sig
|
||||
.args
|
||||
.into_iter()
|
||||
.map(|x| match x.typ {
|
||||
Typ::Bytes => {
|
||||
format!(
|
||||
"if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n \
|
||||
kwargs[\"{}\"] = base64.b64decode(kwargs[\"{}\"])\n",
|
||||
x.name, x.name, x.name, x.name
|
||||
)
|
||||
}
|
||||
Typ::Datetime => {
|
||||
format!(
|
||||
"if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n \
|
||||
kwargs[\"{}\"] = datetime.strptime(kwargs[\"{}\"], \
|
||||
'%Y-%m-%dT%H:%M')\n",
|
||||
x.name, x.name, x.name, x.name
|
||||
)
|
||||
}
|
||||
_ => "".to_string(),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
let tx = db.begin().await?;
|
||||
|
||||
|
||||
+19
-19
@@ -1,21 +1,19 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::flows::{FlowModuleValue, FlowValue, InputTransform};
|
||||
use crate::jobs::{
|
||||
add_completed_job, add_completed_job_error, get_queued_job, postprocess_queued_job, push,
|
||||
script_path_to_payload, JobPayload,
|
||||
};
|
||||
use crate::js_eval::{eval_timeout, EvalCreds};
|
||||
use crate::users::create_token_for_owner;
|
||||
use crate::{
|
||||
db::DB,
|
||||
error::{self, Error},
|
||||
jobs::QueuedJob,
|
||||
flows::{FlowModuleValue, FlowValue, InputTransform},
|
||||
jobs::{
|
||||
add_completed_job, add_completed_job_error, get_queued_job, postprocess_queued_job, push,
|
||||
script_path_to_payload, JobPayload, QueuedJob,
|
||||
},
|
||||
js_eval::{eval_timeout, EvalCreds},
|
||||
users::create_token_for_owner,
|
||||
};
|
||||
use async_recursion::async_recursion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serde_json::{Map, Value};
|
||||
use serde_json::{json, Map, Value};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -134,20 +132,22 @@ pub async fn update_flow_status_after_job_completion(
|
||||
);
|
||||
|
||||
let prev_step = old_status.step;
|
||||
let (stop_early_expr, skip_if_stop_early) = sqlx::query_as::<_, (Option<String>, Option<bool>)>(&format!(
|
||||
"UPDATE queue
|
||||
let (stop_early_expr, skip_if_stop_early) =
|
||||
sqlx::query_as::<_, (Option<String>, Option<bool>)>(&format!(
|
||||
"UPDATE queue
|
||||
SET
|
||||
flow_status = jsonb_set(jsonb_set(flow_status, '{{modules, {prev_step}}}', $1), '{{\"step\"}}', $2)
|
||||
flow_status = jsonb_set(jsonb_set(flow_status, '{{modules, {prev_step}}}', $1), \
|
||||
'{{\"step\"}}', $2)
|
||||
WHERE id = $3
|
||||
RETURNING
|
||||
(raw_flow->'modules'->{prev_step}->>'stop_after_if_expr'),
|
||||
(raw_flow->'modules'->{prev_step}->>'skip_if_stopped')::bool",
|
||||
))
|
||||
.bind(serde_json::json!(new_status))
|
||||
.bind(serde_json::json!(step_counter))
|
||||
.bind(flow)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
))
|
||||
.bind(serde_json::json!(new_status))
|
||||
.bind(serde_json::json!(step_counter))
|
||||
.bind(flow)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
|
||||
tracing::debug!("UPDATE: {:?}", new_status);
|
||||
|
||||
|
||||
+189
-135
@@ -6,15 +6,27 @@
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
audit::{audit_log, ActionKind},
|
||||
db::{UserDB, DB},
|
||||
error::{Error, JsonResult, Result},
|
||||
users::{Authed, WorkspaceInvite}, utils::{require_admin, require_super_admin, Pagination}, audit::{audit_log, ActionKind}, scripts::{Script, Schema}, resources::{Resource, ResourceType}, flows::Flow, variables::ListableVariable,
|
||||
flows::Flow,
|
||||
resources::{Resource, ResourceType},
|
||||
scripts::{Schema, Script},
|
||||
users::{Authed, WorkspaceInvite},
|
||||
utils::{require_admin, require_super_admin, Pagination},
|
||||
variables::ListableVariable,
|
||||
};
|
||||
use axum::{
|
||||
body::StreamBody,
|
||||
extract::{Extension, Path, Query},
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use axum::{extract::{Extension, Path, Query}, routing::{get, post, delete}, Json, Router, response::{IntoResponse}, body::StreamBody};
|
||||
|
||||
use hyper::{StatusCode, header};
|
||||
use hyper::{header, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow};
|
||||
use sqlx::FromRow;
|
||||
use tempfile::TempDir;
|
||||
use tokio::fs::File;
|
||||
use tokio_util::io::ReaderStream;
|
||||
@@ -29,19 +41,16 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/get_settings", get(get_settings))
|
||||
.route("/edit_slack_command", post(edit_slack_command))
|
||||
.route("/tarball", get(tarball_workspace))
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
.route("/list_as_superadmin", get(list_workspaces_as_super_admin))
|
||||
.route("/list", get(list_workspaces))
|
||||
.route("/users", get(user_workspaces))
|
||||
.route("/create", post(create_workspace))
|
||||
.route("/exists", post(exists_workspace))
|
||||
.route("/exists_username", post(exists_username))
|
||||
.route("/list_as_superadmin", get(list_workspaces_as_super_admin))
|
||||
.route("/list", get(list_workspaces))
|
||||
.route("/users", get(user_workspaces))
|
||||
.route("/create", post(create_workspace))
|
||||
.route("/exists", post(exists_workspace))
|
||||
.route("/exists_username", post(exists_username))
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
@@ -51,7 +60,7 @@ struct Workspace {
|
||||
owner: String,
|
||||
domain: Option<String>,
|
||||
deleted: bool,
|
||||
premium: bool
|
||||
premium: bool,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
@@ -59,20 +68,18 @@ pub struct WorkspaceSettings {
|
||||
pub workspace_id: String,
|
||||
pub slack_team_id: Option<String>,
|
||||
pub slack_name: Option<String>,
|
||||
pub slack_command_script: Option<String>
|
||||
pub slack_command_script: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Debug)]
|
||||
#[sqlx(type_name = "WORKSPACE_KEY_KIND", rename_all = "lowercase")]
|
||||
pub enum WorkspaceKeyKind {
|
||||
Cloud
|
||||
Cloud,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditCommandScript {
|
||||
slack_command_script: Option<String>
|
||||
slack_command_script: Option<String>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct CreateWorkspace {
|
||||
@@ -82,15 +89,13 @@ struct CreateWorkspace {
|
||||
domain: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditWorkspace {
|
||||
name: String,
|
||||
owner: String,
|
||||
domain: Option<String>
|
||||
domain: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct WorkspaceList {
|
||||
pub email: String,
|
||||
@@ -104,7 +109,6 @@ struct UserWorkspace {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WorkspaceId {
|
||||
pub id: String,
|
||||
@@ -116,14 +120,12 @@ struct ValidateUsername {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NewWorkspaceInvite {
|
||||
pub email: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
|
||||
async fn list_pending_invites(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -131,26 +133,30 @@ async fn list_pending_invites(
|
||||
) -> JsonResult<Vec<WorkspaceInvite>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let rows = sqlx::query_as!(WorkspaceInvite, "SELECT * from workspace_invite WHERE workspace_id = $1", w_id)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
let rows = sqlx::query_as!(
|
||||
WorkspaceInvite,
|
||||
"SELECT * from workspace_invite WHERE workspace_id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
|
||||
async fn exists_workspace(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Json(WorkspaceId { id }): Json<WorkspaceId>
|
||||
Json(WorkspaceId { id }): Json<WorkspaceId>,
|
||||
) -> JsonResult<bool> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM workspace WHERE workspace.id = $1)",
|
||||
id)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
"SELECT EXISTS(SELECT 1 FROM workspace WHERE workspace.id = $1)",
|
||||
id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
tx.commit().await?;
|
||||
Ok(Json(exists))
|
||||
}
|
||||
@@ -161,16 +167,17 @@ async fn list_workspaces(
|
||||
) -> JsonResult<Vec<Workspace>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let workspaces = sqlx::query_as!(
|
||||
Workspace,
|
||||
"SELECT workspace.* FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = false",
|
||||
authed.email.as_ref())
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
Workspace,
|
||||
"SELECT workspace.* FROM workspace, usr WHERE usr.workspace_id = workspace.id AND \
|
||||
usr.email = $1 AND deleted = false",
|
||||
authed.email.as_ref()
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(workspaces))
|
||||
}
|
||||
|
||||
|
||||
async fn get_settings(
|
||||
authed: Authed,
|
||||
Path(w_id): Path<String>,
|
||||
@@ -178,11 +185,12 @@ async fn get_settings(
|
||||
) -> 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?;
|
||||
WorkspaceSettings,
|
||||
"SELECT * FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(settings))
|
||||
}
|
||||
@@ -191,8 +199,10 @@ async fn edit_slack_command(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Authed { is_admin, username, .. }: Authed,
|
||||
Json(es): Json<EditCommandScript>
|
||||
Authed {
|
||||
is_admin, username, ..
|
||||
}: Authed,
|
||||
Json(es): Json<EditCommandScript>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
let mut tx = db.begin().await?;
|
||||
@@ -205,15 +215,21 @@ async fn edit_slack_command(
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"workspaces.edit_command_script",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email.unwrap()),
|
||||
Some([
|
||||
("script", es.slack_command_script.unwrap_or("NO_SCRIPT".to_string()).as_str())
|
||||
].into()),
|
||||
Some(&authed.email.unwrap()),
|
||||
Some(
|
||||
[(
|
||||
"script",
|
||||
es.slack_command_script
|
||||
.unwrap_or("NO_SCRIPT".to_string())
|
||||
.as_str(),
|
||||
)]
|
||||
.into(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -221,7 +237,6 @@ Some([
|
||||
Ok(format!("Edit command script {}", &w_id))
|
||||
}
|
||||
|
||||
|
||||
async fn list_workspaces_as_super_admin(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -233,10 +248,13 @@ async fn list_workspaces_as_super_admin(
|
||||
let (per_page, offset) = crate::utils::paginate(pagination);
|
||||
|
||||
let workspaces = sqlx::query_as!(
|
||||
Workspace,
|
||||
"SELECT * FROM workspace LIMIT $1 OFFSET $2", per_page as i32, offset as i32)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
Workspace,
|
||||
"SELECT * FROM workspace LIMIT $1 OFFSET $2",
|
||||
per_page as i32,
|
||||
offset as i32
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(workspaces))
|
||||
}
|
||||
@@ -250,12 +268,14 @@ async fn user_workspaces(
|
||||
.map_err(|x| Error::NotAuthorized(x.to_string()))?;
|
||||
let mut tx = db.begin().await?;
|
||||
let workspaces = sqlx::query_as!(
|
||||
UserWorkspace,
|
||||
"SELECT workspace.id, workspace.name, usr.username
|
||||
FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = false",
|
||||
email)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
UserWorkspace,
|
||||
"SELECT workspace.id, workspace.name, usr.username
|
||||
FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = \
|
||||
false",
|
||||
email
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(WorkspaceList { email, workspaces }))
|
||||
}
|
||||
@@ -263,10 +283,10 @@ async fn user_workspaces(
|
||||
async fn create_workspace(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Json(nw): Json<CreateWorkspace>
|
||||
Json(nw): Json<CreateWorkspace>,
|
||||
) -> Result<String> {
|
||||
if &nw.username == "bot" {
|
||||
return Err(Error::BadRequest("bot is a reserved username".to_string()))
|
||||
return Err(Error::BadRequest("bot is a reserved username".to_string()));
|
||||
}
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!(
|
||||
@@ -293,7 +313,8 @@ async fn create_workspace(
|
||||
"INSERT INTO workspace_key
|
||||
(workspace_id, kind, key)
|
||||
VALUES ($1, 'cloud', $2)",
|
||||
nw.id, &key
|
||||
nw.id,
|
||||
&key
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -341,7 +362,7 @@ async fn create_workspace(
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"workspaces.create",
|
||||
ActionKind::Create,
|
||||
@@ -351,7 +372,6 @@ async fn create_workspace(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
|
||||
Ok(format!("Created workspace {}", &nw.id))
|
||||
}
|
||||
@@ -360,8 +380,10 @@ async fn edit_workspace(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Authed { is_admin, username, .. }: Authed,
|
||||
Json(ew): Json<EditWorkspace>
|
||||
Authed {
|
||||
is_admin, username, ..
|
||||
}: Authed,
|
||||
Json(ew): Json<EditWorkspace>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
let mut tx = db.begin().await?;
|
||||
@@ -376,15 +398,19 @@ async fn edit_workspace(
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"workspaces.update",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email.unwrap()),
|
||||
Some([
|
||||
("domain", ew.domain.unwrap_or("NO_DOMAIN".to_string()).as_str())
|
||||
].into()),
|
||||
Some(&authed.email.unwrap()),
|
||||
Some(
|
||||
[(
|
||||
"domain",
|
||||
ew.domain.unwrap_or("NO_DOMAIN".to_string()).as_str(),
|
||||
)]
|
||||
.into(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -395,25 +421,27 @@ Some([
|
||||
async fn delete_workspace(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Authed { is_admin, username, email, .. }: Authed,
|
||||
Authed {
|
||||
is_admin,
|
||||
username,
|
||||
email,
|
||||
..
|
||||
}: Authed,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!(
|
||||
"UPDATE workspace SET deleted = true WHERE id = $1",
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
sqlx::query!("UPDATE workspace SET deleted = true WHERE id = $1", &w_id)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&mut tx,
|
||||
&username,
|
||||
"workspaces.delete",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&email.unwrap_or("noemail".to_string())),
|
||||
None,
|
||||
Some(&email.unwrap_or("noemail".to_string())),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -421,14 +449,14 @@ None,
|
||||
Ok(format!("Deleted workspace {}", &w_id))
|
||||
}
|
||||
|
||||
|
||||
async fn invite_user(
|
||||
Authed { username, is_admin, .. }: Authed,
|
||||
Authed {
|
||||
username, is_admin, ..
|
||||
}: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(nu): Json<NewWorkspaceInvite>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
@@ -452,14 +480,14 @@ async fn invite_user(
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
async fn delete_invite(
|
||||
Authed { username, is_admin, .. }: Authed,
|
||||
Authed {
|
||||
username, is_admin, ..
|
||||
}: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(nu): Json<NewWorkspaceInvite>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
@@ -486,7 +514,6 @@ async fn exists_username(
|
||||
Extension(db): Extension<DB>,
|
||||
Json(vu): Json<ValidateUsername>,
|
||||
) -> Result<String> {
|
||||
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 AND workspace_id = $2)",
|
||||
vu.username,
|
||||
@@ -497,7 +524,7 @@ async fn exists_username(
|
||||
.unwrap_or(true);
|
||||
|
||||
if exists {
|
||||
return Err(Error::BadRequest("username already taken".to_string()))
|
||||
return Err(Error::BadRequest("username already taken".to_string()));
|
||||
}
|
||||
|
||||
Ok("valid username".to_string())
|
||||
@@ -509,55 +536,67 @@ struct ScriptMetadata {
|
||||
description: String,
|
||||
schema: Option<Schema>,
|
||||
is_template: bool,
|
||||
lock: Vec<String>
|
||||
lock: Vec<String>,
|
||||
}
|
||||
|
||||
async fn tarball_workspace(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> Result<([(headers::HeaderName, String); 2], impl IntoResponse)> {
|
||||
) -> Result<([(headers::HeaderName, String); 2], impl IntoResponse)> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let tmp_dir = TempDir::new_in(".")?;
|
||||
|
||||
|
||||
let name = format!("windmill-{w_id}.tar");
|
||||
let file_path = tmp_dir.path().join(&name);
|
||||
let file = File::create(&file_path).await?;
|
||||
let mut a = tokio_tar::Builder::new(file);
|
||||
|
||||
|
||||
{
|
||||
let scripts = sqlx::query_as::<_, Script>(
|
||||
"SELECT * FROM script as o WHERE workspace_id = $1 AND archived = false
|
||||
AND created_at = (select max(created_at) from script where path = o.path AND workspace_id = $1)"
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
for script in scripts {
|
||||
write_to_archive(script.content, format!("scripts/{}.py", script.path), &mut a).await?;
|
||||
|
||||
let lock = script.lock.unwrap_or_else(|| "".to_string())
|
||||
.lines()
|
||||
.map(|x| x.to_string())
|
||||
.collect();
|
||||
let metadata = ScriptMetadata {
|
||||
summary: script.summary, description: script.description, schema: script.schema, is_template: script.is_template, lock };
|
||||
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
|
||||
write_to_archive(metadata_str, format!("scripts/{}.json", script.path), &mut a).await?;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
"SELECT * FROM script as o WHERE workspace_id = $1 AND archived = false
|
||||
AND created_at = (select max(created_at) from script where path = o.path AND \
|
||||
workspace_id = $1)",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
for script in scripts {
|
||||
write_to_archive(
|
||||
script.content,
|
||||
format!("scripts/{}.py", script.path),
|
||||
&mut a,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let lock = script
|
||||
.lock
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.lines()
|
||||
.map(|x| x.to_string())
|
||||
.collect();
|
||||
let metadata = ScriptMetadata {
|
||||
summary: script.summary,
|
||||
description: script.description,
|
||||
schema: script.schema,
|
||||
is_template: script.is_template,
|
||||
lock,
|
||||
};
|
||||
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
|
||||
write_to_archive(
|
||||
metadata_str,
|
||||
format!("scripts/{}.json", script.path),
|
||||
&mut a,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
let resources = sqlx::query_as!(Resource,
|
||||
let resources = sqlx::query_as!(
|
||||
Resource,
|
||||
"SELECT * FROM resource WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
@@ -566,27 +605,38 @@ async fn tarball_workspace(
|
||||
|
||||
for resource in resources {
|
||||
let resource_str = serde_json::to_string_pretty(&resource).unwrap();
|
||||
write_to_archive(resource_str, format!("resources/{}.json", resource.path), &mut a).await?;
|
||||
write_to_archive(
|
||||
resource_str,
|
||||
format!("resources/{}.json", resource.path),
|
||||
&mut a,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let resource_types = sqlx::query_as!(ResourceType,
|
||||
"SELECT * FROM resource_type WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
let resource_types = sqlx::query_as!(
|
||||
ResourceType,
|
||||
"SELECT * FROM resource_type WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
for resource_type in resource_types {
|
||||
let resource_str = serde_json::to_string_pretty(&resource_type).unwrap();
|
||||
write_to_archive(resource_str, format!("resource_types/{}.json", resource_type.name), &mut a).await?;
|
||||
write_to_archive(
|
||||
resource_str,
|
||||
format!("resource_types/{}.json", resource_type.name),
|
||||
&mut a,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let flows = sqlx::query_as::<_, Flow>(
|
||||
"SELECT * FROM flow WHERE workspace_id = $1 AND archived = false"
|
||||
"SELECT * FROM flow WHERE workspace_id = $1 AND archived = false",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&db)
|
||||
@@ -599,8 +649,8 @@ async fn tarball_workspace(
|
||||
}
|
||||
|
||||
{
|
||||
let variables = sqlx::query_as::<_, ListableVariable>(
|
||||
"SELECT * FROM variable WHERE workspace_id = $1 AND is_secret = false"
|
||||
let variables = sqlx::query_as::<_, ListableVariable>(
|
||||
"SELECT * FROM variable WHERE workspace_id = $1 AND is_secret = false",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&db)
|
||||
@@ -629,7 +679,11 @@ async fn tarball_workspace(
|
||||
Ok((headers, body))
|
||||
}
|
||||
|
||||
async fn write_to_archive(content: String, path: String, a: &mut tokio_tar::Builder<File>) -> Result<()> {
|
||||
async fn write_to_archive(
|
||||
content: String,
|
||||
path: String,
|
||||
a: &mut tokio_tar::Builder<File>,
|
||||
) -> Result<()> {
|
||||
let bytes = content.as_bytes();
|
||||
let mut header = tokio_tar::Header::new_gnu();
|
||||
header.set_size(bytes.len() as u64);
|
||||
|
||||
Reference in New Issue
Block a user