mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat(service): variables editor with masked-secret preservation
PUT /services/{id}/variables replaced every row, and GET masks secrets as
******** — echoing the fetched list back would have overwritten real
secrets with asterisks. The backend now keeps the stored value when a
submitted secret still carries the mask.
Service detail gains a Variables section: add/edit/remove rows, secret
toggle, bulk .env paste (comments, export prefix, quoted values), and
inline validation for empty/invalid/duplicate keys. Parser and validator
live in variable-utils.ts with unit coverage.
Generated OpenAPI types drive the new updateServiceVariables client call.
ROADMAP: Variables UX marked done; per-environment values are already
structural — each environment owns its service instances.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
e6e76c1084
commit
1d5b99e1ba
6 files changed
+354
-13
No files matched your search
@@ -0,0 +1,48 @@
|
||||
export type VariableDraft = { key: string; value: string; isSecret: boolean };
|
||||
|
||||
export const VARIABLE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
|
||||
// parseDotenv handles KEY=VAL lines, '#' comments, 'export ' prefixes and
|
||||
// single/double-quoted values — enough for standard .env pastes.
|
||||
export function parseDotenv(text: string): VariableDraft[] {
|
||||
const rows: VariableDraft[] = [];
|
||||
for (const rawLine of text.split('\n')) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
const stripped = line.startsWith('export ') ? line.slice(7).trim() : line;
|
||||
const eq = stripped.indexOf('=');
|
||||
if (eq <= 0) {
|
||||
continue;
|
||||
}
|
||||
const key = stripped.slice(0, eq).trim();
|
||||
let value = stripped.slice(eq + 1).trim();
|
||||
if (
|
||||
value.length >= 2 &&
|
||||
((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
rows.push({ key, value, isSecret: false });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function validateVariableRows(rows: VariableDraft[]): Record<number, string> {
|
||||
const errors: Record<number, string> = {};
|
||||
const seen = new Set<string>();
|
||||
rows.forEach((row, i) => {
|
||||
const key = row.key.trim();
|
||||
if (!key) {
|
||||
errors[i] = 'Key is required';
|
||||
} else if (!VARIABLE_KEY_RE.test(key)) {
|
||||
errors[i] = 'Letters, digits, underscore; must not start with a digit';
|
||||
} else if (seen.has(key)) {
|
||||
errors[i] = 'Duplicate key';
|
||||
} else {
|
||||
seen.add(key);
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
Reference in new issue
Block a user