more visibility on OAuth errors Vol 1

This commit is contained in:
Ruben Fiszel
2022-11-27 14:00:55 +01:00
parent 02a9c5c4ea
commit 346a7f3f47
12 changed files with 270 additions and 71 deletions
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE account ADD COLUMN refresh_error TEXT;
+14
View File
@@ -146,6 +146,20 @@
},
"query": "DELETE FROM variable WHERE path = $1 AND workspace_id = $2"
},
"09e2a19435068f9e9bfd5bcb44b4e283c71729f81550f6f7156ce4970345cc07": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text",
"Int4"
]
}
},
"query": "UPDATE account SET refresh_error = $1 WHERE workspace_id = $2 AND id = $3"
},
"0a7212dd507ed8f7a311724185e39ecc1809abb208a681ad711614c27baadd83": {
"describe": {
"columns": [
+4
View File
@@ -4128,6 +4128,10 @@ components:
type: object
additionalProperties:
type: boolean
is_expired:
type: boolean
refresh_error:
type: string
required:
- workspace_id
- path
+37 -11
View File
@@ -509,18 +509,25 @@ pub async fn _refresh_token<'c>(
.client)
.to_owned();
let token_json = client
.exchange_refresh_token(&RefreshToken::from(account.refresh_token.clone()))
.with_client(&http_client)
.execute::<serde_json::Value>()
.await
.map_err(to_anyhow)?;
let token = _exchange_token(client, &account.refresh_token, http_client).await;
let token = serde_json::from_value::<TokenResponse>(token_json.clone()).map_err(|e| {
Error::BadConfig(format!(
"Error deserializing response as a new token: {e}\nresponse:{token_json}"
))
})?;
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(),
w_id,
id,
)
.execute(&mut tx)
.await?;
tx.commit().await?;
return Err(error::Error::BadRequest(format!(
"Error refreshing token: {:#?}",
token_err
)));
};
let token = token.unwrap();
let expires_at = now_from_db(&mut tx).await?
+ chrono::Duration::seconds(
@@ -560,6 +567,25 @@ pub async fn _refresh_token<'c>(
Ok(token_str)
}
async fn _exchange_token(
client: OClient,
refresh_token: &str,
http_client: Client,
) -> Result<TokenResponse, Error> {
let token_json = client
.exchange_refresh_token(&RefreshToken::from(refresh_token.clone()))
.with_client(&http_client)
.execute::<serde_json::Value>()
.await
.map_err(to_anyhow)?;
let token = serde_json::from_value::<TokenResponse>(token_json.clone()).map_err(|e| {
Error::BadConfig(format!(
"Error deserializing response as a new token: {e}\nresponse:{token_json}"
))
})?;
Ok(token)
}
#[derive(Deserialize)]
pub struct OAuthCallback {
code: String,
+6 -6
View File
@@ -82,11 +82,11 @@ async fn list_variables(
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, ListableVariable>(
"SELECT workspace_id, path, CASE WHEN is_secret IS TRUE THEN null ELSE value::text END as \
value, is_secret, description, extra_perms, account, is_oauth, false as is_expired from \
variable
WHERE (workspace_id = $1 OR (is_secret IS NOT TRUE AND workspace_id = 'starter')) ORDER \
BY path",
"SELECT 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
LEFT JOIN account ON variable.account = account.id AND account.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)
.fetch_all(&mut tx)
@@ -113,7 +113,7 @@ 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 from variable
"SELECT variable.*, (now() > account.expires_at) as is_expired, account.refresh_error 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'))
+1
View File
@@ -29,6 +29,7 @@ pub struct ListableVariable {
pub account: Option<i32>,
pub is_oauth: bool,
pub is_expired: Option<bool>,
pub refresh_error: Option<String>,
}
#[derive(Deserialize)]
+122 -11
View File
@@ -21,6 +21,7 @@
"monaco-editor": "^0.34.1",
"monaco-editor-workers": "^0.34.2",
"monaco-languageclient": "4.0.1",
"monaco-yaml": "^4.0.2",
"set-interval-async": "^3.0.2",
"svelte-autosize": "^1.0.1",
"svelte-chartjs": "^3.1.0",
@@ -546,8 +547,7 @@
"node_modules/@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
},
"node_modules/@types/marked": {
"version": "4.0.3",
@@ -3626,6 +3626,11 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
"node_modules/jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@@ -4006,6 +4011,56 @@
"npm": ">=8.0.0"
}
},
"node_modules/monaco-marker-data-provider": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.1.1.tgz",
"integrity": "sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==",
"funding": {
"url": "https://github.com/sponsors/remcohaszing"
},
"peerDependencies": {
"monaco-editor": ">=0.30.0"
}
},
"node_modules/monaco-worker-manager": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz",
"integrity": "sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==",
"peerDependencies": {
"monaco-editor": ">=0.30.0"
}
},
"node_modules/monaco-yaml": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-4.0.2.tgz",
"integrity": "sha512-Wxn6CblkQDLOUusfi0eZ3qZhkuKYIrK7fXlkJOOG+W18zgKePbuZW0XNWpczlxDC27D753dB18pMnx4U7MZ3yg==",
"dependencies": {
"@types/json-schema": "^7.0.0",
"jsonc-parser": "^3.0.0",
"monaco-marker-data-provider": "^1.0.0",
"monaco-worker-manager": "^2.0.0",
"path-browserify": "^1.0.0",
"prettier": "^2.0.0",
"vscode-languageserver-textdocument": "^1.0.0",
"vscode-languageserver-types": "^3.0.0",
"vscode-uri": "^3.0.0",
"yaml": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/remcohaszing"
},
"peerDependencies": {
"monaco-editor": ">=0.30"
}
},
"node_modules/monaco-yaml/node_modules/yaml": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
"engines": {
"node": ">= 14"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -4301,8 +4356,7 @@
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"dev": true
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
},
"node_modules/path-exists": {
"version": "4.0.0",
@@ -4984,7 +5038,6 @@
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
@@ -6678,6 +6731,11 @@
"vscode-languageserver-types": "3.17.2"
}
},
"node_modules/vscode-languageserver-textdocument": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.7.tgz",
"integrity": "sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg=="
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
@@ -6695,6 +6753,11 @@
"integrity": "sha512-OkE/mYm1h5ZX9IEKeKR/2zKDt2SzYyIfTEOVFX4QhA+B3BPROvNEmDDXvBThz3qknKO3Cy/VVb8/sx1UlqP/Xw==",
"peer": true
},
"node_modules/vscode-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz",
"integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ=="
},
"node_modules/vscode-ws-jsonrpc": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-2.0.0.tgz",
@@ -7308,8 +7371,7 @@
"@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
},
"@types/marked": {
"version": "4.0.3",
@@ -9453,6 +9515,11 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
"jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@@ -9747,6 +9814,42 @@
"vscode-languageclient": "8.0.2"
}
},
"monaco-marker-data-provider": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.1.1.tgz",
"integrity": "sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==",
"requires": {}
},
"monaco-worker-manager": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz",
"integrity": "sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==",
"requires": {}
},
"monaco-yaml": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-4.0.2.tgz",
"integrity": "sha512-Wxn6CblkQDLOUusfi0eZ3qZhkuKYIrK7fXlkJOOG+W18zgKePbuZW0XNWpczlxDC27D753dB18pMnx4U7MZ3yg==",
"requires": {
"@types/json-schema": "^7.0.0",
"jsonc-parser": "^3.0.0",
"monaco-marker-data-provider": "^1.0.0",
"monaco-worker-manager": "^2.0.0",
"path-browserify": "^1.0.0",
"prettier": "^2.0.0",
"vscode-languageserver-textdocument": "^1.0.0",
"vscode-languageserver-types": "^3.0.0",
"vscode-uri": "^3.0.0",
"yaml": "^2.0.0"
},
"dependencies": {
"yaml": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg=="
}
}
},
"mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -9967,8 +10070,7 @@
"path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"dev": true
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
},
"path-exists": {
"version": "4.0.0",
@@ -10394,8 +10496,7 @@
"prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g=="
},
"prettier-plugin-svelte": {
"version": "2.8.0",
@@ -11625,6 +11726,11 @@
"vscode-languageserver-types": "3.17.2"
}
},
"vscode-languageserver-textdocument": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.7.tgz",
"integrity": "sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg=="
},
"vscode-languageserver-types": {
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
@@ -11642,6 +11748,11 @@
"integrity": "sha512-OkE/mYm1h5ZX9IEKeKR/2zKDt2SzYyIfTEOVFX4QhA+B3BPROvNEmDDXvBThz3qknKO3Cy/VVb8/sx1UlqP/Xw==",
"peer": true
},
"vscode-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.6.tgz",
"integrity": "sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ=="
},
"vscode-ws-jsonrpc": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-2.0.0.tgz",
+1
View File
@@ -71,6 +71,7 @@
"monaco-editor": "^0.34.1",
"monaco-editor-workers": "^0.34.2",
"monaco-languageclient": "4.0.1",
"monaco-yaml": "^4.0.2",
"set-interval-async": "^3.0.2",
"svelte-autosize": "^1.0.1",
"svelte-chartjs": "^3.1.0",
@@ -20,6 +20,7 @@
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import yamlWorker from 'monaco-yaml/yaml.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import { buildWorkerDefinition } from 'monaco-editor-workers'
@@ -58,6 +59,8 @@
getWorker: function (_moduleId: any, label: string) {
if (label === 'json') {
return new jsonWorker()
} else if (label === 'yaml') {
return new yamlWorker()
} else if (label === 'typescript' || label === 'javascript') {
return new tsWorker()
} else {
@@ -6,12 +6,14 @@
import { workspaceStore } from '$lib/stores'
import Required from './Required.svelte'
import Tooltip from './Tooltip.svelte'
import { Button } from './common'
import { Button, ToggleButton, ToggleButtonGroup } from './common'
import Drawer from './common/drawer/Drawer.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import Alert from './common/alert/Alert.svelte'
import autosize from 'svelte-autosize'
import Toggle from './Toggle.svelte'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import SimpleEditor from './SimpleEditor.svelte'
const dispatch = createEventDispatcher()
@@ -114,9 +116,10 @@
sendUserToast(`Could not update variable: ${err.body}`, true)
}
}
let editorKind: 'plain' | 'json' | 'yaml' = 'plain'
</script>
<Drawer bind:this={drawer}>
<Drawer bind:this={drawer} size="900px">
<DrawerContent
title={edit ? `Update variable at ${initialPath}` : 'Add a variable'}
on:close={drawer.closeDrawer}
@@ -136,28 +139,15 @@
<div class="mt-4">
<Toggle bind:checked={variable.is_secret} options={{ right: 'Secret' }} />
{#if variable.is_secret}
<Alert type="warning" title="Not visible after this">
If the variable is a secret, you will not be able to read the value of it from the
variable editor UI but only within scripts.
<Tooltip>
Within scripts, every read of the value create the audit log:
'variables.decrypt_secret'
</Tooltip>
<Alert type="warning" title="Audit log for each access">
Every secret is encrypted at rest and in transit with a key specific to this
workspace. In addition, any read of a secret variable generates an audit log whose
operation name is: variables.decrypt_secret
</Alert>
{/if}
</div>
</div>
<!--
{#if variable.is_secret}
<div class="mb-1 col-span-10">
<Password
bind:password={variable.value}
placeholder={'******** (only fill to update value)'}
label={`<span class="font-semibold text-gray-700">Secret value</span>
<span class="text-sm text-gray-500">(${variable.value.length}/3000 characters)</span>`}
/>
</div>
{:else} -->
<div>
<div class="mb-1">
<span class="font-semibold text-gray-700">Variable value</span>
@@ -165,17 +155,34 @@
{#if edit && variable.is_secret}<Button
variant="border"
size="xs"
on:click={() => loadVariable(initialPath)}
>Load secret value (generate audit log)</Button
on:click={() => loadVariable(initialPath)}>Load secret value</Button
>{/if}
</div>
<textarea
rows="4"
type="text"
use:autosize
bind:value={variable.value}
placeholder="Update variable value"
/>
<div class="flex flex-row">
{#if editorKind == 'plain'}
<textarea
rows="4"
type="text"
use:autosize
bind:value={variable.value}
placeholder="Update variable value"
/>
{:else if editorKind == 'json'}
<div class="border rounded mb-4 w-full border-gray-700">
<SimpleEditor autoHeight lang="json" bind:code={variable.value} />
</div>
{:else if editorKind == 'yaml'}
<div class="border rounded mb-4 w-full border-gray-700">
<SimpleEditor autoHeight lang="yaml" bind:code={variable.value} />
</div>
{/if}
<ToggleButtonGroup col bind:selected={editorKind}>
<ToggleButton light position="center" value="plain" size="xs">Plain</ToggleButton>
<ToggleButton light position="center" value="json" size="xs">Json</ToggleButton>
<ToggleButton light position="center" value="yaml" size="xs">YAML</ToggleButton>
</ToggleButtonGroup>
</div>
</div>
<!-- {/if} -->
@@ -197,8 +204,10 @@
<Button
on:click={() => (edit ? updateVariable() : createVariable())}
disabled={!valid || pathError != ''}
btnClasses="mr-2"
startIcon={{ icon: faSave }}
>
{edit ? 'Save' : 'Add'}
{edit ? 'Update' : 'Save'}
</Button>
</div>
</DrawerContent>
+40 -13
View File
@@ -102,7 +102,7 @@
<th />
</tr>
<tbody slot="body">
{#each variables as { path, value, is_secret, description, extra_perms, canWrite, account, is_oauth }}
{#each variables as { path, value, is_secret, description, extra_perms, canWrite, account, is_oauth, is_expired, refresh_error }}
<tr>
<td
><a
@@ -137,18 +137,45 @@
<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 variable is tied to an OAuth app. The token is refreshed automatically if
applicable.
</div>
</Popover>
{#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>
{/if}
{/if}
</td>
<td