mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
feat: Workspace encryption key can be manually updated (#3223)
* feat: Workspace encryption key can be manually updated * sqlx prepare
This commit is contained in:
committed by
GitHub
parent
28192ec014
commit
e8ed4783b2
-1
@@ -48,7 +48,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
-1
@@ -69,7 +69,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
-1
@@ -28,7 +28,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
-1
@@ -42,7 +42,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT key FROM workspace_key WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "key",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3a637063e1d256639b0e900606b641ab71cc1b5049a104e0699e81484a61ae63"
|
||||
}
|
||||
-1
@@ -62,7 +62,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
-1
@@ -37,7 +37,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_key SET key = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7ee6056b7fb40b312333b489788337574398d63a6ab85171e43c3ab76400ec22"
|
||||
}
|
||||
-1
@@ -42,7 +42,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path, value, is_secret FROM variable WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "value",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_secret",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b656f38e5f1d6a0799767a775b8550027cf7a3bb202269e8dda92d31bc8789dd"
|
||||
}
|
||||
-1
@@ -42,7 +42,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
-1
@@ -42,7 +42,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
version: 1.268.0
|
||||
version: 1.269.0
|
||||
title: Windmill API
|
||||
contact:
|
||||
name: Windmill Team
|
||||
@@ -1961,6 +1961,58 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/w/{workspace}/workspaces/encryption_key:
|
||||
get:
|
||||
summary: retrieves the encryption key for this workspace
|
||||
operationId: getWorkspaceEncryptionKey
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
responses:
|
||||
'200':
|
||||
description: status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
post:
|
||||
summary: update the encryption key for this workspace
|
||||
operationId: setWorkspaceEncryptionKey
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
requestBody:
|
||||
description: New encryption key
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
new_key:
|
||||
type: string
|
||||
required:
|
||||
- new_key
|
||||
responses:
|
||||
'200':
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/w/{workspace}/workspaces/default_app:
|
||||
get:
|
||||
summary: get default app for workspace
|
||||
@@ -7109,6 +7161,13 @@ paths:
|
||||
in: query
|
||||
schema: &ref_64
|
||||
type: string
|
||||
- name: all_workspaces
|
||||
description: >-
|
||||
get jobs from all workspaces (only valid if request come from the
|
||||
`admins` workspace)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: All queued jobs
|
||||
@@ -7334,6 +7393,13 @@ paths:
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
- name: all_workspaces
|
||||
description: >-
|
||||
get jobs from all workspaces (only valid if request come from the
|
||||
`admins` workspace)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: queue count
|
||||
@@ -7706,6 +7772,13 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: all_workspaces
|
||||
description: >-
|
||||
get jobs from all workspaces (only valid if request come from the
|
||||
`admins` workspace)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: All jobs
|
||||
|
||||
@@ -1574,6 +1574,54 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/encryption_key:
|
||||
get:
|
||||
summary: retrieves the encryption key for this workspace
|
||||
operationId: getWorkspaceEncryptionKey
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
post:
|
||||
summary: update the encryption key for this workspace
|
||||
operationId: setWorkspaceEncryptionKey
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: New encryption key
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
new_key:
|
||||
type: string
|
||||
required:
|
||||
- new_key
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/default_app:
|
||||
get:
|
||||
summary: get default app for workspace
|
||||
|
||||
@@ -12,7 +12,6 @@ use axum::{
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use magic_crypt::MagicCryptTrait;
|
||||
use quick_cache::sync::Cache;
|
||||
use serde_json::value::RawValue;
|
||||
use windmill_audit::audit_ee::audit_log;
|
||||
@@ -22,6 +21,7 @@ use windmill_common::{
|
||||
variables::build_crypt,
|
||||
};
|
||||
|
||||
use crate::variables::decrypt;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -73,9 +73,7 @@ async fn get_variable_or_self(path: String, db: &DB, w_id: &String) -> Result<St
|
||||
.await?;
|
||||
if variable.is_secret {
|
||||
let mc = build_crypt(&mut tx, &w_id).await?;
|
||||
variable.value = mc
|
||||
.decrypt_base64_to_string(variable.value)
|
||||
.map_err(|e| Error::InternalErr(e.to_string()))?;
|
||||
variable.value = decrypt(&mc, variable.value)?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(variable.value)
|
||||
|
||||
@@ -166,10 +166,7 @@ async fn get_variable(
|
||||
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()))?,
|
||||
)
|
||||
Some(decrypt(&mc, value)?)
|
||||
} else if q.include_encrypted.unwrap_or(false) {
|
||||
Some(value)
|
||||
} else {
|
||||
@@ -637,9 +634,7 @@ pub async fn get_value_internal<'c>(
|
||||
} else if !value.is_empty() {
|
||||
let mc = build_crypt(&mut tx, &w_id).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
mc.decrypt_base64_to_string(value)
|
||||
.map_err(|e| Error::InternalErr(e.to_string()))?
|
||||
decrypt(&mc, value)?
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
@@ -653,3 +648,8 @@ pub async fn get_value_internal<'c>(
|
||||
pub fn encrypt(mc: &MagicCrypt256, value: &str) -> String {
|
||||
mc.encrypt_str_to_base64(value)
|
||||
}
|
||||
|
||||
pub fn decrypt(mc: &MagicCrypt256, value: String) -> Result<String> {
|
||||
mc.decrypt_base64_to_string(value)
|
||||
.map_err(|e| Error::InternalErr(e.to_string()))
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ use axum::{
|
||||
use chrono::Utc;
|
||||
#[cfg(feature = "stripe")]
|
||||
use chrono::{Datelike, TimeZone, Timelike};
|
||||
use magic_crypt::MagicCryptTrait;
|
||||
use regex::Regex;
|
||||
#[cfg(feature = "stripe")]
|
||||
use stripe::CustomerId;
|
||||
@@ -57,6 +56,7 @@ use windmill_common::{
|
||||
};
|
||||
use windmill_queue::QueueTransaction;
|
||||
|
||||
use crate::variables::{decrypt, encrypt};
|
||||
use hyper::{header, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Map};
|
||||
@@ -64,6 +64,11 @@ use sqlx::{FromRow, Postgres, Transaction};
|
||||
use tempfile::TempDir;
|
||||
use tokio::fs::File;
|
||||
use tokio_util::io::ReaderStream;
|
||||
use windmill_common::utils::not_found_if_none;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref WORKSPACE_KEY_REGEXP: Regex = Regex::new("^[a-zA-Z0-9]{64}$").unwrap();
|
||||
}
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
let router = Router::new()
|
||||
@@ -95,6 +100,10 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/edit_git_sync_config", post(edit_git_sync_config))
|
||||
.route("/edit_default_app", post(edit_default_app))
|
||||
.route("/default_app", get(get_default_app))
|
||||
.route(
|
||||
"/encryption_key",
|
||||
get(get_encryption_key).post(set_encryption_key),
|
||||
)
|
||||
.route("/leave", post(leave_workspace));
|
||||
|
||||
#[cfg(feature = "stripe")]
|
||||
@@ -1334,6 +1343,95 @@ async fn edit_error_handler(
|
||||
Ok(format!("Edit error_handler for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetEncryptionKeyResponse {
|
||||
key: String,
|
||||
}
|
||||
|
||||
async fn get_encryption_key(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<GetEncryptionKeyResponse> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
let encryption_key_opt = sqlx::query_scalar!(
|
||||
"SELECT key FROM workspace_key WHERE workspace_id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
let encryption_key = not_found_if_none(encryption_key_opt, "workspace_encryption_key", w_id)?;
|
||||
return Ok(Json(GetEncryptionKeyResponse { key: encryption_key }));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SetEncryptionKeyRequest {
|
||||
new_key: String,
|
||||
}
|
||||
|
||||
async fn set_encryption_key(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(request): Json<SetEncryptionKeyRequest>,
|
||||
) -> Result<()> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
if !WORKSPACE_KEY_REGEXP.is_match(request.new_key.as_str()) {
|
||||
return Err(Error::BadRequest(
|
||||
"Encryption key should be an alphanumeric string of 64 characters".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
let previous_encryption_key = build_crypt(&mut tx, w_id.as_str()).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_key SET key = $1 WHERE workspace_id = $2",
|
||||
request.new_key.clone(),
|
||||
w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
let new_encryption_key = build_crypt(&mut tx, w_id.as_str()).await?;
|
||||
|
||||
let mut truncated_new_key = request.new_key.clone();
|
||||
truncated_new_key.truncate(8);
|
||||
tracing::warn!(
|
||||
"Re-encrypting all secrets for workspace {}. New key is {}***",
|
||||
w_id,
|
||||
truncated_new_key
|
||||
);
|
||||
|
||||
let all_variables = sqlx::query!(
|
||||
"SELECT path, value, is_secret FROM variable WHERE workspace_id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for variable in all_variables {
|
||||
if !variable.is_secret {
|
||||
continue;
|
||||
}
|
||||
let decrypted_value = decrypt(&previous_encryption_key, variable.value)?;
|
||||
let new_encrypted_value = encrypt(&new_encryption_key, decrypted_value.as_str());
|
||||
sqlx::query!(
|
||||
"UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3",
|
||||
new_encrypted_value,
|
||||
w_id,
|
||||
variable.path
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
async fn list_workspaces_as_super_admin(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -2321,10 +2419,7 @@ async fn tarball_workspace(
|
||||
&& var.value.is_some()
|
||||
&& var.is_secret
|
||||
{
|
||||
var.value = Some(
|
||||
mc.decrypt_base64_to_string(var.value.unwrap())
|
||||
.map_err(|e| Error::InternalErr(e.to_string()))?,
|
||||
);
|
||||
var.value = Some(decrypt(&mc, var.value.unwrap())?);
|
||||
}
|
||||
let var_str = &to_string_without_metadata(&var, false).unwrap();
|
||||
archive
|
||||
|
||||
@@ -33,7 +33,17 @@
|
||||
} from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { setQueryWithoutLoad, emptyString, tryEvery } from '$lib/utils'
|
||||
import { Scroll, Slack, XCircle, RotateCw, CheckCircle2, X, Plus, Loader2 } from 'lucide-svelte'
|
||||
import {
|
||||
Scroll,
|
||||
Slack,
|
||||
XCircle,
|
||||
RotateCw,
|
||||
CheckCircle2,
|
||||
X,
|
||||
Plus,
|
||||
Loader2,
|
||||
Save
|
||||
} from 'lucide-svelte'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
|
||||
import PremiumInfo from '$lib/components/settings/PremiumInfo.svelte'
|
||||
@@ -101,6 +111,10 @@
|
||||
status: 'running' | 'success' | 'failure' | undefined
|
||||
}[]
|
||||
let workspaceDefaultAppPath: string | undefined = undefined
|
||||
let workspaceEncryptionKey: string | undefined = undefined
|
||||
let editedWorkspaceEncryptionKey: string | undefined = undefined
|
||||
let workspaceReencryptionInProgress: boolean = false
|
||||
let encryptionKeyRegex = /^[a-zA-Z0-9]{64}$/
|
||||
let codeCompletionEnabled: boolean = false
|
||||
let tab =
|
||||
($page.url.searchParams.get('tab') as
|
||||
@@ -338,6 +352,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkspaceEncryptionKey(): Promise<void> {
|
||||
let resp = await WorkspaceService.getWorkspaceEncryptionKey({
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
workspaceEncryptionKey = resp.key
|
||||
editedWorkspaceEncryptionKey = resp.key
|
||||
}
|
||||
|
||||
async function setWorkspaceEncryptionKey(): Promise<void> {
|
||||
if (
|
||||
emptyString(editedWorkspaceEncryptionKey) ||
|
||||
workspaceEncryptionKey === editedWorkspaceEncryptionKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
const timeStart = new Date().getTime()
|
||||
workspaceReencryptionInProgress = true
|
||||
await WorkspaceService.setWorkspaceEncryptionKey({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
new_key: editedWorkspaceEncryptionKey ?? '' // cannot be undefined at this point
|
||||
}
|
||||
})
|
||||
await loadWorkspaceEncryptionKey()
|
||||
const timeEnd = new Date().getTime()
|
||||
sendUserToast('All workspace secrets have been re-encrypted with the new key')
|
||||
setTimeout(() => {
|
||||
workspaceReencryptionInProgress = false
|
||||
}, 1000 - (timeEnd - timeStart))
|
||||
}
|
||||
|
||||
async function loadSettings(): Promise<void> {
|
||||
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
||||
team_name = settings.slack_name
|
||||
@@ -601,6 +646,9 @@
|
||||
<Tab size="xs" value="default_app">
|
||||
<div class="flex gap-2 items-center my-1"> Default App </div>
|
||||
</Tab>
|
||||
<Tab size="xs" value="encryption">
|
||||
<div class="flex gap-2 items-center my-1"> Encryption </div>
|
||||
</Tab>
|
||||
<Tab size="xs" value="export_delete">
|
||||
<div class="flex gap-2 items-center my-1"> Delete Workspace </div>
|
||||
</Tab>
|
||||
@@ -1367,6 +1415,50 @@ git push</code
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{:else if tab == 'encryption'}
|
||||
<PageHeader title="Workspace secret encryption" primary={false} />
|
||||
<Alert type="info" title="Windmill EE only feature">
|
||||
When updating the encryption key of a workspace, all secrets will be re-encrypted with the
|
||||
new key and the previous key will be replaced by the new one.
|
||||
<br />
|
||||
If you're manually updating the key to match another workspace key from another Windmill instance,
|
||||
make sure not to use the 'SECRET_SALT' environment variable or, if you're using it, make sure
|
||||
it the salt matches across both instances.
|
||||
</Alert>
|
||||
<div class="mt-5 flex gap-1 mb-10">
|
||||
<Button
|
||||
color="blue"
|
||||
disabled={editedWorkspaceEncryptionKey === workspaceEncryptionKey ||
|
||||
!encryptionKeyRegex.test(editedWorkspaceEncryptionKey ?? '')}
|
||||
startIcon={{
|
||||
icon: workspaceReencryptionInProgress ? RotateCw : Save,
|
||||
classes: workspaceReencryptionInProgress ? 'animate-spin' : ''
|
||||
}}
|
||||
on:click={() => {
|
||||
setWorkspaceEncryptionKey()
|
||||
}}>Save & Re-encrypt workspace</Button
|
||||
>
|
||||
</div>
|
||||
<h6> Workspace encryption key </h6>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<input
|
||||
class="justify-start"
|
||||
type="text"
|
||||
placeholder={'*'.repeat(64)}
|
||||
bind:value={editedWorkspaceEncryptionKey}
|
||||
/>
|
||||
<Button
|
||||
color="light"
|
||||
on:click={() => {
|
||||
loadWorkspaceEncryptionKey()
|
||||
}}>Load current key</Button
|
||||
>
|
||||
</div>
|
||||
{#if !emptyString(editedWorkspaceEncryptionKey) && !encryptionKeyRegex.test(editedWorkspaceEncryptionKey ?? '')}
|
||||
<div class="text-xs text-red-600">
|
||||
Key invalid - it should be 64 characters long and only contain letters and numbers.
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4" role="alert">
|
||||
|
||||
Reference in New Issue
Block a user