feat: auto-invite from same domain
This commit is contained in:
@@ -1 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE workspace_settings DROP COLUMN auto_invite_domain;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
3
backend/migrations/20221212130731_add_autoinvite.up.sql
Normal file
3
backend/migrations/20221212130731_add_autoinvite.up.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE workspace_settings ADD COLUMN auto_invite_domain VARCHAR(50);
|
||||
ALTER TABLE workspace DROP COLUMN domain;
|
||||
@@ -330,19 +330,14 @@
|
||||
"ordinal": 2,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"ordinal": 3,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "deleted",
|
||||
"ordinal": 4,
|
||||
"ordinal": 3,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "premium",
|
||||
"ordinal": 5,
|
||||
"ordinal": 4,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
@@ -350,7 +345,6 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
],
|
||||
@@ -405,6 +399,11 @@
|
||||
"name": "slack_email",
|
||||
"ordinal": 4,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "auto_invite_domain",
|
||||
"ordinal": 5,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
@@ -412,7 +411,8 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
@@ -855,6 +855,19 @@
|
||||
},
|
||||
"query": "DELETE FROM workspace_invite WHERE\n workspace_id = $1 AND email = $2 AND is_admin = $3"
|
||||
},
|
||||
"2c6c2358fc24d92db179306a0f18a83d71bf15d8dc76502c451634dff35715a6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ($1, $2, false)\n ON CONFLICT DO NOTHING"
|
||||
},
|
||||
"2e11e3ef361c41e6e055dd4805cb9d5e45eaa486e35fc5f01dae311189d6800f": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1120,6 +1133,20 @@
|
||||
},
|
||||
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2)\n WHERE id = $3\n "
|
||||
},
|
||||
"4815068e6d833e918613d324b5d694c479d039576cabb88d5be8894f171c3b4e": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace\n (id, name, owner)\n VALUES ($1, $2, $3)"
|
||||
},
|
||||
"486f181a9ced2bdc7c8d93da22c9d3e229ef106174bff2c472dbe82622f382b6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -1181,21 +1208,6 @@
|
||||
},
|
||||
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['step'], $1)\n WHERE id = $2\n "
|
||||
},
|
||||
"4e8ff055a80cc2e6b7fd6653c0c8e9a0e4b223a4bdeab0cf43dc79ce10f635b9": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace\n (id, name, owner, domain)\n VALUES ($1, $2, $3, $4)"
|
||||
},
|
||||
"4eb6b80c410e00e8a72eced6c25cdd3ed941e7a46a0619173f272eec7f28a3c1": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1450,6 +1462,11 @@
|
||||
"name": "slack_email",
|
||||
"ordinal": 4,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "auto_invite_domain",
|
||||
"ordinal": 5,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
@@ -1457,7 +1474,8 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
@@ -2093,6 +2111,19 @@
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ('demo', $1, false)\n ON CONFLICT DO NOTHING"
|
||||
},
|
||||
"7b0a6bf71751668eea56923f70e198c36386a82b8a8abbf4f55caeb0618478fe": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace_settings SET auto_invite_domain = $1 WHERE workspace_id = $2"
|
||||
},
|
||||
"7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2166,21 +2197,6 @@
|
||||
},
|
||||
"query": "INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4)"
|
||||
},
|
||||
"8114333781add802159cd2c26d57cea40e27143606c71bf4d87dc14bb7040cd6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace SET name = $1, owner = $2, domain = $3 WHERE id = $4"
|
||||
},
|
||||
"826c6a36020f402169cab2ebe097e9f38f416f6f080a2fe8a9b83e97c4e15150": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2625,6 +2641,20 @@
|
||||
},
|
||||
"query": "UPDATE password SET password_hash = $1 WHERE email = $2"
|
||||
},
|
||||
"903db87d61052a7ed39164be72c365e63acd7f87e103a3bdac870f764ddd7a9f": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING"
|
||||
},
|
||||
"90719f6230467b08e5f2cc89271bcf5e4a6cec39e9d9b42ef3b90f09b3135b83": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3704,6 +3734,26 @@
|
||||
},
|
||||
"query": "\n INSERT INTO resume_job\n (id, resume_id, job, flow, value, approver)\n VALUES ($1, $2, $3, $4, $5, $6)\n "
|
||||
},
|
||||
"c5429cd91368fcdc4da65ef6d84b32b2726f5ea938bc53bf6ed63465bd0eb0f2": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT workspace_id FROM workspace_settings WHERE auto_invite_domain = $1"
|
||||
},
|
||||
"c59dd666b9a316c027e8c319b80ccbab3a220d93b64357981bd4a03324dad1d0": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3833,6 +3883,32 @@
|
||||
},
|
||||
"query": "UPDATE queue SET running = false WHERE last_ping < now() - ($1 || ' seconds')::interval AND running = true AND job_kind != $2 AND same_worker = false RETURNING id, workspace_id, last_ping"
|
||||
},
|
||||
"ccb05bf4c0a0f297f75498a684673ae7e4f6ed47f7a8f5d7154e0e0350865d41": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace_settings SET auto_invite_domain = NULL WHERE workspace_id = $1"
|
||||
},
|
||||
"d0308abac80575038203b60bb66d3b39b586939da0421a595e47c7a759616431": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace SET name = $1, owner = $2 WHERE id = $3"
|
||||
},
|
||||
"d07faf1c9501d1bc5f6fd9588bb3da7b7f4897fcfd26dcb3b8ce406b1f653077": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -4567,19 +4643,14 @@
|
||||
"ordinal": 2,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"ordinal": 3,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "deleted",
|
||||
"ordinal": 4,
|
||||
"ordinal": 3,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "premium",
|
||||
"ordinal": 5,
|
||||
"ordinal": 4,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
@@ -4587,7 +4658,6 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
],
|
||||
@@ -4753,5 +4823,18 @@
|
||||
}
|
||||
},
|
||||
"query": "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3"
|
||||
},
|
||||
"fceeeaa8d3c97273c8e77b3badd9e7cf82deead59c0d0619e0c303ea665f7d72": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n SELECT $1::text, email, false FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING"
|
||||
}
|
||||
}
|
||||
1
backend/windmill-api/banned_domains.txt
Normal file
1
backend/windmill-api/banned_domains.txt
Normal file
@@ -0,0 +1 @@
|
||||
gmail.com
|
||||
@@ -351,6 +351,20 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/Workspace"
|
||||
|
||||
/workspaces/allowed_domain_auto_invite:
|
||||
get:
|
||||
summary: is domain allowed for auto invi
|
||||
operationId: isDomainAllowed
|
||||
tags:
|
||||
- workspace
|
||||
responses:
|
||||
"200":
|
||||
description: domain allowed or not
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
/workspaces/users:
|
||||
get:
|
||||
summary: list all workspaces visible to me with user info
|
||||
@@ -765,6 +779,8 @@ paths:
|
||||
type: string
|
||||
slack_command_script:
|
||||
type: string
|
||||
auto_invite_domain:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_slack_command:
|
||||
post:
|
||||
@@ -793,6 +809,35 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_auto_invite:
|
||||
post:
|
||||
summary: edit auto invite
|
||||
operationId: editAutoInvite
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: WorkspaceInvite
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
set:
|
||||
type: boolean
|
||||
required:
|
||||
- set
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/users/list:
|
||||
get:
|
||||
summary: list users
|
||||
@@ -4783,13 +4828,10 @@ components:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
domain:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- username
|
||||
- domain
|
||||
|
||||
Workspace:
|
||||
type: object
|
||||
|
||||
@@ -33,6 +33,7 @@ use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::utils::{not_found_if_none, now_from_db};
|
||||
|
||||
use crate::users::Authed;
|
||||
use crate::workspaces::invite_user_to_all_auto_invite_worspaces;
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
variables::{build_crypt, encrypt},
|
||||
@@ -923,6 +924,9 @@ async fn login_callback(
|
||||
.bind(user.company)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
invite_user_to_all_auto_invite_worspaces(&db, &email).await?;
|
||||
tx = db.begin().await?;
|
||||
crate::users::create_session_token(
|
||||
&email,
|
||||
false,
|
||||
|
||||
@@ -45,6 +45,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/delete_invite", post(delete_invite))
|
||||
.route("/get_settings", get(get_settings))
|
||||
.route("/edit_slack_command", post(edit_slack_command))
|
||||
.route("/edit_auto_invite", post(edit_auto_invite))
|
||||
.route("/tarball", get(tarball_workspace))
|
||||
}
|
||||
|
||||
@@ -56,6 +57,7 @@ pub fn global_service() -> Router {
|
||||
.route("/create", post(create_workspace))
|
||||
.route("/exists", post(exists_workspace))
|
||||
.route("/exists_username", post(exists_username))
|
||||
.route("/allowed_domain_auto_invite", get(is_allowed_auto_domain))
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
@@ -63,7 +65,6 @@ struct Workspace {
|
||||
id: String,
|
||||
name: String,
|
||||
owner: String,
|
||||
domain: Option<String>,
|
||||
deleted: bool,
|
||||
premium: bool,
|
||||
}
|
||||
@@ -75,6 +76,7 @@ pub struct WorkspaceSettings {
|
||||
pub slack_name: Option<String>,
|
||||
pub slack_command_script: Option<String>,
|
||||
pub slack_email: String,
|
||||
pub auto_invite_domain: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
@@ -96,19 +98,23 @@ pub enum WorkspaceKeyKind {
|
||||
struct EditCommandScript {
|
||||
slack_command_script: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditAutoInvite {
|
||||
set: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateWorkspace {
|
||||
id: String,
|
||||
name: String,
|
||||
username: String,
|
||||
domain: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditWorkspace {
|
||||
name: String,
|
||||
owner: String,
|
||||
domain: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -252,6 +258,76 @@ async fn edit_slack_command(
|
||||
Ok(format!("Edit command script {}", &w_id))
|
||||
}
|
||||
|
||||
const BANNED_DOMAINS: &str = include_str!("../banned_domains.txt");
|
||||
|
||||
async fn is_allowed_auto_domain(Authed { email, .. }: Authed) -> JsonResult<bool> {
|
||||
let domain = email.split('@').last().unwrap();
|
||||
return Ok(Json(!BANNED_DOMAINS.contains(domain)));
|
||||
}
|
||||
|
||||
async fn edit_auto_invite(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Authed { is_admin, email, username, .. }: Authed,
|
||||
Json(ea): Json<EditAutoInvite>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
let domain = email.split('@').last().unwrap();
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
if ea.set {
|
||||
if BANNED_DOMAINS.contains(domain) {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Domain {} is not allowed",
|
||||
domain
|
||||
)));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET auto_invite_domain = $1 WHERE workspace_id = $2",
|
||||
domain,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_invite
|
||||
(workspace_id, email, is_admin)
|
||||
SELECT $1::text, email, false FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS (
|
||||
SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email
|
||||
)
|
||||
ON CONFLICT DO NOTHING",
|
||||
&w_id,
|
||||
&domain
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET auto_invite_domain = NULL WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"workspaces.edit_auto_invite_domain",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([("set", &ea.set.to_string()[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Edit command script {}", &w_id))
|
||||
}
|
||||
|
||||
async fn list_workspaces_as_super_admin(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -303,12 +379,11 @@ async fn create_workspace(
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace
|
||||
(id, name, owner, domain)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
(id, name, owner)
|
||||
VALUES ($1, $2, $3)",
|
||||
nw.id,
|
||||
nw.name,
|
||||
authed.email,
|
||||
nw.domain
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -384,7 +459,6 @@ async fn create_workspace(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Created workspace {}", &nw.id))
|
||||
}
|
||||
|
||||
@@ -398,10 +472,9 @@ async fn edit_workspace(
|
||||
require_admin(is_admin, &username)?;
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!(
|
||||
"UPDATE workspace SET name = $1, owner = $2, domain = $3 WHERE id = $4",
|
||||
"UPDATE workspace SET name = $1, owner = $2 WHERE id = $3",
|
||||
ew.name,
|
||||
ew.owner,
|
||||
ew.domain,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
@@ -414,13 +487,7 @@ async fn edit_workspace(
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some(
|
||||
[(
|
||||
"domain",
|
||||
ew.domain.unwrap_or("NO_DOMAIN".to_string()).as_str(),
|
||||
)]
|
||||
.into(),
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -463,6 +530,31 @@ async fn delete_workspace(
|
||||
Ok(format!("Deleted workspace {}", &w_id))
|
||||
}
|
||||
|
||||
pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> Result<()> {
|
||||
let mut tx = db.begin().await?;
|
||||
let domain = email.split('@').last().unwrap();
|
||||
let workspaces = sqlx::query_scalar!(
|
||||
"SELECT workspace_id FROM workspace_settings WHERE auto_invite_domain = $1",
|
||||
domain
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
for w in workspaces {
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_invite
|
||||
(workspace_id, email, is_admin)
|
||||
VALUES ($1, $2, false)
|
||||
ON CONFLICT DO NOTHING",
|
||||
w,
|
||||
email,
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn invite_user(
|
||||
Authed { username, is_admin, .. }: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
|
||||
@@ -128,9 +128,9 @@
|
||||
deleteConfirmedCallback = async () => {
|
||||
await UserService.globalUserDelete({ email })
|
||||
sendUserToast(`User ${email} removed`)
|
||||
listUsers()
|
||||
await listUsers()
|
||||
}
|
||||
openConfirmation = true
|
||||
openConfirmation = false
|
||||
}}>remove</button
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { faDashboard, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { AppService, Policy } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
@@ -60,7 +60,9 @@
|
||||
</div>
|
||||
</Path>
|
||||
|
||||
<Button on:click={() => createApp()}>Create app</Button>
|
||||
<div slot="actions">
|
||||
<Button on:click={() => createApp()}>Create app</Button>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import type { ListableApp } from '$lib/gen'
|
||||
import { AppService, type ListableApp } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { faEdit, faEye } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEdit, faEye, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
import Row from './Row.svelte'
|
||||
|
||||
@@ -12,6 +13,8 @@
|
||||
export let starred: boolean
|
||||
|
||||
let { summary, path, extra_perms, canWrite, workspace_id } = app
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<Row
|
||||
@@ -28,38 +31,49 @@
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<span class="hidden md:inline-flex gap-x-1">
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/apps/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
href="/apps/get/{path}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
</span>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'View app',
|
||||
icon: faEye,
|
||||
href: `/apps/get/${path}`
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: faTrashAlt,
|
||||
action: async () => {
|
||||
await AppService.deleteApp({ workspace: $workspaceStore ?? '', path })
|
||||
dispatch('change')
|
||||
},
|
||||
type: 'delete',
|
||||
disabled: !canWrite
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/apps/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
href="/apps/get/{path}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Row>
|
||||
|
||||
@@ -55,7 +55,56 @@
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<span class="hidden md:inline-flex gap-x-1">
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/flows/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
href="/flows/add?template={path}"
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
href="/flows/get/{path}"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href="/flows/run/{path}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
endIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'View flow',
|
||||
@@ -102,51 +151,5 @@
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/flows/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
href="/flows/add?template={path}"
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
href="/flows/get/{path}"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href="/flows/run/{path}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
endIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Row>
|
||||
|
||||
@@ -29,19 +29,19 @@
|
||||
>
|
||||
<RowIcon {kind} />
|
||||
|
||||
<div class="w-full">
|
||||
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
|
||||
<div class="w-full min-w-0 ">
|
||||
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1 truncate">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-gray-600 text-xs ">
|
||||
<div class="text-gray-600 text-xs truncate">
|
||||
{path}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-96 flex flex-row max-w-xs gap-1 items-start flex-wrap">
|
||||
<div class="w-96 hidden lg:flex flex-row max-w-xs gap-1 items-start flex-wrap">
|
||||
<slot name="badges" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -73,7 +73,55 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<span class="hidden md:inline-flex gap-x-1">
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/scripts/edit/{hash}?step=2"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
href="/scripts/add?template={path}"
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
href="/scripts/get/{hash}"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href="/scripts/run/{hash}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
endIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</span>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'View script',
|
||||
@@ -126,51 +174,5 @@
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/scripts/edit/{hash}?step=2"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
href="/scripts/add?template={path}"
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
href="/scripts/get/{hash}"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href="/scripts/run/{hash}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
endIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Row>
|
||||
|
||||
@@ -38,7 +38,7 @@ export const userWorkspaces: Readable<
|
||||
([store, superadmin]) => {
|
||||
const originalWorkspaces = (store?.workspaces ?? []);
|
||||
if (superadmin) {
|
||||
return [...originalWorkspaces, {
|
||||
return [...originalWorkspaces.filter((x) => x.id != 'starter' && x.id != 'admins'), {
|
||||
id: "admins",
|
||||
name: "Admins",
|
||||
username: "superadmin",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
})
|
||||
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
||||
workspaceStore.set(workspace_id)
|
||||
goto($page.url.searchParams.get('rd') ?? '/scripts')
|
||||
goto($page.url.searchParams.get('rd') ?? '/')
|
||||
}
|
||||
|
||||
async function validateName(username: string): Promise<void> {
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
import { sendUserToast, validateUsername } from '$lib/utils'
|
||||
import { logoutWithRedirect } from '$lib/logout'
|
||||
import { page } from '$app/stores'
|
||||
import { usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
||||
import { userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
const rd = $page.url.searchParams.get('rd')
|
||||
|
||||
let id = ''
|
||||
let name = ''
|
||||
let username = ''
|
||||
let domain = ''
|
||||
|
||||
let errorId = ''
|
||||
let errorUser = ''
|
||||
@@ -39,11 +39,17 @@
|
||||
requestBody: {
|
||||
id,
|
||||
name,
|
||||
username,
|
||||
domain
|
||||
username
|
||||
}
|
||||
})
|
||||
if (auto_invite) {
|
||||
await WorkspaceService.editAutoInvite({
|
||||
workspace: id,
|
||||
requestBody: { set: true }
|
||||
})
|
||||
}
|
||||
sendUserToast(`Successfully created workspace id: ${id}`)
|
||||
|
||||
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
||||
workspaceStore.set(id)
|
||||
goto(rd ?? '/')
|
||||
@@ -78,6 +84,16 @@
|
||||
}
|
||||
username = username.toLowerCase()
|
||||
})
|
||||
|
||||
WorkspaceService.isDomainAllowed().then((x) => {
|
||||
isDomainAllowed = x
|
||||
})
|
||||
|
||||
let isDomainAllowed = false
|
||||
|
||||
$: domain = $usersWorkspaceStore?.email.split('@')[1]
|
||||
|
||||
let auto_invite = false
|
||||
</script>
|
||||
|
||||
<CenteredModal title="New Workspace">
|
||||
@@ -99,6 +115,14 @@
|
||||
{/if}
|
||||
<input type="text" bind:value={username} on:keyup={handleKeyUp} class="mt-1" />
|
||||
</label>
|
||||
<Toggle
|
||||
disabled={!isDomainAllowed}
|
||||
bind:checked={auto_invite}
|
||||
options={{ right: `Auto invite users with the same domain (${domain})` }}
|
||||
/>
|
||||
{#if !isDomainAllowed}
|
||||
<div class="text-gray-600 text-sm mb-4 mt-2">{domain} domain not allowed for auto-invite</div>
|
||||
{/if}
|
||||
<div class="flex flex-row justify-between pt-4 gap-x-1">
|
||||
<Button variant="border" size="sm" href="/user/workspaces"
|
||||
>← Back to workspaces</Button
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import InviteUser from '$lib/components/InviteUser.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import { Button, Skeleton } from '$lib/components/common'
|
||||
import { Badge, Button, Skeleton } from '$lib/components/common'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { faScroll, faBarsStaggered } from '@fortawesome/free-solid-svg-icons'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
@@ -36,6 +36,7 @@
|
||||
let scriptPath: string
|
||||
let initialPath: string
|
||||
let team_name: string | undefined
|
||||
let auto_invite_domain: string | undefined
|
||||
let itemKind: 'flow' | 'script' = 'flow'
|
||||
|
||||
// function getDropDownItems(username: string): DropdownItem[] {
|
||||
@@ -71,9 +72,10 @@
|
||||
sendUserToast(`slack command script set to ${scriptPath}`)
|
||||
}
|
||||
|
||||
async function loadSlack(): Promise<void> {
|
||||
async function loadSettings(): Promise<void> {
|
||||
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
||||
team_name = settings.slack_name
|
||||
auto_invite_domain = settings.auto_invite_domain
|
||||
scriptPath = (settings.slack_command_script ?? '').split('/').slice(1).join('/')
|
||||
initialPath = scriptPath
|
||||
}
|
||||
@@ -86,11 +88,20 @@
|
||||
invites = await WorkspaceService.listPendingInvites({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
let allowedAutoDomain = false
|
||||
|
||||
async function getDisallowedAutoDomain() {
|
||||
allowedAutoDomain = await WorkspaceService.isDomainAllowed()
|
||||
}
|
||||
|
||||
$: domain = $userStore?.email.split('@')[1]
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
getDisallowedAutoDomain()
|
||||
listUsers()
|
||||
listInvites()
|
||||
loadSlack()
|
||||
loadSettings()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -210,9 +221,42 @@
|
||||
</TableCustom>
|
||||
</div>
|
||||
<div class="mt-20" />
|
||||
<PageHeader title="Auto Invite" primary={false} />
|
||||
<div class="flex gap-2">
|
||||
{#if auto_invite_domain != domain}
|
||||
<Button
|
||||
disabled={!allowedAutoDomain}
|
||||
on:click={async () => {
|
||||
await WorkspaceService.editAutoInvite({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { set: true }
|
||||
})
|
||||
loadSettings()
|
||||
listInvites()
|
||||
}}>Set auto-invite to {domain}</Button
|
||||
>
|
||||
{/if}
|
||||
{#if auto_invite_domain}
|
||||
<Button
|
||||
on:click={async () => {
|
||||
await WorkspaceService.editAutoInvite({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { set: false }
|
||||
})
|
||||
loadSettings()
|
||||
listInvites()
|
||||
}}>Unset auto-invite from {auto_invite_domain}</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !allowedAutoDomain}
|
||||
<div class="text-red-400 text-sm mb-2">{domain} domain not allowed for auto-invite</div>
|
||||
{/if}
|
||||
<div class="mt-20" />
|
||||
<PageHeader title="Slack integration" primary={false} />
|
||||
<p class="text-xs text-gray-700 my-1">
|
||||
Status: {#if team_name}Connected to slack workspace {team_name}{:else}Not connected{/if}
|
||||
Status: {#if team_name}Connected to slack workspace <Badge>{team_name}</Badge>{:else}Not
|
||||
connected{/if}
|
||||
</p>
|
||||
|
||||
{#if team_name}
|
||||
@@ -226,7 +270,7 @@
|
||||
await OauthService.disconnectSlack({
|
||||
workspace: $workspaceStore ?? ''
|
||||
})
|
||||
loadSlack()
|
||||
loadSettings()
|
||||
sendUserToast('Disconnected Slack')
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user