Skip to content

Commit f1c9179

Browse files
author
Vadim Belov
committed
Merge branch 'main' of https://github.com/bvdcode/EasyVault
2 parents cf3f429 + c140e70 commit f1c9179

18 files changed

Lines changed: 410 additions & 134 deletions

File tree

.github/workflows/publish-release.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ jobs:
5151
id: tags
5252
shell: bash
5353
run: |
54-
V_MINOR="v${{ steps.gitversion.outputs.Major }}.${{ steps.gitversion.outputs.Minor }}"
55-
echo "minor_tag=$V_MINOR" >> "$GITHUB_OUTPUT"
56-
echo "docker_tag=${{ env.DOCKER_NAMESPACE }}/${{ env.DOCKER_IMAGE_NAME }}:$V_MINOR" >> "$GITHUB_OUTPUT"
54+
VERSION="v${{ steps.gitversion.outputs.SemVer }}"
55+
echo "version_tag=$VERSION" >> "$GITHUB_OUTPUT"
56+
echo "docker_tag=${{ env.DOCKER_NAMESPACE }}/${{ env.DOCKER_IMAGE_NAME }}:$VERSION" >> "$GITHUB_OUTPUT"
5757
echo "docker_tag_latest=${{ env.DOCKER_NAMESPACE }}/${{ env.DOCKER_IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT"
58-
echo "ghcr_tag=ghcr.io/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE_NAME }}:$V_MINOR" >> "$GITHUB_OUTPUT"
58+
echo "ghcr_tag=ghcr.io/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE_NAME }}:$VERSION" >> "$GITHUB_OUTPUT"
5959
echo "ghcr_tag_latest=ghcr.io/${{ github.repository_owner }}/${{ env.DOCKER_IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT"
6060
6161
- name: Login to Docker Hub
@@ -125,8 +125,8 @@ jobs:
125125
if: ${{ steps.gitversion.outputs.CommitsSinceVersionSource > 0 }}
126126
uses: ncipollo/release-action@v1
127127
with:
128-
tag: ${{ steps.tags.outputs.minor_tag }}
129-
name: Release ${{ steps.tags.outputs.minor_tag }}
128+
tag: ${{ steps.tags.outputs.version_tag }}
129+
name: Release ${{ steps.tags.outputs.version_tag }}
130130
draft: false
131131
prerelease: false
132132
token: ${{ github.token }}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
# EasyVault
1414

15-
> Live: [easyvault.belov.us](https://easyvault.belov.us) - The key you enter will encrypt your secrets (Easy)
15+
> Live: [vault.splidex.com](https://vault.splidex.com) - The key you enter will encrypt your secrets (Easy)
1616
1717
Lightweight, self‑contained Zero-Trust secrets service — a single Docker image with a built‑in Web UI. Run the container, open the UI, enter any encryption key and manage secrets without extra setup.
1818

Sources/EasyVault.Server/Controllers/VaultController.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace EasyVault.Server.Controllers
1515
public class VaultController(ILogger<VaultController> _logger, AppDbContext _dbContext, IVault _vault) : ControllerBase
1616
{
1717
[HttpGet("/api/v1/vault/{key}")]
18+
[HttpGet("/api/v2/vault/{key}")]
1819
public async Task<IEnumerable<VaultSecret>> GetVaultAsync([FromRoute][Required] string key)
1920
{
2021
string ip = Request.GetRemoteAddress();
@@ -46,6 +47,7 @@ public async Task<IEnumerable<VaultSecret>> GetVaultAsync([FromRoute][Required]
4647
}
4748

4849
[HttpPost("/api/v1/vault/{key}")]
50+
[HttpPost("/api/v2/vault/{key}")]
4951
public async Task<IActionResult> UpdateVaultAsync([FromRoute][Required] string key, [FromBody] IEnumerable<VaultSecret> secrets)
5052
{
5153
string ip = Request.GetRemoteAddress();
@@ -95,6 +97,7 @@ public async Task<IActionResult> UpdateVaultAsync([FromRoute][Required] string k
9597
}
9698

9799
[HttpGet("/api/v1/vault/secrets/{keyId}")]
100+
[HttpGet("/api/v2/vault/secrets/{keyId}")]
98101
public async Task<IActionResult> GetSecret(Guid keyId, [FromQuery] string format = "json")
99102
{
100103
string ip = Request.GetRemoteAddress();
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:5e0c43410671087f1cd1552bdc0397413bc13f5ea38a95654498328dc08749e5
2+
oid sha256:3ac4c8784565839b177f3c9e9db29d49079c1563bdd0c92557a6197031f3f846
33
size 4286

Sources/easyvault.client/src/App.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
11
import "./App.css";
2-
import { Box, CssBaseline } from "@mui/material";
2+
import { GitHub } from "@mui/icons-material";
33
import { LoginPage, VaultPage } from "./pages";
44
import "react-toastify/dist/ReactToastify.css";
5+
import { Box, CssBaseline, Fab } from "@mui/material";
56
import { ConfirmProvider } from "material-ui-confirm";
67
import { ThemeProvider } from "./contexts/ThemeContext";
8+
import { VaultProvider } from "./contexts/VaultContext";
79
import { BrowserRouter, Route, Routes } from "react-router-dom";
810

911
function App() {
1012
return (
1113
<Box className="app">
1214
<ThemeProvider>
13-
<ConfirmProvider>
14-
<BrowserRouter basename="/">
15-
<Routes>
16-
<Route path="/vault" element={<VaultPage />} />
17-
<Route path="*" element={<LoginPage />} />
18-
</Routes>
19-
</BrowserRouter>
20-
<CssBaseline enableColorScheme={true} />
21-
</ConfirmProvider>
15+
<VaultProvider>
16+
<ConfirmProvider>
17+
<BrowserRouter basename="/">
18+
<Routes>
19+
<Route path="/vault" element={<VaultPage />} />
20+
<Route path="*" element={<LoginPage />} />
21+
</Routes>
22+
</BrowserRouter>
23+
<CssBaseline enableColorScheme={true} />
24+
<Fab
25+
color="primary"
26+
aria-label="add"
27+
sx={{ position: "fixed", bottom: 16, right: 16 }}
28+
href="https://github.com/bvdcode/EasyVault"
29+
target="_blank"
30+
rel="noopener noreferrer"
31+
>
32+
<GitHub />
33+
</Fab>
34+
</ConfirmProvider>
35+
</VaultProvider>
2236
</ThemeProvider>
2337
</Box>
2438
);

Sources/easyvault.client/src/assets/vault.png

Lines changed: 0 additions & 3 deletions
This file was deleted.

Sources/easyvault.client/src/components/Settings.tsx

Lines changed: 175 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,173 @@
11
import {
22
Box,
3-
Button,
4-
InputLabel,
5-
MenuItem,
63
Paper,
7-
Select,
8-
SelectChangeEvent,
94
Stack,
5+
Select,
106
Switch,
7+
Button,
8+
MenuItem,
9+
InputLabel,
10+
SelectChangeEvent,
1111
} from "@mui/material";
12+
import {
13+
Upload,
14+
Download,
15+
Brightness4,
16+
Brightness7,
17+
} from "@mui/icons-material";
18+
import { useRef } from "react";
19+
import { toast } from "react-toastify";
1220
import { useNavigate } from "react-router-dom";
1321
import { useTranslation } from "react-i18next";
1422
import { useAppTheme } from "../contexts/ThemeContext";
15-
import { Brightness4, Brightness7 } from "@mui/icons-material";
23+
import { useVault } from "../contexts/VaultContext";
24+
import { VaultData } from "../types";
25+
26+
interface SettingsProps {
27+
onTabChange: (tab: number) => void;
28+
}
1629

17-
const Settings: React.FC = () => {
30+
const Settings: React.FC<SettingsProps> = ({ onTabChange }) => {
1831
const navigate = useNavigate();
1932
const { t, i18n } = useTranslation();
2033
const { isDarkMode, toggleTheme } = useAppTheme();
34+
const { vaultData, importVaultData } = useVault();
35+
const fileInputRef = useRef<HTMLInputElement>(null);
2136

2237
const handleLanguageChange = (event: SelectChangeEvent<string>) => {
2338
i18n.changeLanguage(event.target.value);
2439
};
2540

41+
const validateVaultData = (data: unknown): data is VaultData[] => {
42+
if (!Array.isArray(data)) {
43+
return false;
44+
}
45+
46+
return data.every((item) => {
47+
if (typeof item !== "object" || item === null) {
48+
return false;
49+
}
50+
51+
const entry = item as Record<string, unknown>;
52+
53+
// Check required fields
54+
if (
55+
typeof entry.keyId !== "string" ||
56+
typeof entry.appName !== "string" ||
57+
typeof entry.values !== "object" ||
58+
entry.values === null
59+
) {
60+
return false;
61+
}
62+
63+
// Validate values is Record<string, string>
64+
const values = entry.values as Record<string, unknown>;
65+
if (!Object.values(values).every((v) => typeof v === "string")) {
66+
return false;
67+
}
68+
69+
// Validate optional arrays
70+
if (
71+
entry.allowedAddresses !== undefined &&
72+
(!Array.isArray(entry.allowedAddresses) ||
73+
!entry.allowedAddresses.every((addr) => typeof addr === "string"))
74+
) {
75+
return false;
76+
}
77+
78+
if (
79+
entry.allowedUserAgents !== undefined &&
80+
(!Array.isArray(entry.allowedUserAgents) ||
81+
!entry.allowedUserAgents.every((ua) => typeof ua === "string"))
82+
) {
83+
return false;
84+
}
85+
86+
return true;
87+
});
88+
};
89+
90+
const handleExport = () => {
91+
if (!vaultData || vaultData.length === 0) {
92+
toast.info(t("vaultList.emptyMessage"));
93+
return;
94+
}
95+
96+
try {
97+
const dataStr = JSON.stringify(vaultData, null, 2);
98+
const blob = new Blob([dataStr], { type: "application/json" });
99+
const url = URL.createObjectURL(blob);
100+
const link = document.createElement("a");
101+
link.href = url;
102+
link.download = `easyvault-export-${
103+
new Date().toISOString().split("T")[0]
104+
}.json`;
105+
document.body.appendChild(link);
106+
link.click();
107+
document.body.removeChild(link);
108+
URL.revokeObjectURL(url);
109+
110+
toast.success(t("settings.exportSuccess"));
111+
} catch (error) {
112+
toast.error(
113+
t("settings.importError", {
114+
error: error instanceof Error ? error.message : String(error),
115+
}),
116+
);
117+
}
118+
};
119+
120+
const handleImport = () => {
121+
fileInputRef.current?.click();
122+
};
123+
124+
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
125+
const file = event.target.files?.[0];
126+
if (!file) {
127+
return;
128+
}
129+
130+
const reader = new FileReader();
131+
reader.onload = (e) => {
132+
try {
133+
const content = e.target?.result as string;
134+
const parsedData = JSON.parse(content);
135+
136+
if (!validateVaultData(parsedData)) {
137+
toast.error(t("settings.invalidFileFormat"));
138+
return;
139+
}
140+
141+
importVaultData(parsedData);
142+
toast.success(t("settings.importSuccess"));
143+
onTabChange(0);
144+
} catch (error) {
145+
toast.error(
146+
t("settings.importError", {
147+
error: error instanceof Error ? error.message : String(error),
148+
}),
149+
);
150+
} finally {
151+
// Reset file input
152+
if (fileInputRef.current) {
153+
fileInputRef.current.value = "";
154+
}
155+
}
156+
};
157+
158+
reader.readAsText(file);
159+
};
160+
26161
return (
27162
<Paper sx={{ p: 3, width: "100%", mx: "auto" }}>
28-
<Stack spacing={4} mt={3} maxWidth={400} mx="auto">
163+
<input
164+
type="file"
165+
ref={fileInputRef}
166+
onChange={handleFileChange}
167+
accept=".json"
168+
style={{ display: "none" }}
169+
/>
170+
<Stack spacing={4} mt={3} maxWidth={600} mx="auto">
29171
<Box display="flex" alignItems="center" justifyContent="space-between">
30172
<InputLabel id="language-select-label">
31173
{t("settings.darkMode")}
@@ -83,6 +225,31 @@ const Settings: React.FC = () => {
83225
{t("settings.logout")}
84226
</Button>
85227
</Box>
228+
229+
<Box
230+
display="flex"
231+
alignItems="center"
232+
justifyContent="space-between"
233+
gap={1}
234+
>
235+
<Button
236+
onClick={handleImport}
237+
variant="outlined"
238+
fullWidth
239+
endIcon={<Upload />}
240+
>
241+
{t("settings.import")}
242+
</Button>
243+
<Button
244+
onClick={handleExport}
245+
variant="outlined"
246+
fullWidth
247+
endIcon={<Download />}
248+
disabled={!vaultData || vaultData.length === 0}
249+
>
250+
{t("settings.export")}
251+
</Button>
252+
</Box>
86253
</Stack>
87254
</Paper>
88255
);

Sources/easyvault.client/src/components/VaultEntryEditForm.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const VaultEntryEditForm = forwardRef<
9797
"https://" +
9898
window.location.hostname +
9999
"/api/v1/vault/secrets/" +
100-
formData.keyId
100+
formData.keyId,
101101
);
102102
toast.info(t("vaultEdit.linkCopied"));
103103
}}
@@ -264,7 +264,7 @@ const VaultEntryEditForm = forwardRef<
264264
setFormData((prev) => ({
265265
...prev,
266266
allowedAddresses: prev.allowedAddresses.filter(
267-
(a) => a !== address
267+
(a) => a !== address,
268268
),
269269
}))
270270
}
@@ -325,7 +325,7 @@ const VaultEntryEditForm = forwardRef<
325325
setFormData((prev) => ({
326326
...prev,
327327
allowedUserAgents: prev.allowedUserAgents.filter(
328-
(a) => a !== agent
328+
(a) => a !== agent,
329329
),
330330
}))
331331
}

0 commit comments

Comments
 (0)