Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,22 @@ jobs:

- name: Run frontend tests
run: cd portal/frontend && npm test

build-frontend:
runs-on: ubuntu-latest
needs: validate
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: portal/frontend/package-lock.json

- name: Install frontend dependencies
run: cd portal/frontend && npm ci

- name: Build frontend for production
run: make build-frontend
7 changes: 7 additions & 0 deletions portal/frontend/src/pages/HomePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,12 @@ describe('HomePage', () => {
expect(screen.getByText('URL de arranque iPXE')).toBeInTheDocument();
expect(screen.getByText(/boot\/tokentest/i)).toBeInTheDocument();
});

// Verifica que el POST incluye encryption y snapshots por defecto
const calls = (globalThis.fetch as any).mock.calls;
const postCall = calls.find((c: any[]) => c[1]?.method === 'POST');
const body = JSON.parse(postCall[1].body);
expect(body.encryption).toEqual({ enabled: true, method: 'keyfile' });
expect(body.snapshots).toEqual({ enabled: true });
});
});
78 changes: 77 additions & 1 deletion portal/frontend/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { CheckCircle, Copy, Server, Terminal, Wifi } from 'lucide-react';
import { CheckCircle, Copy, Server, Terminal, Wifi, Shield, History } from 'lucide-react';

const statusColors: Record<Installation['status'], string> = {
pending: 'bg-cyan-500/10 text-cyan-400 border-cyan-500/20',
Expand All @@ -29,6 +29,9 @@ export function HomePage() {
const [submitting, setSubmitting] = useState(false);
const [result, setResult] = useState<InstallResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [enableEncryption, setEnableEncryption] = useState(true);
const [encryptionMethod, setEncryptionMethod] = useState<'keyfile' | 'passphrase'>('keyfile');
const [enableSnapshots, setEnableSnapshots] = useState(true);

useEffect(() => {
loadInstallations();
Expand Down Expand Up @@ -61,6 +64,13 @@ export function HomePage() {
.filter(Boolean),
};

if (enableEncryption) {
body.encryption = { enabled: true, method: encryptionMethod };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passphrase method sends no secret

Medium Severity

Choosing passphrase posts only { enabled: true, method: 'passphrase' }; InstallRequest has no passphrase field and the form never collects one. The installer formats LUKS with the profile secret or aborts if it is empty, so base and developer fail and production silently uses neubat.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b427562. Configure here.

}
if (enableSnapshots) {
body.snapshots = { enabled: true };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchecked options keep profile defaults

High Severity

Unchecking enableEncryption or enableSnapshots omits those objects from the install POST instead of sending enabled: false. The portal merges them only when present, so the default production profile still enables LUKS and snapper. The form states that unchecked encryption formats root and home without LUKS.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b427562. Configure here.


try {
const data = await api.install(body);
setResult(data);
Expand Down Expand Up @@ -129,6 +139,72 @@ export function HomePage() {
</div>
</div>

<div className="rounded-md border border-border bg-secondary/30 p-4 space-y-4">
<h3 className="text-sm font-medium flex items-center gap-2">
<Shield className="h-4 w-4 text-cyan-400" />
Opciones avanzadas
</h3>

<div className="flex items-start gap-3">
<input
id="enable-encryption"
type="checkbox"
checked={enableEncryption}
onChange={(e) => setEnableEncryption(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-border bg-background text-cyan-400 focus:ring-cyan-400"
/>
<div className="flex-1 space-y-2">
<Label htmlFor="enable-encryption" className="font-normal">
Cifrar disco con LUKS2
</Label>
{enableEncryption && (
<Select
value={encryptionMethod}
onValueChange={(v) => setEncryptionMethod(v as 'keyfile' | 'passphrase')}
>
<SelectTrigger className="w-full sm:w-64">
<SelectValue placeholder="Método de arranque" />
</SelectTrigger>
<SelectContent>
<SelectItem value="keyfile">
Keyfile en /boot (desatendido)
</SelectItem>
<SelectItem value="passphrase">
Passphrase (más seguro, interactivo)
</SelectItem>
</SelectContent>
</Select>
)}
<p className="text-xs text-muted-foreground">
{enableEncryption
? encryptionMethod === 'keyfile'
? 'Arranque zero-touch. Cambia la llave tras la instalación para mayor seguridad física.'
: 'El arranque pedirá la contraseña en cada reinicio. Rompe el despliegue desatendido.'
: 'Las particiones raíz y home se formatearán sin cifrado.'}
</p>
</div>
</div>

<div className="flex items-start gap-3">
<input
id="enable-snapshots"
type="checkbox"
checked={enableSnapshots}
onChange={(e) => setEnableSnapshots(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-border bg-background text-cyan-400 focus:ring-cyan-400"
/>
<div>
<Label htmlFor="enable-snapshots" className="font-normal flex items-center gap-2">
<History className="h-3.5 w-3.5" />
Snapshots btrfs automáticos
</Label>
<p className="text-xs text-muted-foreground">
Instala snapper + snap-pac para snapshots pre/post actualización y rollback.
</p>
</div>
</div>
</div>

<Button type="submit" disabled={submitting} className="w-full">
{submitting ? 'Generando...' : 'Generar instalación'}
</Button>
Expand Down
7 changes: 7 additions & 0 deletions portal/frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export interface InstallRequest {
username?: string;
desktop?: string;
packages?: string[];
encryption?: {
enabled: boolean;
method?: 'keyfile' | 'passphrase';
};
snapshots?: {
enabled: boolean;
};
}

export interface InstallResponse {
Expand Down
Loading