diff options
Diffstat (limited to 'src/components/templates/Authors.astro')
-rw-r--r-- | src/components/templates/Authors.astro | 355 |
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 + ? ( + <><<a + itemprop="email" + rel="author external noreferrer" + target="_blank" + href={primary?.email && `mailto:${primary.email}`} + >{primary?.email}</a>></> + ) + : "" + } + </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> |