feat: account part II, handle refresh tokens, clarify oauth UI (#196)

This commit is contained in:
Ruben Fiszel
2022-07-13 10:35:08 +02:00
committed by GitHub
parent 4a1a2e5db8
commit 670e55b34b
15 changed files with 565 additions and 92 deletions

View File

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

View File

@@ -0,0 +1,25 @@
-- Add up migration script here
ALTER TABLE account ADD COLUMN owner VARCHAR(50) NOT NULL;
ALTER TABLE account ADD COLUMN client VARCHAR(50) NOT NULL;
ALTER TABLE resource ADD COLUMN is_oauth BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE variable ADD COLUMN is_oauth BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE resource DROP COLUMN account;
ALTER TABLE account ALTER COLUMN expires_at TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE account ALTER COLUMN expires_at SET NOT NULL;
ALTER TABLE account ALTER COLUMN refresh_token SET NOT NULL;
GRANT ALL ON account TO app;
GRANT ALL ON account TO admin;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO admin;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app;
ALTER TABLE account ENABLE ROW LEVEL SECURITY;
CREATE POLICY see_own ON account FOR ALL
USING (SPLIT_PART(account.owner, '/', 1) = 'u' AND SPLIT_PART(account.owner, '/', 2) = current_setting('session.user'));
CREATE POLICY see_member ON account FOR ALL
USING (SPLIT_PART(account.owner, '/', 1) = 'g' AND SPLIT_PART(account.owner, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));

View File

@@ -13,5 +13,19 @@
"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"
]
}
}

View File

@@ -1067,12 +1067,75 @@ paths:
content:
application/json:
schema:
type: object
properties:
token:
type: string
$ref: "#/components/schemas/TokenResponse"
/w/{workspace}/oauth/disconnect/{account_id}:
/w/{workspace}/oauth/create_account:
post:
summary: create OAuth account
operationId: createAccount
tags:
- oauth
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: code endpoint
required: true
content:
application/json:
schema:
type: object
properties:
refresh_token:
type: string
expires_in:
type: integer
owner:
type: string
client:
type: string
required:
- refresh_token
- expires_in
- owner
- client
responses:
"200":
description: account set
content:
text/plain:
schema:
type: string
/w/{workspace}/oauth/refresh_token/{id}:
post:
summary: refresh token
operationId: refreshToken
tags:
- oauth
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/AccountId"
requestBody:
description: variable path
required: true
content:
application/json:
schema:
type: object
properties:
path:
type: string
required:
- path
responses:
"200":
description: token refreshed
content:
text/plain:
schema:
type: string
/w/{workspace}/oauth/disconnect/{id}:
post:
summary: disconnect account
operationId: disconnectAccount
@@ -2690,7 +2753,7 @@ components:
schema:
type: string
AccountId:
name: account
name: id
in: path
required: true
schema:
@@ -3187,6 +3250,10 @@ components:
type: boolean
description:
type: string
account:
type: string
is_oauth:
type: boolean
extra_perms:
type: object
additionalProperties:
@@ -3222,6 +3289,10 @@ components:
type: boolean
description:
type: string
account:
type: integer
is_oauth:
type: boolean
required:
- path
- value
@@ -3352,6 +3423,8 @@ components:
type: string
resource_type:
type: string
is_oauth:
type: boolean
required:
- path
- value
@@ -3380,6 +3453,8 @@ components:
type: string
value:
type: object
is_oauth:
type: boolean
extra_perms:
type: object
additionalProperties:
@@ -3387,6 +3462,7 @@ components:
required:
- path
- resource_type
- is_oauth
ResourceType:
type: object
@@ -3787,3 +3863,23 @@ components:
- team_id
- team_name
- bot
TokenResponse:
type: object
properties:
access_token:
type: string
expires_in:
type: integer
refresh_token:
type: string
scope:
type: array
items:
type: string
required:
- access_token
- expires_in
- refresh_token
- scope

View File

@@ -555,6 +555,23 @@
]
}
},
"27eb5f99dc9289670673fb999ba9e67abccba212b506c30ba47e6c4fef6d53c4": {
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Text",
"Varchar",
"Bool"
]
},
"nullable": []
}
},
"28c042adef65c3055edc324fbbd2f267285d3566cbec58404983323d410ace27": {
"query": "SELECT super_admin FROM password WHERE email = $1",
"describe": {
@@ -646,6 +663,24 @@
]
}
},
"2e4115bb2e6c8c85ad1492ad135d6b0454b342126cb5fa17e58caf71b32ee755": {
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description, account, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6, $7)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Int4",
"Bool"
]
},
"nullable": []
}
},
"37d3ee8009055e869941e548a6d5a352053a5d7782f662c34b94706488abccb6": {
"query": "UPDATE queue SET running = false WHERE last_ping < $1 RETURNING id",
"describe": {
@@ -1697,22 +1732,6 @@
]
}
},
"8a80333c2fbf7b50fed305882de6e4ffda985d5c648cd617add6c9e6a9c03f34": {
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Text",
"Varchar"
]
},
"nullable": []
}
},
"8ad6a17eecce77f61236e0585ba89b99a32e07ad37b97662007db35acb59d139": {
"query": "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name",
"describe": {
@@ -1937,8 +1956,8 @@
},
{
"ordinal": 6,
"name": "account",
"type_info": "Int4"
"name": "is_oauth",
"type_info": "Bool"
}
],
"parameters": {
@@ -1954,7 +1973,7 @@
true,
false,
false,
true
false
]
}
},
@@ -2278,22 +2297,6 @@
"nullable": []
}
},
"afe9ea97d6e4c45453e21dc3d218fe8927f9faa58921666ceb7014e1f695d900": {
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description)\n VALUES ($1, $2, $3, $4, $5)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar"
]
},
"nullable": []
}
},
"b20977e70ebac7ccbaec5a2a1e940301dd331a5f9a4be67a27cfbff8619ac8f0": {
"query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, true)",
"describe": {
@@ -2395,6 +2398,27 @@
]
}
},
"bb56e61c7cfb09c0a28fb3226dfe91704c70d9fe15eda18e6889adfd7496f80b": {
"query": "DELETE FROM account WHERE workspace_id = $1 AND id = $2 RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Int4"
]
},
"nullable": [
false
]
}
},
"be33c6eb702c149044650d49b3c50493d7538d590be3f4ff6242fea85c57c667": {
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13)",
"describe": {
@@ -2598,6 +2622,21 @@
]
}
},
"cac594031a21b4806de9c4616317d3541522ef9712a83ecff7bd8b5f6e870748": {
"query": "UPDATE account SET refresh_token = $1, expires_at = $2 WHERE workspace_id = $3 AND id = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Timestamptz",
"Text",
"Int4"
]
},
"nullable": []
}
},
"cb12aee5f8e04cb196d4b8fad81699fcbb1ae7b0c84090d5705b14eac76074ff": {
"query": "INSERT INTO group_\n VALUES ($1, 'all', 'The group that always contains all users of this workspace')",
"describe": {
@@ -2610,6 +2649,30 @@
"nullable": []
}
},
"d1513cfa13037cbfe076d184cc698b896add2daba02b8add600f643249b95ad2": {
"query": "INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, $2, $3, $4, $5) RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Timestamptz",
"Varchar"
]
},
"nullable": [
false
]
}
},
"d2dcf69b20488d610599c309862722f805049e479035be6a416d05d73528a8e1": {
"query": "INSERT INTO group_\n (workspace_id, name, summary)\n VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
"describe": {
@@ -3076,8 +3139,8 @@
},
{
"ordinal": 6,
"name": "account",
"type_info": "Int4"
"name": "is_oauth",
"type_info": "Bool"
}
],
"parameters": {
@@ -3092,8 +3155,49 @@
true,
false,
false,
true
false
]
}
},
"fbe14569717b4c4937b74e6dc6f6b4ea29a2b0c2b1cde48ef06d687f6f9e8f15": {
"query": "SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "client",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "refresh_token",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Int4"
]
},
"nullable": [
false,
false
]
}
},
"fcb6ce4fab662602827281e3e85fffaa48542f8d590100d0e7eead2ad4ad4873": {
"query": "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
}
}
}

View File

@@ -8,6 +8,7 @@ use axum::extract::{Extension, FromRequest, Path, Query, RequestParts};
use axum::response::Redirect;
use axum::routing::{get, post};
use axum::{async_trait, Json, Router};
use chrono::{Duration, Utc};
use hyper::StatusCode;
use itertools::Itertools;
@@ -16,16 +17,18 @@ use reqwest::Client;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use slack_http_verifier::SlackVerifier;
use sqlx::{Postgres, Transaction};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use tower_cookies::{Cookie, Cookies};
use crate::audit::{audit_log, ActionKind};
use crate::db::{UserDB, DB};
use crate::error::{self, to_anyhow, Result};
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::workspaces::WorkspaceSettings;
use crate::BaseUrl;
@@ -47,9 +50,12 @@ pub fn global_service() -> Router {
pub fn workspaced_service() -> Router {
Router::new()
.route("/disconnect/:account_id", post(disconnect))
.route("/disconnect/:id", post(disconnect))
.route("/disconnect_slack", post(disconnect_slack))
.route("/set_workspace_slack", post(set_workspace_slack))
.route("/create_account", post(create_account))
.route("/delete_account/:id", post(delete_account))
.route("/refresh_token/:id", post(refresh_token))
}
pub struct ClientWithScopes {
@@ -209,7 +215,7 @@ pub struct SlackTokenResponse {
bot: SlackBotToken,
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TokenResponse {
access_token: AccessToken,
expires_in: Option<u64>,
@@ -244,6 +250,68 @@ async fn connect(
)
}
#[derive(Deserialize)]
struct CreateAccount {
client: String,
owner: String,
refresh_token: String,
expires_in: i64,
}
async fn create_account(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(payload): Json<CreateAccount>,
) -> error::Result<String> {
let mut tx = user_db.begin(&authed).await?;
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",
w_id,
payload.client,
payload.owner,
expires_at,
payload.refresh_token
)
.fetch_one(&mut tx)
.await?;
tx.commit().await?;
Ok(id.to_string())
}
async fn delete_account(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Query((w_id, id)): Query<(String, i32)>,
) -> error::Result<String> {
let mut tx = user_db.begin(&authed).await?;
let exists = sqlx::query!(
"DELETE FROM account WHERE workspace_id = $1 AND id = $2 RETURNING id",
w_id,
id,
)
.fetch_optional(&mut tx)
.await?;
let id_str = id.to_string();
not_found_if_none(exists, "Account", &id_str)?;
audit_log(
&mut tx,
&authed.username,
"account.delete",
ActionKind::Delete,
&w_id,
Some(&id_str),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Deleted account id {id}"))
}
async fn list_logins(
Extension(clients): Extension<Arc<AllClients>>,
) -> error::JsonResult<Vec<String>> {
@@ -333,24 +401,96 @@ async fn login(
oauth_redirect(clients, client_name, cookies, None)
}
#[derive(Deserialize)]
struct VariablePath {
path: String,
}
async fn refresh_token(
authed: Authed,
Path((w_id, id)): Path<(String, i32)>,
Json(VariablePath { path }): Json<VariablePath>,
Extension(user_db): Extension<UserDB>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
) -> error::Result<String> {
let tx = user_db.begin(&authed).await?;
_refresh_token(tx, &path, w_id, id, clients, http_client).await?;
Ok(format!("Token at path {path} refreshed"))
}
pub async fn _refresh_token<'c>(
mut tx: Transaction<'c, Postgres>,
path: &str,
w_id: String,
id: i32,
clients: Arc<AllClients>,
http_client: Client,
) -> error::Result<String> {
let account = sqlx::query!(
"SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2",
w_id,
id,
)
.fetch_optional(&mut tx)
.await?;
let account = not_found_if_none(account, "Account", &id.to_string())?;
let client = (&clients
.connects
.get(&account.client)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?
.client)
.to_owned();
let token = client
.exchange_refresh_token(&RefreshToken::from(account.refresh_token))
.with_client(&http_client)
.execute::<TokenResponse>()
.await
.map_err(to_anyhow)?;
let expires_at = Utc::now()
+ chrono::Duration::seconds(
token
.expires_in
.ok_or_else(|| Error::InternalErr("expires_in exepcted and not found".to_string()))?
.try_into()
.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()),
expires_at,
w_id,
id
)
.execute(&mut tx)
.await?;
let token = token.access_token.to_string();
sqlx::query!(
"UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3",
token,
w_id,
path
)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(token)
}
#[derive(Deserialize)]
pub struct OAuthCallback {
code: String,
state: String,
}
#[derive(Serialize)]
pub struct ConnectResponse {
token: String,
}
async fn connect_callback(
cookies: Cookies,
Path(client_name): Path<String>,
Json(callback): Json<OAuthCallback>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
) -> error::JsonResult<ConnectResponse> {
) -> error::JsonResult<TokenResponse> {
let client = (&clients
.connects
.get(&client_name)
@@ -358,12 +498,10 @@ async fn connect_callback(
.client)
.to_owned();
let token = exchange_code::<TokenResponse>(callback, &cookies, client, &http_client)
.await?
.access_token
.to_string();
let token_response =
exchange_code::<TokenResponse>(callback, &cookies, client, &http_client).await?;
Ok(Json(ConnectResponse { token }))
Ok(Json(token_response))
}
async fn connect_slack_callback(

View File

@@ -67,7 +67,7 @@ pub struct Resource {
pub description: Option<String>,
pub resource_type: String,
pub extra_perms: serde_json::Value,
pub account: Option<i32>,
pub is_oauth: bool,
}
#[derive(Deserialize)]
@@ -76,6 +76,7 @@ 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 {
@@ -105,7 +106,7 @@ async fn list_resources(
"description",
"resource_type",
"extra_perms",
"account",
"is_oauth",
])
.order_by("path", true)
.and_where("workspace_id = ? OR workspace_id = 'starter'".bind(&w_id))
@@ -180,13 +181,14 @@ async fn create_resource(
sqlx::query!(
"INSERT INTO resource
(workspace_id, path, value, description, resource_type)
VALUES ($1, $2, $3, $4, $5)",
(workspace_id, path, value, description, resource_type, is_oauth)
VALUES ($1, $2, $3, $4, $5, $6)",
w_id,
resource.path,
resource.value,
resource.description,
resource.resource_type,
resource.is_oauth
)
.execute(&mut tx)
.await?;

View File

@@ -698,7 +698,7 @@ async fn decline_invite(
&mut tx,
&email,
"users.decline_invite",
ActionKind::Create,
ActionKind::Delete,
&nu.workspace_id,
Some(&email),
None,

View File

@@ -5,10 +5,13 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::sync::Arc;
use crate::{
audit::{audit_log, ActionKind},
db::{UserDB, DB},
error::{Error, JsonResult, Result},
oauth2::{AllClients, _refresh_token},
users::Authed,
utils::StripPath,
};
@@ -20,6 +23,7 @@ use axum::{
use hyper::StatusCode;
use magic_crypt::{MagicCrypt256, MagicCryptTrait};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Postgres, Transaction};
@@ -51,6 +55,8 @@ pub struct ListableVariable {
pub description: String,
pub extra_perms: serde_json::Value,
pub account: Option<i32>,
pub is_oauth: bool,
pub is_expired: Option<bool>,
}
#[derive(Deserialize)]
@@ -59,6 +65,8 @@ pub struct CreateVariable {
pub value: String,
pub is_secret: bool,
pub description: String,
pub account: Option<i32>,
pub is_oauth: Option<bool>,
}
#[derive(Deserialize)]
@@ -152,7 +160,7 @@ 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 from variable
"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)
@@ -173,12 +181,17 @@ async fn get_variable(
Extension(user_db): Extension<UserDB>,
Query(q): Query<GetVariableQuery>,
Path((w_id, path)): Path<(String, StripPath)>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
) -> JsonResult<ListableVariable> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let variable_o = sqlx::query_as::<_, ListableVariable>(
"SELECT * from variable WHERE path = $1 AND (workspace_id = $2 OR (is_secret IS NOT TRUE AND workspace_id = 'starter'))",
"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'))
LIMIT 1",
)
.bind(&path)
.bind(&w_id)
@@ -202,8 +215,22 @@ async fn get_variable(
.await?;
let value = variable.value.unwrap_or_else(|| "".to_string());
ListableVariable {
value: if !value.is_empty() && decrypt_secret {
value: if variable.is_expired.unwrap_or(false) && variable.account.is_some() {
Some(
_refresh_token(
tx,
&variable.path,
w_id,
variable.account.unwrap(),
clients,
http_client,
)
.await?,
)
} else if !value.is_empty() && decrypt_secret {
let mc = build_crypt(&mut tx, &w_id).await?;
tx.commit().await?;
Some(
mc.decrypt_base64_to_string(value)
.map_err(|e| Error::InternalErr(e.to_string()))?,
@@ -216,7 +243,6 @@ async fn get_variable(
} else {
variable
};
tx.commit().await?;
Ok(Json(r))
}
@@ -238,13 +264,15 @@ async fn create_variable(
sqlx::query!(
"INSERT INTO variable
(workspace_id, path, value, is_secret, description)
VALUES ($1, $2, $3, $4, $5)",
(workspace_id, path, value, is_secret, description, account, is_oauth)
VALUES ($1, $2, $3, $4, $5, $6, $7)",
&w_id,
variable.path,
value,
variable.is_secret,
variable.description
variable.description,
variable.account,
variable.is_oauth.unwrap_or(false),
)
.execute(&mut tx)
.await?;

View File

@@ -13,18 +13,18 @@
import { workspaceStore, userStore, oauthStore } from '$lib/stores'
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
import { OauthService, ResourceService, VariableService } from '$lib/gen'
import { OauthService, ResourceService, VariableService, type TokenResponse } from '$lib/gen'
import { createEventDispatcher, onMount } from 'svelte'
import Modal from './Modal.svelte'
import Icon from 'svelte-awesome'
import Path from './Path.svelte'
import Password from './Password.svelte'
import { sendUserToast, truncate, truncateRev } from '$lib/utils'
import { goto } from '$app/navigation'
import { sendUserToast, truncateRev } from '$lib/utils'
let manual = false
let value = ''
let value: string = ''
let valueToken: TokenResponse
let connects: Record<string, string[]> = {}
let connectsManual: [string, { img?: string; instructions: string }][] = []
@@ -46,7 +46,8 @@
export function openFromOauth(rt: string) {
resource_type = rt
value = $oauthStore!
value = $oauthStore?.access_token!
valueToken = $oauthStore!
$oauthStore = undefined
manual = false
step = 3
@@ -95,13 +96,30 @@
if (exists) {
throw Error(`Resource at path ${path} already exists. Delete it or pick another path`)
}
let account: number | undefined = undefined
if (valueToken.refresh_token != undefined && valueToken.expires_in != undefined) {
account = Number(
await OauthService.createAccount({
workspace: $workspaceStore!,
requestBody: {
refresh_token: valueToken.refresh_token!,
expires_in: valueToken.expires_in!,
owner: path.split('/').slice(0, 2).join('/'),
client: resource_type
}
})
)
}
await VariableService.createVariable({
workspace: $workspaceStore!,
requestBody: {
path,
value,
is_secret: true,
description: `OAuth token for ${resource_type}`
description: `OAuth token for ${resource_type}`,
is_oauth: true,
account: account
}
})
await ResourceService.createResource({
@@ -110,7 +128,8 @@
resource_type,
path,
value: { token: `$var:${path}` },
description: `OAuth token for ${resource_type}`
description: `OAuth token for ${resource_type}`,
is_oauth: true
}
})
dispatch('refresh')
@@ -147,7 +166,7 @@
<div slot="title">Connect an app</div>
<div slot="content">
{#if step == 1}
<PageHeader title="Oauth apps" />
<PageHeader title="OAuth apps" />
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
{#each Object.entries(connects) as [key, values]}
<button

View File

@@ -2,6 +2,7 @@ import { browser } from '$app/env'
import { writable } from 'svelte/store'
import type { UserWorkspaceList } from '$lib/gen/models/UserWorkspaceList.js'
import { getUserExt } from './user'
import type { TokenResponse } from './gen'
export interface UserExt {
email: string
@@ -14,7 +15,7 @@ export interface UserExt {
let persistedWorkspace = browser && localStorage.getItem('workspace')
export const oauthStore = writable<string | undefined>(undefined)
export const oauthStore = writable<TokenResponse | undefined>(undefined)
export const userStore = writable<UserExt | undefined>(undefined)
export const workspaceStore = writable<string | undefined>(
persistedWorkspace ? String(persistedWorkspace) : undefined

View File

@@ -25,7 +25,7 @@
clientName: client_name,
requestBody: { code, state }
})
$oauthStore = res.token
$oauthStore = res
goto(`/resources?resource_type=${client_name}`)
} catch (e) {
sendUserToast(`Error parsing the response token, ${e.body}`, true)

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { canWrite, emptySchema, sendUserToast } from '$lib/utils'
import { ResourceService } from '$lib/gen'
import { ResourceService, VariableService } from '$lib/gen'
import type { Resource, ResourceType } from '$lib/gen'
import PageHeader from '$lib/components/PageHeader.svelte'
import ResourceEditor from '$lib/components/ResourceEditor.svelte'
@@ -19,7 +19,7 @@
import type { Schema } from '$lib/common'
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
import Dropdown from '$lib/components/Dropdown.svelte'
import { faEdit, faPlus, faShare, faTrash } from '@fortawesome/free-solid-svg-icons'
import { faEdit, faPlus, faShare, faTrash, faCircle } from '@fortawesome/free-solid-svg-icons'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
import Required from '$lib/components/Required.svelte'
@@ -69,7 +69,10 @@
)
}
async function deleteResource(path: string): Promise<void> {
async function deleteResource(path: string, is_oauth: boolean): Promise<void> {
if (is_oauth) {
await VariableService.deleteVariable({ workspace: $workspaceStore!, path })
}
await ResourceService.deleteResource({ workspace: $workspaceStore!, path })
loadResources()
}
@@ -146,11 +149,12 @@
<th>path</th>
<th>resource_type</th>
<th>description</th>
<th>OAuth</th>
<th />
</tr>
<tbody slot="body">
{#if resources}
{#each resources as { path, description, resource_type, extra_perms, canWrite }}
{#each resources as { path, description, resource_type, extra_perms, canWrite, is_oauth }}
<tr>
<td class="my-12"
><a
@@ -171,6 +175,16 @@
</td>
<td><IconedResourceType name={resource_type} /></td>
<td><SvelteMarkdown source={description ?? ''} /></td>
<td>
{#if is_oauth}
<Icon
class="text-green-600"
data={faCircle}
scale={0.7}
label="Resource is tied to an OAuth app"
/>
{/if}
</td>
<td>
<Dropdown
dropdownItems={[
@@ -196,7 +210,7 @@
icon: faTrash,
type: 'delete',
action: () => {
deleteResource(path)
deleteResource(path, is_oauth)
}
}
]}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte'
import { onDestroy } from 'svelte'
import { JobService, Job, CompletedJob } from '$lib/gen'
import { displayDate, displayDaysAgo, forLater, truncateHash } from '$lib/utils'
import Icon from 'svelte-awesome'

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { canWrite, sendUserToast } from '$lib/utils'
import { VariableService } from '$lib/gen'
import { OauthService, VariableService } from '$lib/gen'
import type { ListableVariable, ContextualVariable } from '$lib/gen'
import Dropdown from '$lib/components/Dropdown.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
@@ -12,7 +12,7 @@
import { userStore, workspaceStore } from '$lib/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { faPlus, faCircle } from '@fortawesome/free-solid-svg-icons'
type ListableVariableW = ListableVariable & { canWrite: boolean }
@@ -37,7 +37,10 @@
})
}
async function deleteVariable(path: string): Promise<void> {
async function deleteVariable(path: string, account?: string): Promise<void> {
if (account) {
OauthService.disconnectAccount({ workspace: $workspaceStore!, id: account })
}
await VariableService.deleteVariable({ workspace: $workspaceStore!, path })
loadVariables()
sendUserToast(`Variable ${path} was deleted`)
@@ -71,10 +74,11 @@
<th>value</th>
<th>secret</th>
<th>description</th>
<th>OAuth</th>
<th />
</tr>
<tbody slot="body">
{#each variables as { path, value, is_secret, description, extra_perms, canWrite }}
{#each variables as { path, value, is_secret, description, extra_perms, canWrite, account, is_oauth }}
<tr>
<td
><a
@@ -87,6 +91,16 @@
<td>{value ?? '******'}</td>
<td>{is_secret ? 'secret' : 'visible'}</td>
<td>{description}</td>
<td>
{#if is_oauth}
<Icon
class="text-green-600"
data={faCircle}
scale={0.7}
label="Variable is tied to an OAuth app"
/>
{/if}
</td>
<td
><Dropdown
dropdownItems={[
@@ -97,7 +111,7 @@
},
{
displayName: 'Delete',
action: () => deleteVariable(path),
action: () => deleteVariable(path, account),
disabled: !canWrite
},
{
@@ -106,7 +120,24 @@
shareModal.openModal(path)
},
disabled: !canWrite
}
},
...(account != undefined
? [
{
displayName: 'Refresh token',
action: async () => {
await OauthService.refreshToken({
workspace: $workspaceStore ?? '',
id: account,
requestBody: {
path
}
})
sendUserToast('Token refreshed')
}
}
]
: [])
]}
relative={false}
/></td