feat: refactor variable + resource linkage + OAuth visibility
This commit is contained in:
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE resource DROP COLUMN is_oauth;
|
||||
@@ -1,96 +1,75 @@
|
||||
{
|
||||
"github": {
|
||||
"auth_url": "https://github.com/login/oauth/authorize",
|
||||
"token_url": "https://github.com/login/oauth/access_token",
|
||||
"scopes": [
|
||||
"workflow",
|
||||
"repo"
|
||||
]
|
||||
},
|
||||
"gitlab": {
|
||||
"auth_url": "https://gitlab.com/oauth/authorize",
|
||||
"token_url": "https://gitlab.com/oauth/token",
|
||||
"scopes": [
|
||||
"api"
|
||||
]
|
||||
},
|
||||
"bitbucket": {
|
||||
"auth_url": "https://bitbucket.org/site/oauth2/authorize",
|
||||
"token_url": "https://bitbucket.org/site/oauth2/access_token",
|
||||
"scopes": [
|
||||
"repository"
|
||||
]
|
||||
},
|
||||
"slack": {
|
||||
"auth_url": "https://slack.com/oauth/authorize",
|
||||
"token_url": "https://slack.com/api/oauth.access",
|
||||
"scopes": [
|
||||
"chat:write:user",
|
||||
"users:read",
|
||||
"users:read.email"
|
||||
]
|
||||
},
|
||||
"gsheets": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/spreadsheets"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"consent": "prompt"
|
||||
}
|
||||
},
|
||||
"gdrive": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/drive"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"consent": "prompt"
|
||||
}
|
||||
},
|
||||
"gmail": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/gmail.send"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"consent": "prompt"
|
||||
}
|
||||
},
|
||||
"gcal": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/calendar.events"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"consent": "prompt"
|
||||
}
|
||||
},
|
||||
"gcloud": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/cloud-platform"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"consent": "prompt"
|
||||
}
|
||||
},
|
||||
"basecamp": {
|
||||
"auth_url": "https://launchpad.37signals.com/authorization/new",
|
||||
"token_url": "https://launchpad.37signals.com/authorization/token",
|
||||
"scopes": [],
|
||||
"extra_params": {
|
||||
"type": "web_server"
|
||||
}
|
||||
"github": {
|
||||
"auth_url": "https://github.com/login/oauth/authorize",
|
||||
"token_url": "https://github.com/login/oauth/access_token",
|
||||
"scopes": ["workflow", "repo"]
|
||||
},
|
||||
"gitlab": {
|
||||
"auth_url": "https://gitlab.com/oauth/authorize",
|
||||
"token_url": "https://gitlab.com/oauth/token",
|
||||
"scopes": ["api"]
|
||||
},
|
||||
"bitbucket": {
|
||||
"auth_url": "https://bitbucket.org/site/oauth2/authorize",
|
||||
"token_url": "https://bitbucket.org/site/oauth2/access_token",
|
||||
"scopes": ["repository"]
|
||||
},
|
||||
"slack": {
|
||||
"auth_url": "https://slack.com/oauth/authorize",
|
||||
"token_url": "https://slack.com/api/oauth.access",
|
||||
"scopes": ["chat:write:user", "users:read", "users:read.email"]
|
||||
},
|
||||
"gsheets": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/spreadsheets"],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
}
|
||||
},
|
||||
"gdrive": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive"],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
}
|
||||
},
|
||||
"gmail": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/gmail.send"],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
}
|
||||
},
|
||||
"gcal": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/calendar.events"],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
}
|
||||
},
|
||||
"gcloud": {
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/cloud-platform"],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
}
|
||||
},
|
||||
"basecamp": {
|
||||
"auth_url": "https://launchpad.37signals.com/authorization/new",
|
||||
"token_url": "https://launchpad.37signals.com/authorization/token",
|
||||
"scopes": [],
|
||||
"extra_params": {
|
||||
"type": "web_server"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,23 +224,6 @@
|
||||
},
|
||||
"query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, $4)"
|
||||
},
|
||||
"0c3a39eafc349870be019318d6925922558ac20fdd76b042d69ccd8a527e3ff5": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3"
|
||||
},
|
||||
"0d6412bc3ebb1d58bdd9cbcef774dacf9016fa402af5c1b4e339b9a3d7163d5e": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -678,23 +661,6 @@
|
||||
},
|
||||
"query": "SELECT now()"
|
||||
},
|
||||
"27eb5f99dc9289670673fb999ba9e67abccba212b506c30ba47e6c4fef6d53c4": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6)"
|
||||
},
|
||||
"282655dc568d000379196fae0dee32438a824d93f4ad2fcc155269e76f4ca20d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -1072,6 +1038,20 @@
|
||||
},
|
||||
"query": "UPDATE queue SET logs = concat(logs, $1::text) WHERE id = $2"
|
||||
},
|
||||
"493be465bf70411f933de8dd4da1c5b410fd5c79a0c17172e7882e292c1378b5": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3"
|
||||
},
|
||||
"499978e08a3a5e4d09d20f4212dbeff6ed5c5d4a696243cd3687c71099e8a6a6": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1911,6 +1891,87 @@
|
||||
},
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)"
|
||||
},
|
||||
"71b953191b3175a32a22f803205a5fd3dedef1baa0993afad08109ab5bf62fe9": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"ordinal": 1,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"ordinal": 2,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "resource_type",
|
||||
"ordinal": 4,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "extra_perms",
|
||||
"ordinal": 5,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "is_expired",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "refresh_error",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "is_linked",
|
||||
"ordinal": 8,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "is_oauth?",
|
||||
"ordinal": 9,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_error,\n variable.path IS NOT NULL as is_linked,\n variable.is_oauth as \"is_oauth?\",\n variable.account\n FROM resource\n LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id\n LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id\n WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')"
|
||||
},
|
||||
"765c18d77412cbb4474f4074d583b9b44681f3b9f58754662ac07a3a3470a3c5": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2138,6 +2199,20 @@
|
||||
},
|
||||
"query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3"
|
||||
},
|
||||
"83fc9bf32b9b40987feff61fea82e20613e74efb7bf8ba52485566667ccf2284": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3"
|
||||
},
|
||||
"853788436dbe987853433e8dc83665f68bd127de31d4c807abafeead896f6ac4": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2246,6 +2321,22 @@
|
||||
},
|
||||
"query": "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2"
|
||||
},
|
||||
"8a80333c2fbf7b50fed305882de6e4ffda985d5c648cd617add6c9e6a9c03f34": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5)"
|
||||
},
|
||||
"8ad6a17eecce77f61236e0585ba89b99a32e07ad37b97662007db35acb59d139": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2485,63 +2576,6 @@
|
||||
},
|
||||
"query": "UPDATE password SET password_hash = $1 WHERE email = $2"
|
||||
},
|
||||
"904457944cbfdcefa1934059bbfea015a411c739124cf06367975cfd9cd0dd0d": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"ordinal": 1,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"ordinal": 2,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "resource_type",
|
||||
"ordinal": 4,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "extra_perms",
|
||||
"ordinal": 5,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "is_oauth",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')"
|
||||
},
|
||||
"90719f6230467b08e5f2cc89271bcf5e4a6cec39e9d9b42ef3b90f09b3135b83": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3997,6 +4031,22 @@
|
||||
},
|
||||
"query": "DELETE FROM password WHERE email = $1"
|
||||
},
|
||||
"ea8ebb8d972fe99c960b5a69f794ee2b57bfb1914bf370c5b10313e45fa9b65f": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3"
|
||||
},
|
||||
"ed400dbf4a1fa597bc984240cfe9069369a8a265b127116187d78c0ba93ae076": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -4374,11 +4424,6 @@
|
||||
"name": "extra_perms",
|
||||
"ordinal": 5,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "is_oauth",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
@@ -4387,7 +4432,6 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
|
||||
@@ -1331,7 +1331,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Resource"
|
||||
$ref: "#/components/schemas/ListableResource"
|
||||
|
||||
/w/{workspace}/resources/get_value/{path}:
|
||||
get:
|
||||
@@ -1389,7 +1389,7 @@ paths:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Resource"
|
||||
$ref: "#/components/schemas/ListableResource"
|
||||
|
||||
/w/{workspace}/resources/type/create:
|
||||
post:
|
||||
@@ -4132,6 +4132,8 @@ components:
|
||||
type: boolean
|
||||
refresh_error:
|
||||
type: string
|
||||
is_linked:
|
||||
type: boolean
|
||||
required:
|
||||
- workspace_id
|
||||
- path
|
||||
@@ -4366,8 +4368,6 @@ components:
|
||||
type: string
|
||||
resource_type:
|
||||
type: string
|
||||
is_oauth:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- value
|
||||
@@ -4405,6 +4405,38 @@ components:
|
||||
- resource_type
|
||||
- is_oauth
|
||||
|
||||
ListableResource:
|
||||
type: object
|
||||
properties:
|
||||
workspace_id:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
resource_type:
|
||||
type: string
|
||||
value: {}
|
||||
is_oauth:
|
||||
type: boolean
|
||||
extra_perms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
is_expired:
|
||||
type: boolean
|
||||
refresh_error:
|
||||
type: string
|
||||
is_linked:
|
||||
type: boolean
|
||||
account:
|
||||
type: number
|
||||
required:
|
||||
- path
|
||||
- resource_type
|
||||
- is_oauth
|
||||
- is_linked
|
||||
|
||||
ResourceType:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -514,7 +514,7 @@ pub async fn _refresh_token<'c>(
|
||||
if let Err(token_err) = token {
|
||||
sqlx::query!(
|
||||
"UPDATE account SET refresh_error = $1 WHERE workspace_id = $2 AND id = $3",
|
||||
token_err.to_string(),
|
||||
token_err.alt(),
|
||||
w_id,
|
||||
id,
|
||||
)
|
||||
@@ -522,8 +522,8 @@ pub async fn _refresh_token<'c>(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Error refreshing token: {:#?}",
|
||||
token_err
|
||||
"Error refreshing token: {}",
|
||||
token_err.alt()
|
||||
)));
|
||||
};
|
||||
|
||||
@@ -676,14 +676,13 @@ async fn connect_slack_callback(
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO resource
|
||||
(workspace_id, path, value, description, resource_type, is_oauth)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3",
|
||||
(workspace_id, path, value, description, resource_type)
|
||||
VALUES ($1, $2, $3, $4, $5) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3",
|
||||
w_id,
|
||||
token_path,
|
||||
serde_json::json!({ "token": format!("$var:{token_path}") }),
|
||||
"The slack bot token to act on behalf of the installed app of the connected workspace",
|
||||
"slack",
|
||||
true
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
@@ -72,7 +72,21 @@ pub struct Resource {
|
||||
pub description: Option<String>,
|
||||
pub resource_type: String,
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub is_oauth: bool,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct ListableResource {
|
||||
pub workspace_id: String,
|
||||
pub path: String,
|
||||
pub value: Option<serde_json::Value>,
|
||||
pub description: Option<String>,
|
||||
pub resource_type: String,
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub is_linked: Option<bool>,
|
||||
pub is_oauth: Option<bool>,
|
||||
pub is_expired: Option<bool>,
|
||||
pub refresh_error: Option<String>,
|
||||
pub account: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -81,7 +95,6 @@ pub struct CreateResource {
|
||||
pub value: Option<serde_json::Value>,
|
||||
pub description: Option<String>,
|
||||
pub resource_type: String,
|
||||
pub is_oauth: Option<bool>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct EditResource {
|
||||
@@ -100,31 +113,42 @@ async fn list_resources(
|
||||
Query(pagination): Query<Pagination>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<Resource>> {
|
||||
) -> JsonResult<Vec<ListableResource>> {
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
|
||||
let mut sqlb = SqlBuilder::select_from("resource")
|
||||
.fields(&[
|
||||
"workspace_id",
|
||||
"path",
|
||||
"resource.workspace_id",
|
||||
"resource.path",
|
||||
"null::JSONB as value",
|
||||
"description",
|
||||
"resource.description",
|
||||
"resource_type",
|
||||
"extra_perms",
|
||||
"is_oauth",
|
||||
"resource.extra_perms",
|
||||
"(now() > account.expires_at) as is_expired",
|
||||
"variable.path IS NOT NULL as is_linked",
|
||||
"variable.is_oauth",
|
||||
"variable.account",
|
||||
"account.refresh_error",
|
||||
])
|
||||
.left()
|
||||
.join("variable")
|
||||
.on("variable.path = resource.path AND variable.workspace_id = resource.workspace_id")
|
||||
.left()
|
||||
.join("account")
|
||||
.on("variable.account = account.id AND account.workspace_id = variable.workspace_id")
|
||||
.order_by("path", true)
|
||||
.and_where("workspace_id = ? OR workspace_id = 'starter'".bind(&w_id))
|
||||
.and_where("resource.workspace_id = ? OR resource.workspace_id = 'starter'".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
|
||||
if let Some(rt) = &lq.resource_type {
|
||||
sqlb.and_where_eq("resource_type", "?".bind(rt));
|
||||
}
|
||||
|
||||
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let rows = sqlx::query_as::<_, Resource>(&sql)
|
||||
let rows = sqlx::query_as::<_, ListableResource>(&sql)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
|
||||
@@ -137,14 +161,20 @@ async fn get_resource(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<Resource> {
|
||||
) -> JsonResult<ListableResource> {
|
||||
let path = path.to_path();
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let resource_o = sqlx::query_as!(
|
||||
Resource,
|
||||
"SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = \
|
||||
'starter')",
|
||||
ListableResource,
|
||||
"SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_error,
|
||||
variable.path IS NOT NULL as is_linked,
|
||||
variable.is_oauth as \"is_oauth?\",
|
||||
variable.account
|
||||
FROM resource
|
||||
LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id
|
||||
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id
|
||||
WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')",
|
||||
path.to_owned(),
|
||||
&w_id
|
||||
)
|
||||
@@ -206,14 +236,13 @@ async fn create_resource(
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO resource
|
||||
(workspace_id, path, value, description, resource_type, is_oauth)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
(workspace_id, path, value, description, resource_type)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
w_id,
|
||||
resource.path,
|
||||
resource.value,
|
||||
resource.description,
|
||||
resource.resource_type,
|
||||
resource.is_oauth.unwrap_or(false)
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -250,6 +279,13 @@ async fn delete_resource(
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"DELETE FROM variable WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
@@ -298,6 +334,17 @@ async fn update_resource(
|
||||
|
||||
let npath = not_found_if_none(npath_o, "Resource", path)?;
|
||||
|
||||
if let Some(npath) = ns.path {
|
||||
sqlx::query!(
|
||||
"UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3",
|
||||
npath,
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
|
||||
@@ -82,10 +82,13 @@ async fn list_variables(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let rows = sqlx::query_as::<_, ListableVariable>(
|
||||
"SELECT variable.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, (now() > account.expires_at) as is_expired,
|
||||
account.refresh_error from variable
|
||||
"SELECT variable.workspace_id, variable.path, CASE WHEN is_secret IS TRUE THEN null ELSE variable.value::text END as value,
|
||||
is_secret, variable.description, variable.extra_perms, account, is_oauth, (now() > account.expires_at) as is_expired,
|
||||
account.refresh_error,
|
||||
resource.path IS NOT NULL as is_linked
|
||||
from variable
|
||||
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = variable.workspace_id
|
||||
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
|
||||
WHERE variable.workspace_id = $1 OR (is_secret IS NOT TRUE AND variable.workspace_id = 'starter') ORDER BY path",
|
||||
)
|
||||
.bind(&w_id)
|
||||
@@ -113,8 +116,11 @@ async fn get_variable(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let variable_o = sqlx::query_as::<_, ListableVariable>(
|
||||
"SELECT variable.*, (now() > account.expires_at) as is_expired, account.refresh_error from variable
|
||||
"SELECT variable.*, (now() > account.expires_at) as is_expired, account.refresh_error,
|
||||
resource.path IS NOT NULL as is_linked
|
||||
from variable
|
||||
LEFT JOIN account ON variable.account = account.id
|
||||
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
|
||||
WHERE variable.path = $1 AND (variable.workspace_id = $2 OR (is_secret IS NOT TRUE AND \
|
||||
variable.workspace_id = 'starter'))
|
||||
LIMIT 1",
|
||||
@@ -255,6 +261,13 @@ async fn delete_variable(
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"DELETE FROM resource WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
@@ -335,6 +348,17 @@ async fn update_variable(
|
||||
|
||||
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?;
|
||||
|
||||
if let Some(npath) = ns.path {
|
||||
sqlx::query!(
|
||||
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
|
||||
npath,
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let npath = not_found_if_none(npath_o, "Variable", path)?;
|
||||
|
||||
audit_log(
|
||||
|
||||
@@ -27,9 +27,10 @@ pub struct ListableVariable {
|
||||
pub description: String,
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub account: Option<i32>,
|
||||
pub is_oauth: bool,
|
||||
pub is_oauth: Option<bool>,
|
||||
pub is_expired: Option<bool>,
|
||||
pub refresh_error: Option<String>,
|
||||
pub is_linked: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -316,17 +316,17 @@ async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyho
|
||||
}
|
||||
|
||||
#[op]
|
||||
async fn op_resource(
|
||||
args: Vec<String>,
|
||||
) -> Result<windmill_api_client::types::Resource, anyhow::Error> {
|
||||
async fn op_resource(args: Vec<String>) -> Result<serde_json::Value, anyhow::Error> {
|
||||
let workspace = &args[0];
|
||||
let path = &args[1];
|
||||
let token = &args[2];
|
||||
let base_url = &args[3];
|
||||
let client = windmill_api_client::create_client(base_url, token.clone());
|
||||
let result = client.get_resource(workspace, path).await?;
|
||||
// TODO: verify this works. Previously this returned Option<serde_jons::Value>, now it's statically typed.
|
||||
Ok(result.into_inner())
|
||||
Ok(result
|
||||
.into_inner()
|
||||
.value
|
||||
.unwrap_or_else(|| serde_json::json!({})))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -151,8 +151,7 @@
|
||||
resource_type,
|
||||
path,
|
||||
value: resourceValue,
|
||||
description,
|
||||
is_oauth: !manual
|
||||
description
|
||||
}
|
||||
})
|
||||
dispatch('refresh')
|
||||
|
||||
@@ -155,7 +155,8 @@
|
||||
{#if edit && variable.is_secret}<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => loadVariable(initialPath)}>Load secret value</Button
|
||||
on:click={() => loadVariable(initialPath)}
|
||||
>Load secret value<Tooltip>Will generate an audit log</Tooltip></Button
|
||||
>{/if}
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { canWrite, emptySchema, sendUserToast, truncate } from '$lib/utils'
|
||||
import { ResourceService, VariableService } from '$lib/gen'
|
||||
import { OauthService, ResourceService, VariableService, type ListableResource } from '$lib/gen'
|
||||
import type { Resource, ResourceType } from '$lib/gen'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import ResourceEditor from '$lib/components/ResourceEditor.svelte'
|
||||
@@ -31,7 +31,8 @@
|
||||
faTrash,
|
||||
faCircle,
|
||||
faChain,
|
||||
faSave
|
||||
faSave,
|
||||
faRefresh
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
@@ -47,7 +48,7 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
type ResourceW = Resource & { canWrite: boolean }
|
||||
type ResourceW = ListableResource & { canWrite: boolean }
|
||||
type ResourceTypeW = ResourceType & { canWrite: boolean }
|
||||
|
||||
let resources: ResourceW[] | undefined
|
||||
@@ -94,9 +95,9 @@
|
||||
loading.types = false
|
||||
}
|
||||
|
||||
async function deleteResource(path: string, is_oauth: boolean): Promise<void> {
|
||||
if (is_oauth) {
|
||||
await VariableService.deleteVariable({ workspace: $workspaceStore!, path })
|
||||
async function deleteResource(path: string, account?: number): Promise<void> {
|
||||
if (account) {
|
||||
OauthService.disconnectAccount({ workspace: $workspaceStore!, id: account })
|
||||
}
|
||||
await ResourceService.deleteResource({ workspace: $workspaceStore!, path })
|
||||
loadResources()
|
||||
@@ -253,12 +254,12 @@
|
||||
<th>path</th>
|
||||
<th>resource type</th>
|
||||
<th>description</th>
|
||||
<th>OAuth</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if resources}
|
||||
{#each resources as { path, description, resource_type, extra_perms, canWrite, is_oauth }}
|
||||
{#each resources as { path, description, resource_type, extra_perms, canWrite, is_oauth, is_linked, account, refresh_error, is_expired }}
|
||||
<tr>
|
||||
<td class="my-12"
|
||||
><a
|
||||
@@ -285,20 +286,74 @@
|
||||
></td
|
||||
>
|
||||
<td class="text-center">
|
||||
{#if is_oauth}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-green-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is tied to an OAuth app"
|
||||
/>
|
||||
<div slot="text">
|
||||
The resource is tied to an OAuth app. The token is refreshed automatically
|
||||
if applicable.
|
||||
<div class="flex flex-row">
|
||||
<div class="w-10">
|
||||
{#if is_linked}
|
||||
<Popover>
|
||||
<Icon data={faChain} />
|
||||
<div slot="text">
|
||||
This resource is linked with a variable of the same path. They are
|
||||
deleted and renamed together.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-10">
|
||||
{#if account}
|
||||
<Popover>
|
||||
<Icon data={faRefresh} />
|
||||
<div slot="text">
|
||||
The OAuth token is kept up-to-date in the background by Windmill using
|
||||
its refresh token
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if is_oauth}
|
||||
<div class="w-10">
|
||||
{#if refresh_error}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-red-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during exchange of the refresh token"
|
||||
/>
|
||||
<div slot="text">
|
||||
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
|
||||
</div>
|
||||
</Popover>
|
||||
{:else if is_expired}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is expired"
|
||||
/>
|
||||
<div slot="text">
|
||||
The access_token is expired, it will get renewed the next time this
|
||||
variable is fetched or you can request is to be refreshed in the
|
||||
dropdown on the right.
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-green-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is tied to an OAuth app"
|
||||
/>
|
||||
<div slot="text">
|
||||
The resource was connected through OAuth and the token is not expired.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<Dropdown
|
||||
@@ -326,14 +381,33 @@
|
||||
type: 'delete',
|
||||
action: (event) => {
|
||||
if (event?.shiftKey) {
|
||||
deleteResource(path, is_oauth)
|
||||
deleteResource(path, account)
|
||||
} else {
|
||||
deleteConfirmedCallback = () => {
|
||||
deleteResource(path, is_oauth)
|
||||
deleteResource(path, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...(account != undefined
|
||||
? [
|
||||
{
|
||||
displayName: 'Refresh token',
|
||||
icon: faRefresh,
|
||||
action: async () => {
|
||||
await OauthService.refreshToken({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: account ?? 0,
|
||||
requestBody: {
|
||||
path
|
||||
}
|
||||
})
|
||||
sendUserToast('Token refreshed')
|
||||
loadResources()
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
relative={true}
|
||||
/>
|
||||
|
||||
@@ -20,7 +20,18 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faPlus, faCircle, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faPlus,
|
||||
faCircle,
|
||||
faEyeSlash,
|
||||
faRotateRight,
|
||||
faArrowRotateRight,
|
||||
faRefresh,
|
||||
faChain,
|
||||
faTrash,
|
||||
faEdit,
|
||||
faShare
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button } from '$lib/components/common'
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { Alert, Badge, Skeleton } from '$lib/components/common'
|
||||
@@ -98,11 +109,11 @@
|
||||
|
||||
<th>value</th>
|
||||
<th>description</th>
|
||||
<th>OAuth</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each variables as { path, value, is_secret, description, extra_perms, canWrite, account, is_oauth, is_expired, refresh_error }}
|
||||
{#each variables as { path, value, is_secret, description, extra_perms, canWrite, account, is_oauth, is_expired, refresh_error, is_linked }}
|
||||
<tr>
|
||||
<td
|
||||
><a
|
||||
@@ -136,59 +147,88 @@
|
||||
>
|
||||
|
||||
<td class="text-center">
|
||||
{#if is_oauth}
|
||||
{#if refresh_error}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-red-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during exchange of the refresh token"
|
||||
/>
|
||||
<div slot="text">
|
||||
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
|
||||
</div>
|
||||
</Popover>
|
||||
{:else if is_expired}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is expired"
|
||||
/>
|
||||
<div slot="text">
|
||||
The access_token is expired, it will get renewed the next time this variable
|
||||
is fetched or you can request is to be refreshed in the variable dropdown.
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-green-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is tied to an OAuth app"
|
||||
/>
|
||||
<div slot="text">
|
||||
The variable is tied to an OAuth app. The token is refreshed automatically
|
||||
if applicable.
|
||||
</div>
|
||||
</Popover>
|
||||
<div class="flex flex-row">
|
||||
<div class="w-10">
|
||||
{#if is_linked}
|
||||
<Popover>
|
||||
<Icon data={faChain} />
|
||||
<div slot="text">
|
||||
This variable is linked with a resource of the same path. They are deleted
|
||||
and renamed together.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-10">
|
||||
{#if account}
|
||||
<Popover>
|
||||
<Icon data={faRefresh} />
|
||||
<div slot="text">
|
||||
This OAuth token is kept up-to-date in the background by Windmill using
|
||||
its refresh token
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if is_oauth}
|
||||
<div class="w-10">
|
||||
{#if refresh_error}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-red-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during exchange of the refresh token"
|
||||
/>
|
||||
<div slot="text">
|
||||
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
|
||||
</div>
|
||||
</Popover>
|
||||
{:else if is_expired}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is expired"
|
||||
/>
|
||||
<div slot="text">
|
||||
The access_token is expired, it will get renewed the next time this
|
||||
variable is fetched or you can request is to be refreshed in the
|
||||
dropdown on the right.
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-green-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is tied to an OAuth app"
|
||||
/>
|
||||
<div slot="text">
|
||||
The variable was connected through OAuth and the token is not expired.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
><Dropdown
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
action: () => variableEditor.editVariable(path),
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
|
||||
icon: faTrash,
|
||||
type: 'delete',
|
||||
action: (event) => {
|
||||
if (event?.shiftKey) {
|
||||
deleteVariable(path, account)
|
||||
@@ -205,12 +245,14 @@
|
||||
action: () => {
|
||||
shareModal.openDrawer(path)
|
||||
},
|
||||
icon: faShare,
|
||||
disabled: !canWrite
|
||||
},
|
||||
...(account != undefined
|
||||
? [
|
||||
{
|
||||
displayName: 'Refresh token',
|
||||
icon: faRefresh,
|
||||
action: async () => {
|
||||
await OauthService.refreshToken({
|
||||
workspace: $workspaceStore ?? '',
|
||||
@@ -220,6 +262,7 @@
|
||||
}
|
||||
})
|
||||
sendUserToast('Token refreshed')
|
||||
loadVariables()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user