refactor: migrate UnlockLayout to Tailwind + extract ToolShell wrapper
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m54s
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m54s
- Convert all ~40 inline styles in UnlockLayout.vue to Tailwind CSS v4 arbitrary-value classes, matching MainLayout.vue color conventions (CSS variables + hex arbitraries). Visual appearance preserved exactly. - Create ToolShell.vue reusable wrapper that owns output/running state and execute/setOutput API via defineExpose. - Refactor PingTool, TracerouteTool, DnsLookup, WhoisTool, BandwidthTest to use ToolShell — each tool now contains only its unique inputs and invoke calls. Zero vue-tsc errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b86e2d68d8
commit
d4bfb3d5fd
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full p-4 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<ToolShell ref="shell" placeholder="Select a mode and click Run Test">
|
||||
<template #default="{ running }">
|
||||
<select v-model="mode" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none cursor-pointer">
|
||||
<option value="speedtest">Internet Speed Test</option>
|
||||
<option value="iperf">iperf3 (LAN)</option>
|
||||
@ -13,32 +13,31 @@
|
||||
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="run">
|
||||
{{ running ? "Testing..." : "Run Test" }}
|
||||
</button>
|
||||
</div>
|
||||
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Select a mode and click Run Test" }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</ToolShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import ToolShell from "./ToolShell.vue";
|
||||
|
||||
const props = defineProps<{ sessionId: string }>();
|
||||
const mode = ref("speedtest");
|
||||
const server = ref("");
|
||||
const duration = ref(5);
|
||||
const output = ref("");
|
||||
const running = ref(false);
|
||||
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
|
||||
|
||||
async function run(): Promise<void> {
|
||||
running.value = true;
|
||||
output.value = mode.value === "iperf" ? `Running iperf3 to ${server.value}...\n` : "Running speed test...\n";
|
||||
try {
|
||||
if (mode.value === "iperf") {
|
||||
if (!server.value) { output.value = "Enter an iperf3 server IP"; running.value = false; return; }
|
||||
output.value = await invoke<string>("tool_bandwidth_iperf", { sessionId: props.sessionId, server: server.value, duration: duration.value });
|
||||
} else {
|
||||
output.value = await invoke<string>("tool_bandwidth_speedtest", { sessionId: props.sessionId });
|
||||
if (mode.value === "iperf" && !server.value) {
|
||||
shell.value?.setOutput("Enter an iperf3 server IP");
|
||||
return;
|
||||
}
|
||||
} catch (err) { output.value = String(err); }
|
||||
running.value = false;
|
||||
shell.value?.execute(() => {
|
||||
if (mode.value === "iperf") {
|
||||
return invoke<string>("tool_bandwidth_iperf", { sessionId: props.sessionId, server: server.value, duration: duration.value });
|
||||
}
|
||||
return invoke<string>("tool_bandwidth_speedtest", { sessionId: props.sessionId });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,31 +1,29 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full p-4 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<ToolShell ref="shell" placeholder="Enter a domain and click Lookup">
|
||||
<template #default="{ running }">
|
||||
<input v-model="domain" type="text" placeholder="Domain name" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="lookup" />
|
||||
<select v-model="recordType" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none cursor-pointer">
|
||||
<option v-for="t in ['A','AAAA','MX','NS','TXT','CNAME','SOA','SRV','PTR']" :key="t" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="lookup">Lookup</button>
|
||||
</div>
|
||||
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a domain and click Lookup" }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</ToolShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import ToolShell from "./ToolShell.vue";
|
||||
|
||||
const props = defineProps<{ sessionId: string }>();
|
||||
const domain = ref("");
|
||||
const recordType = ref("A");
|
||||
const output = ref("");
|
||||
const running = ref(false);
|
||||
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
|
||||
|
||||
async function lookup(): Promise<void> {
|
||||
if (!domain.value) return;
|
||||
running.value = true;
|
||||
try {
|
||||
output.value = await invoke<string>("tool_dns_lookup", { sessionId: props.sessionId, domain: domain.value, recordType: recordType.value });
|
||||
} catch (err) { output.value = String(err); }
|
||||
running.value = false;
|
||||
shell.value?.execute(() =>
|
||||
invoke<string>("tool_dns_lookup", { sessionId: props.sessionId, domain: domain.value, recordType: recordType.value })
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,32 +1,28 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full p-4 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<ToolShell ref="shell" placeholder="Enter a host and click Ping">
|
||||
<template #default="{ running }">
|
||||
<input v-model="target" type="text" placeholder="Host to ping" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="ping" />
|
||||
<input v-model.number="count" type="number" min="1" max="100" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-16" />
|
||||
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="ping">Ping</button>
|
||||
</div>
|
||||
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a host and click Ping" }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</ToolShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import ToolShell from "./ToolShell.vue";
|
||||
|
||||
const props = defineProps<{ sessionId: string }>();
|
||||
const target = ref("");
|
||||
const count = ref(4);
|
||||
const output = ref("");
|
||||
const running = ref(false);
|
||||
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
|
||||
|
||||
async function ping(): Promise<void> {
|
||||
if (!target.value) return;
|
||||
running.value = true;
|
||||
output.value = `Pinging ${target.value}...\n`;
|
||||
try {
|
||||
shell.value?.execute(async () => {
|
||||
const result = await invoke<{ target: string; output: string }>("tool_ping", { sessionId: props.sessionId, target: target.value, count: count.value });
|
||||
output.value = result.output;
|
||||
} catch (err) { output.value = String(err); }
|
||||
running.value = false;
|
||||
return result.output;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
37
src/components/tools/ToolShell.vue
Normal file
37
src/components/tools/ToolShell.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
defineProps<{
|
||||
placeholder?: string;
|
||||
}>();
|
||||
|
||||
const output = ref("");
|
||||
const running = ref(false);
|
||||
|
||||
async function execute(fn: () => Promise<string>): Promise<void> {
|
||||
running.value = true;
|
||||
output.value = "";
|
||||
try {
|
||||
output.value = await fn();
|
||||
} catch (err: unknown) {
|
||||
output.value = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
||||
} finally {
|
||||
running.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setOutput(value: string): void {
|
||||
output.value = value;
|
||||
}
|
||||
|
||||
defineExpose({ execute, setOutput, output, running });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full p-4 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<slot :running="running" />
|
||||
</div>
|
||||
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || placeholder || "Ready." }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,29 +1,25 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full p-4 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<ToolShell ref="shell" placeholder="Enter a host and click Trace">
|
||||
<template #default="{ running }">
|
||||
<input v-model="target" type="text" placeholder="Host to trace" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="trace" />
|
||||
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="trace">Trace</button>
|
||||
</div>
|
||||
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a host and click Trace" }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</ToolShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import ToolShell from "./ToolShell.vue";
|
||||
|
||||
const props = defineProps<{ sessionId: string }>();
|
||||
const target = ref("");
|
||||
const output = ref("");
|
||||
const running = ref(false);
|
||||
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
|
||||
|
||||
async function trace(): Promise<void> {
|
||||
if (!target.value) return;
|
||||
running.value = true;
|
||||
output.value = `Tracing route to ${target.value}...\n`;
|
||||
try {
|
||||
output.value = await invoke<string>("tool_traceroute", { sessionId: props.sessionId, target: target.value });
|
||||
} catch (err) { output.value = String(err); }
|
||||
running.value = false;
|
||||
shell.value?.execute(() =>
|
||||
invoke<string>("tool_traceroute", { sessionId: props.sessionId, target: target.value })
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,26 +1,25 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full p-4 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<ToolShell ref="shell" placeholder="Enter a domain or IP and click Whois">
|
||||
<template #default="{ running }">
|
||||
<input v-model="target" type="text" placeholder="Domain or IP" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="lookup" />
|
||||
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="lookup">Whois</button>
|
||||
</div>
|
||||
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a domain or IP and click Whois" }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</ToolShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import ToolShell from "./ToolShell.vue";
|
||||
|
||||
const props = defineProps<{ sessionId: string }>();
|
||||
const target = ref("");
|
||||
const output = ref("");
|
||||
const running = ref(false);
|
||||
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
|
||||
|
||||
async function lookup(): Promise<void> {
|
||||
if (!target.value) return;
|
||||
running.value = true;
|
||||
try { output.value = await invoke<string>("tool_whois", { sessionId: props.sessionId, target: target.value }); }
|
||||
catch (err) { output.value = String(err); }
|
||||
running.value = false;
|
||||
shell.value?.execute(() =>
|
||||
invoke<string>("tool_whois", { sessionId: props.sessionId, target: target.value })
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -50,68 +50,25 @@ const displayError = computed(() => localError.value ?? app.error);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="unlock-root"
|
||||
style="
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--wraith-bg-primary);
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="unlock-card"
|
||||
style="
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 2.5rem;
|
||||
background-color: var(--wraith-bg-secondary);
|
||||
border: 1px solid var(--wraith-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
"
|
||||
>
|
||||
<div class="h-full flex items-center justify-center bg-[var(--wraith-bg-primary)]">
|
||||
<div class="w-full max-w-[400px] p-10 bg-[var(--wraith-bg-secondary)] border border-[var(--wraith-border)] rounded-xl shadow-[0_8px_32px_rgba(0,0,0,0.5)]">
|
||||
<!-- Logo -->
|
||||
<div style="text-align: center; margin-bottom: 2rem">
|
||||
<span
|
||||
style="
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--wraith-accent-blue);
|
||||
text-transform: uppercase;
|
||||
font-family: 'Inter', monospace;
|
||||
"
|
||||
>
|
||||
<div class="text-center mb-8">
|
||||
<span class="text-[2rem] font-extrabold tracking-[0.3em] text-[var(--wraith-accent-blue)] uppercase font-['Inter',monospace]">
|
||||
WRAITH
|
||||
</span>
|
||||
<p
|
||||
style="
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--wraith-text-muted);
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
"
|
||||
>
|
||||
<p class="mt-2 text-[0.8rem] text-[var(--wraith-text-muted)] tracking-[0.15em] uppercase">
|
||||
{{ isFirstRun ? "Initialize Secure Vault" : "Secure Desktop" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form @submit.prevent="handleSubmit" style="display: flex; flex-direction: column; gap: 1rem">
|
||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-4">
|
||||
<!-- Master password -->
|
||||
<div>
|
||||
<label
|
||||
for="master-password"
|
||||
style="
|
||||
display: block;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--wraith-text-secondary);
|
||||
letter-spacing: 0.05em;
|
||||
"
|
||||
class="block mb-[0.4rem] text-[0.8rem] text-[var(--wraith-text-secondary)] tracking-[0.05em]"
|
||||
>
|
||||
MASTER PASSWORD
|
||||
</label>
|
||||
@ -122,20 +79,7 @@ const displayError = computed(() => localError.value ?? app.error);
|
||||
autocomplete="current-password"
|
||||
placeholder="Enter master password"
|
||||
:disabled="loading"
|
||||
style="
|
||||
width: 100%;
|
||||
padding: 0.65rem 0.9rem;
|
||||
background-color: var(--wraith-bg-tertiary);
|
||||
border: 1px solid var(--wraith-border);
|
||||
border-radius: 6px;
|
||||
color: var(--wraith-text-primary);
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
"
|
||||
@focus="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-accent-blue)'"
|
||||
@blur="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-border)'"
|
||||
class="w-full px-[0.9rem] py-[0.65rem] bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] rounded-[6px] text-[var(--wraith-text-primary)] text-[0.95rem] outline-none transition-colors duration-150 box-border focus:border-[var(--wraith-accent-blue)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -143,13 +87,7 @@ const displayError = computed(() => localError.value ?? app.error);
|
||||
<div v-if="isFirstRun">
|
||||
<label
|
||||
for="confirm-password"
|
||||
style="
|
||||
display: block;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--wraith-text-secondary);
|
||||
letter-spacing: 0.05em;
|
||||
"
|
||||
class="block mb-[0.4rem] text-[0.8rem] text-[var(--wraith-text-secondary)] tracking-[0.05em]"
|
||||
>
|
||||
CONFIRM PASSWORD
|
||||
</label>
|
||||
@ -160,28 +98,9 @@ const displayError = computed(() => localError.value ?? app.error);
|
||||
autocomplete="new-password"
|
||||
placeholder="Confirm master password"
|
||||
:disabled="loading"
|
||||
style="
|
||||
width: 100%;
|
||||
padding: 0.65rem 0.9rem;
|
||||
background-color: var(--wraith-bg-tertiary);
|
||||
border: 1px solid var(--wraith-border);
|
||||
border-radius: 6px;
|
||||
color: var(--wraith-text-primary);
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
"
|
||||
@focus="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-accent-blue)'"
|
||||
@blur="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-border)'"
|
||||
class="w-full px-[0.9rem] py-[0.65rem] bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] rounded-[6px] text-[var(--wraith-text-primary)] text-[0.95rem] outline-none transition-colors duration-150 box-border focus:border-[var(--wraith-accent-blue)]"
|
||||
/>
|
||||
<p
|
||||
style="
|
||||
margin: 0.4rem 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--wraith-text-muted);
|
||||
"
|
||||
>
|
||||
<p class="mt-[0.4rem] text-[0.75rem] text-[var(--wraith-text-muted)]">
|
||||
Minimum 12 characters. This password cannot be recovered.
|
||||
</p>
|
||||
</div>
|
||||
@ -189,14 +108,7 @@ const displayError = computed(() => localError.value ?? app.error);
|
||||
<!-- Error message -->
|
||||
<div
|
||||
v-if="displayError"
|
||||
style="
|
||||
padding: 0.6rem 0.9rem;
|
||||
background-color: rgba(248, 81, 73, 0.1);
|
||||
border: 1px solid rgba(248, 81, 73, 0.3);
|
||||
border-radius: 6px;
|
||||
color: var(--wraith-accent-red);
|
||||
font-size: 0.85rem;
|
||||
"
|
||||
class="px-[0.9rem] py-[0.6rem] bg-[rgba(248,81,73,0.1)] border border-[rgba(248,81,73,0.3)] rounded-[6px] text-[var(--wraith-accent-red)] text-[0.85rem]"
|
||||
>
|
||||
{{ displayError }}
|
||||
</div>
|
||||
@ -205,22 +117,8 @@ const displayError = computed(() => localError.value ?? app.error);
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
style="
|
||||
width: 100%;
|
||||
padding: 0.7rem;
|
||||
margin-top: 0.5rem;
|
||||
background-color: var(--wraith-accent-blue);
|
||||
color: #0d1117;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease, background-color 0.15s ease;
|
||||
"
|
||||
:style="{ opacity: loading ? '0.6' : '1', cursor: loading ? 'not-allowed' : 'pointer' }"
|
||||
class="w-full py-[0.7rem] mt-2 bg-[var(--wraith-accent-blue)] text-[#0d1117] font-bold text-[0.9rem] tracking-[0.08em] uppercase border-none rounded-[6px] transition-[opacity,background-color] duration-150"
|
||||
:class="loading ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'"
|
||||
>
|
||||
<span v-if="loading">
|
||||
{{ isFirstRun ? "Creating vault..." : "Unlocking..." }}
|
||||
@ -232,14 +130,7 @@ const displayError = computed(() => localError.value ?? app.error);
|
||||
</form>
|
||||
|
||||
<!-- Footer hint -->
|
||||
<p
|
||||
style="
|
||||
margin: 1.5rem 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--wraith-text-muted);
|
||||
"
|
||||
>
|
||||
<p class="mt-6 text-center text-[0.75rem] text-[var(--wraith-text-muted)]">
|
||||
<template v-if="isFirstRun">
|
||||
Your vault will be encrypted with AES-256-GCM.
|
||||
</template>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user