SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
import { Router, type IRouter } from "express";
|
|
|
|
|
import { db } from "@workspace/db";
|
|
|
|
|
import { aiProvidersTable, type AiProvider } from "@workspace/db";
|
|
|
|
|
import { eq } from "drizzle-orm";
|
|
|
|
|
import {
|
|
|
|
|
ListProvidersResponse,
|
|
|
|
|
CreateProviderBody,
|
|
|
|
|
UpdateProviderParams,
|
|
|
|
|
UpdateProviderBody,
|
|
|
|
|
UpdateProviderResponse,
|
|
|
|
|
DeleteProviderParams,
|
|
|
|
|
TestProviderParams,
|
|
|
|
|
TestProviderResponse,
|
2026-06-10 18:54:56 +00:00
|
|
|
TestProviderConnectionBody,
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
} from "@workspace/api-zod";
|
|
|
|
|
import { callProvider } from "../lib/aiAnalysis";
|
|
|
|
|
|
|
|
|
|
const router: IRouter = Router();
|
|
|
|
|
|
|
|
|
|
function maskToken(token: string | null): string {
|
|
|
|
|
if (!token) return "";
|
|
|
|
|
if (token.length <= 8) return "••••";
|
|
|
|
|
return `${token.slice(0, 3)}…${token.slice(-4)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function serializeProvider(p: AiProvider) {
|
|
|
|
|
return {
|
|
|
|
|
id: p.id,
|
|
|
|
|
name: p.name,
|
|
|
|
|
apiType: p.apiType,
|
|
|
|
|
baseUrl: p.baseUrl,
|
|
|
|
|
model: p.model,
|
|
|
|
|
enabled: p.enabled,
|
|
|
|
|
hasToken: !!p.apiToken,
|
|
|
|
|
tokenPreview: maskToken(p.apiToken),
|
|
|
|
|
createdAt: p.createdAt.toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
router.get("/providers", async (_req, res) => {
|
|
|
|
|
const rows = await db.select().from(aiProvidersTable).orderBy(aiProvidersTable.id);
|
|
|
|
|
res.json(ListProvidersResponse.parse(rows.map(serializeProvider)));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post("/providers", async (req, res) => {
|
|
|
|
|
const parsed = CreateProviderBody.safeParse(req.body);
|
|
|
|
|
if (!parsed.success)
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ message: "Ungültige Eingabe", details: parsed.error.issues });
|
|
|
|
|
const d = parsed.data;
|
|
|
|
|
const [created] = await db
|
|
|
|
|
.insert(aiProvidersTable)
|
|
|
|
|
.values({
|
|
|
|
|
name: d.name,
|
|
|
|
|
apiType: d.apiType,
|
|
|
|
|
baseUrl: d.baseUrl,
|
|
|
|
|
model: d.model,
|
|
|
|
|
apiToken: d.apiToken ?? null,
|
|
|
|
|
enabled: d.enabled ?? true,
|
|
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
return res
|
|
|
|
|
.status(201)
|
|
|
|
|
.json(UpdateProviderResponse.parse(serializeProvider(created)));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.patch("/providers/:id", async (req, res) => {
|
|
|
|
|
const params = UpdateProviderParams.safeParse(req.params);
|
|
|
|
|
if (!params.success) return res.status(400).json({ message: "Ungültige ID" });
|
|
|
|
|
const parsed = UpdateProviderBody.safeParse(req.body);
|
|
|
|
|
if (!parsed.success)
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ message: "Ungültige Eingabe", details: parsed.error.issues });
|
|
|
|
|
const d = parsed.data;
|
|
|
|
|
|
|
|
|
|
const update: Partial<typeof aiProvidersTable.$inferInsert> = {};
|
|
|
|
|
if (d.name !== undefined) update.name = d.name;
|
|
|
|
|
if (d.apiType !== undefined) update.apiType = d.apiType;
|
|
|
|
|
if (d.baseUrl !== undefined) update.baseUrl = d.baseUrl;
|
|
|
|
|
if (d.model !== undefined) update.model = d.model;
|
|
|
|
|
if (d.enabled !== undefined) update.enabled = d.enabled;
|
|
|
|
|
if (d.apiToken !== undefined && d.apiToken !== "")
|
|
|
|
|
update.apiToken = d.apiToken;
|
|
|
|
|
|
|
|
|
|
const [updated] = await db
|
|
|
|
|
.update(aiProvidersTable)
|
|
|
|
|
.set(update)
|
|
|
|
|
.where(eq(aiProvidersTable.id, params.data.id))
|
|
|
|
|
.returning();
|
|
|
|
|
if (!updated)
|
|
|
|
|
return res.status(404).json({ message: "Provider nicht gefunden" });
|
|
|
|
|
return res.json(UpdateProviderResponse.parse(serializeProvider(updated)));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.delete("/providers/:id", async (req, res) => {
|
|
|
|
|
const params = DeleteProviderParams.safeParse(req.params);
|
|
|
|
|
if (!params.success) return res.status(400).json({ message: "Ungültige ID" });
|
|
|
|
|
await db
|
|
|
|
|
.delete(aiProvidersTable)
|
|
|
|
|
.where(eq(aiProvidersTable.id, params.data.id));
|
|
|
|
|
return res.status(204).send();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post("/providers/:id/test", async (req, res) => {
|
|
|
|
|
const params = TestProviderParams.safeParse(req.params);
|
|
|
|
|
if (!params.success) return res.status(400).json({ message: "Ungültige ID" });
|
|
|
|
|
const [provider] = await db
|
|
|
|
|
.select()
|
|
|
|
|
.from(aiProvidersTable)
|
|
|
|
|
.where(eq(aiProvidersTable.id, params.data.id));
|
|
|
|
|
if (!provider)
|
|
|
|
|
return res.status(404).json({ message: "Provider nicht gefunden" });
|
|
|
|
|
if (!provider.apiToken) {
|
|
|
|
|
return res.json(
|
|
|
|
|
TestProviderResponse.parse({
|
|
|
|
|
ok: false,
|
|
|
|
|
message: "Kein API-Token hinterlegt.",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const reply = await callProvider(
|
|
|
|
|
provider,
|
|
|
|
|
"Du bist ein Verbindungstest.",
|
|
|
|
|
'Antworte mit dem einzelnen Wort "OK".',
|
|
|
|
|
);
|
|
|
|
|
return res.json(
|
|
|
|
|
TestProviderResponse.parse({
|
|
|
|
|
ok: true,
|
|
|
|
|
message: `Verbindung erfolgreich. Antwort: ${reply.trim().slice(0, 80) || "(leer)"}`,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return res.json(
|
|
|
|
|
TestProviderResponse.parse({
|
|
|
|
|
ok: false,
|
|
|
|
|
message: err instanceof Error ? err.message : "Verbindung fehlgeschlagen.",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-10 18:54:56 +00:00
|
|
|
router.post("/providers/test-connection", async (req, res) => {
|
|
|
|
|
const parsed = TestProviderConnectionBody.safeParse(req.body);
|
|
|
|
|
if (!parsed.success)
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ message: "Ungültige Eingabe", details: parsed.error.issues });
|
|
|
|
|
const d = parsed.data;
|
|
|
|
|
|
|
|
|
|
let token: string | null =
|
|
|
|
|
d.apiToken && d.apiToken !== "" ? d.apiToken : null;
|
|
|
|
|
if (!token && d.providerId !== undefined && d.providerId !== null) {
|
|
|
|
|
const [existing] = await db
|
|
|
|
|
.select()
|
|
|
|
|
.from(aiProvidersTable)
|
|
|
|
|
.where(eq(aiProvidersTable.id, d.providerId));
|
|
|
|
|
if (existing?.apiToken) token = existing.apiToken;
|
|
|
|
|
}
|
|
|
|
|
if (!token) {
|
|
|
|
|
return res.json(
|
|
|
|
|
TestProviderResponse.parse({
|
|
|
|
|
ok: false,
|
|
|
|
|
message: "Kein API-Token angegeben.",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const provider: AiProvider = {
|
|
|
|
|
id: d.providerId ?? 0,
|
|
|
|
|
name: "",
|
|
|
|
|
apiType: d.apiType,
|
|
|
|
|
baseUrl: d.baseUrl,
|
|
|
|
|
model: d.model,
|
|
|
|
|
apiToken: token,
|
|
|
|
|
enabled: true,
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const reply = await callProvider(
|
|
|
|
|
provider,
|
|
|
|
|
"Du bist ein Verbindungstest.",
|
|
|
|
|
'Antworte mit dem einzelnen Wort "OK".',
|
|
|
|
|
);
|
|
|
|
|
return res.json(
|
|
|
|
|
TestProviderResponse.parse({
|
|
|
|
|
ok: true,
|
|
|
|
|
message: `Verbindung erfolgreich. Antwort: ${reply.trim().slice(0, 80) || "(leer)"}`,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return res.json(
|
|
|
|
|
TestProviderResponse.parse({
|
|
|
|
|
ok: false,
|
|
|
|
|
message: err instanceof Error ? err.message : "Verbindung fehlgeschlagen.",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
SkillGuard: complete frontend wiring and harden backend
Original task: build "SkillGuard", a German web app to audit agent skills on
two axes (IT-Sicherheit, Datenschutz) with static rule engine + Replit-independent
AI analysis configured via an admin backend.
This session:
- Fixed frontend TS errors: lucide-react name collisions (Badge from ui, Activity
from lucide), widened apiType to AiProviderApiType, added queryKey to useGetScan.
- Verified all pages render in German (Dashboard, Prüfen, Bericht, Verlauf, Admin)
and the full scan flow works end-to-end (malicious sample -> verdict block).
Code-review-driven hardening:
- POST /api/scans now returns the full ScanDetail (files + findings) to match the
OpenAPI contract, instead of only the summary.
- AI provider error bodies are redacted (token, Bearer, sk- patterns) before being
returned/persisted, and provider fetches now have a 60s timeout.
- ZIP parsing now enforces limits (max files, total + per-file size) to mitigate
zip-bomb DoS.
Updated replit.md (project overview, decisions, gotchas) and added a memory note
on lucide-react icon name collisions.
2026-06-08 14:59:17 +00:00
|
|
|
export default router;
|