summaryrefslogtreecommitdiff
path: root/src/components/templates/Authors.astro
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/templates/Authors.astro')
-rw-r--r--src/components/templates/Authors.astro355
1 files changed, 355 insertions, 0 deletions
diff --git a/src/components/templates/Authors.astro b/src/components/templates/Authors.astro
new file mode 100644
index 0000000..61ca026
--- /dev/null
+++ b/src/components/templates/Authors.astro
@@ -0,0 +1,355 @@
+---
+import { type RevocationReason, toPK } from "@lib/pgp";
+import type { Verification } from "@lib/pgp/verify";
+import { defined, get } from "@utils/anonymous";
+import type { getSigners } from "@lib/collection/helpers";
+import type { PublicKey, UserIDPacket } from "openpgp";
+import {
+ createVerificationSummary,
+ VerificationResult,
+} from "@lib/pgp/summary";
+import Date from "@components/organisms/Date.astro";
+
+interface Props {
+ verifications: NonNullable<Verification["verifications"]>;
+ expectedSigners: Map<
+ string,
+ {
+ signer: Awaited<ReturnType<typeof getSigners>>[number];
+ users: UserIDPacket[];
+ key: PublicKey;
+ }
+ >;
+ commitSignerKey?: string;
+}
+
+const {
+ verifications: verificationsPromise,
+ expectedSigners,
+ commitSignerKey,
+} = Astro.props;
+
+let verifications = await Promise.all(
+ verificationsPromise.map(async (verification) => {
+ const { key, keyID, userID, verified } = verification;
+ return {
+ key: await key,
+ keyID,
+ userID: await userID,
+ verified: await verified.catch(() => false),
+ summary: await createVerificationSummary(verification),
+ };
+ }),
+);
+
+const expectedKeys = Array.from(expectedSigners.values()).map(get("key"));
+
+const expectedFingerprints = new Set(
+ expectedKeys.map((key) => key.getFingerprint()),
+);
+
+const verifiedFingerprints = new Set(
+ verifications.map((v) => v.key).filter(defined).map(toPK).map((key) =>
+ key.getFingerprint()
+ ),
+);
+
+if (!expectedFingerprints.isSubsetOf(verifiedFingerprints)) {
+ throw new Error(
+ `Missing signature from expected signers: ${[
+ ...expectedFingerprints.difference(verifiedFingerprints).values(),
+ ]}`,
+ );
+}
+---
+
+<div>
+ <table>
+ <caption>
+ <strong>Assinaturas</strong>
+ <p>
+ Para verificar uma assinatura é necessário a <a href="#message"
+ >mensagem</a>, a <a href="#signature">assinatura digital</a> e as <em
+ >chaves públicas</em> dos assinantes. Esta tabela mostra algumas
+ informações sobre os assinantes, as suas chaves públicas e as suas
+ assinaturas.
+ </p>
+ </caption>
+ <colgroup>
+ <col />
+ <col />
+ </colgroup>
+ <colgroup>
+ <col />
+ <col />
+ <col />
+ </colgroup>
+ <colgroup>
+ <col />
+ </colgroup>
+ <thead>
+ <tr>
+ <th scope="col">Assinante</th>
+ <th scope="col">Função</th>
+ <th scope="col">Fingerprint</th>
+ <th scope="col">Válido</th>
+ <th scope="col">Commiter</th>
+ <th scope="col">Mais Informações</th>
+ </tr>
+ </thead>
+ <tbody>
+ {
+ verifications.map(
+ ({ userID, key, keyID, verified, summary }) => {
+ const fingerprint = key
+ ? toPK(key).getFingerprint()
+ : undefined;
+ const info = fingerprint
+ ? expectedSigners.get(fingerprint)
+ : undefined;
+ const primary = userID?.[0];
+ const signer = info?.signer;
+ let role = "";
+ switch (signer?.role) {
+ case "author": {
+ role = "Autor";
+ break;
+ }
+ case "co-author": {
+ role = "Co-autor";
+ break;
+ }
+ case "translator": {
+ role = "Tradutor";
+ break;
+ }
+ }
+ const {
+ reasons,
+ created,
+ expired,
+ revoked,
+ revocationReason,
+ trusted,
+ } = (summary[1].get(keyID.toHex()) ?? []).reduce(
+ (acc, x) => {
+ if (!("key" in x || "keyID" in x)) {
+ return acc;
+ }
+
+ switch (x.result) {
+ case VerificationResult.MISSING_KEY:
+ acc.reasons.push(x.reason);
+ acc.created = x.created ?? undefined;
+ break;
+ case VerificationResult.UNTRUSTED_KEY:
+ acc.created = x.created ?? undefined;
+ acc.trusted &&= false;
+ break;
+ case VerificationResult.TRUSTED_KEY:
+ acc.created = x.created ?? undefined;
+ acc.trusted = true;
+ break;
+ case VerificationResult
+ .EXPIRATION_AFTER_SIGNATURE:
+ acc.created = x.created ?? undefined;
+ acc.expired = x.expired;
+ break;
+ case VerificationResult
+ .EXPIRATION_BEFORE_SIGNATURE:
+ acc.created = x.created ?? undefined;
+ acc.expired = x.expired;
+ break;
+ case VerificationResult
+ .REVOCATION_AFTER_SIGNATURE:
+ acc.created = x.created ?? undefined;
+ acc.revoked = x.revoked;
+ acc.revocationReason = x.revocationReason;
+ break;
+ case VerificationResult
+ .REVOCATION_BEFORE_SIGNATURE:
+ acc.created = x.created ?? undefined;
+ acc.revoked = x.revoked;
+ acc.revocationReason = x.revocationReason;
+ break;
+ case VerificationResult.KEY_DOES_NOT_SIGN:
+ break;
+ }
+
+ return acc;
+ },
+ {
+ reasons: [],
+ created: undefined,
+ expired: undefined,
+ revoked: undefined,
+ revocationReason: undefined,
+ trusted: false,
+ } as {
+ reasons: Error[];
+ created?: Date;
+ expired?: Date;
+ revoked?: Date;
+ revocationReason?: RevocationReason;
+ trusted: boolean;
+ },
+ );
+ return (
+ <tr>
+ <th scope="row">
+ <address
+ itemprop="author"
+ itemscope
+ itemtype="https://schema.org/Person"
+ >
+ {
+ primary?.name
+ ? signer?.entity?.data?.websites[0]
+ ? (
+ <a
+ itemprop="url"
+ rel="author external noreferrer"
+ target="_blank"
+ href={signer?.entity?.data?.websites[0]}
+ ><span itemprop="name">{primary.name}</span></a>
+ )
+ : <span itemprop="name">{primary.name}</span>
+ : primary?.email
+ ? (
+ <>&lt;<a
+ itemprop="email"
+ rel="author external noreferrer"
+ target="_blank"
+ href={primary?.email && `mailto:${primary.email}`}
+ >{primary?.email}</a>&gt;</>
+ )
+ : ""
+ }
+ </address>
+ {signer !== undefined && <><hr /><a href="#">Ver perfil</a></>}
+ </th>
+ <td>{role}</td>
+ <td>
+ <samp title={fingerprint?.replace(/(....)/g, "$1 ")}>{
+ key
+ ? "0x" + toPK(key).getKeyID().toHex()
+ : "0x" + keyID.toHex()
+ }</samp>
+ </td>
+ <td>{verified ? "✅" : "❌"}</td>
+ <td>
+ {
+ commitSignerKey &&
+ key?.getFingerprint().toUpperCase()?.endsWith(
+ commitSignerKey.toUpperCase(),
+ ) && "✅"
+ }
+ </td>
+ <td>
+ {
+ key && (
+ <>
+ <button
+ type="button"
+ class="emoji"
+ popovertarget={`info-${fingerprint}`}
+ >
+ ➕
+ </button>
+ <section popover id={`info-${fingerprint}`}>
+ <p>Erros</p>
+ {
+ summary[0].map((x) => {
+ if (!("reason" in x)) {
+ return undefined;
+ }
+
+ let reason = x.reason;
+
+ return <pre>{reason.message}</pre>;
+ })
+ }
+ <p><strong>Informações</strong></p>
+ <dl class="divider">
+ {
+ reasons.map(({ message }) => (
+ <> <dt>Erro</dt> <dd>{message}</dd></>
+ ))
+ }
+ {
+ created && (
+ <>
+ <dt>Data da assinatura</dt>
+ <dd><Date date={created} options={{}} /></dd>
+ </>
+ )
+ }
+ {
+ expired && (
+ <>
+ <dt>Data de expiração</dt>
+ <dd><Date date={expired} options={{}} /></dd>
+ </>
+ )
+ }
+ {
+ revoked && (
+ <>
+ <dt>Data de revogação</dt>
+ <dd><Date date={revoked} options={{}} /></dd>
+ </>
+ )
+ }
+ {
+ revocationReason && (
+ <>
+ <dt>Razão para a revogação</dt>
+ <dd>
+ {revocationReason.flag}: {revocationReason.msg}
+ </dd>
+ </>
+ )
+ }
+ <dt>Chave confiável</dt>
+ <dd>{trusted ? "✅" : "❌"}</dd>
+ </dl>
+ </section></>
+ )
+ }
+ </td>
+ </tr>
+ );
+ },
+ )
+ }
+ </tbody>
+ </table>
+</div>
+
+<style>
+ div {
+ overflow-x: auto;
+ }
+
+ table {
+ table-layout: fixed;
+ border-collapse: collapse;
+ border: 3px solid;
+ margin-inline: auto;
+ max-width: 90svw;
+ }
+
+ th,
+ td {
+ padding: 1rem;
+ text-align: center;
+ }
+
+ tbody tr:nth-child(odd) {
+ background-color: oklch(from var(--color-light) l c h / calc(alpha * 0.25));
+ }
+
+ section[popover] {
+ text-align: initial;
+ }
+</style>