Files
qbit-secured-data/src/main/java/com/kingsrook/qbits/secureddata/app/SecuredDataHomeView.java
T
james.maesandClaude Opus 5.5 470b0bd214 feat: retention and wipe with crypto-shredding; move to QQQ RC.6 (phase f)
Moves the pin to QQQ 4.1.0-RC.6 and removes the stand-ins RC.6 provides
(classification, client channels, table features, the UI API extension
point). Adds retention rules, wipe markers, legal holds, wipe runs and the
per-record-per-field audit key table; the wipe process (null with marker,
overwrite, keyed hash, delete, archive then wipe) runs in one transaction
per batch, destroying the audit keys with the field updates; the audit
protector and key source (AES-GCM wrapped DEKs, AAD-bound); the backups
warning; canary and wipe-proof tests.

Fixes a key-source bug that zeroed each DEK after returning it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:35:45 -05:00

106 lines
5.1 KiB
Java

/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.secureddata.app;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Inline styles survive Next's sanitizer. Full-width labelled card rows wrap at
** narrow widths; no inner scrollers. Inherited host text and theme border tokens.
*******************************************************************************/
public final class SecuredDataHomeView
{
private static final DateTimeFormatter WHEN = DateTimeFormatter.ofPattern("MMM d, HH:mm 'UTC'", Locale.ENGLISH).withZone(ZoneOffset.UTC);
private static final String GRID = "display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,10rem),1fr));gap:.75rem;min-width:0;";
private static final String CARD = "border:1px solid var(--qqq-grid-border,currentColor);border-radius:.5rem;padding:1rem;min-width:0;";
/*******************************************************************************
** Utility class.
*******************************************************************************/
private SecuredDataHomeView()
{
}
/*******************************************************************************
** KPI row followed by six full-width tables, never side by side.
*******************************************************************************/
public static String render(SecuredDataHomeData data)
{
StringBuilder html = new StringBuilder("<div style=\"display:flex;flex-direction:column;gap:1rem;min-width:0;color:inherit\">");
html.append("<div aria-label=\"Secured data metrics\" style=\"").append(GRID).append("\">");
data.getKpis().forEach((label, value) -> html.append("<div style=\"").append(CARD)
.append("\"><div style=\"font-size:1.6rem;font-weight:600\">").append(value)
.append("</div><div>").append(esc(label)).append("</div></div>"));
html.append("</div>");
for(String warning : data.getBackupWarnings())
{
html.append("<p role='alert'>").append(esc(warning)).append("</p>");
}
for(SecuredDataHomeData.Section section : data.getSections())
{
html.append("<section style=\"").append(CARD).append("\"><h3 style=\"margin:0;font-size:1rem\">")
.append(esc(section.title())).append("</h3><p><a style=\"color:inherit\" aria-label=\"View all ")
.append(esc(section.title())).append("\" href=\"")
.append(esc(section.href())).append("\">View all</a></p>");
if(section.rows().isEmpty())
{
html.append("<p>").append(esc(section.empty())).append("</p>");
}
for(QRecord row : section.rows())
{
html.append("<div style=\"").append(GRID).append("border-top:1px solid var(--qqq-grid-border,currentColor);padding:.75rem 0\">");
for(int i = 0; i < section.fields().size(); i++)
{
String field = section.fields().get(i);
String value = row.getValueString(field);
boolean date = "created_at".equals(field);
if(date && row.getValueInstant(field) != null)
{
value = WHEN.format(row.getValueInstant(field));
}
if(value == null || value.isBlank())
{
value = "—";
}
html.append("<div style=\"min-width:0\"><span style=\"display:block;font-size:.75rem;opacity:.7\">")
.append(esc(section.labels().get(i))).append("</span><span title=\"").append(esc(value))
.append("\" style=\"display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\">")
.append(esc(value)).append("</span></div>");
}
html.append("</div>");
}
html.append("</section>");
}
return (html.append("</div>").toString());
}
/*******************************************************************************
** Escape every text, tooltip and URL attribute.
*******************************************************************************/
private static String esc(String value)
{
return (value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;"));
}
}