feat: add sha256 and pgp verification
This commit is contained in:
parent
a155a9b355
commit
799cf0611c
12 changed files with 433 additions and 67 deletions
2
migrations/0002_naive_revanche.sql
Normal file
2
migrations/0002_naive_revanche.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE `cv` ADD `sha256` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `cv` ADD `pgp_signature` text;
|
||||||
92
migrations/meta/0002_snapshot.json
Normal file
92
migrations/meta/0002_snapshot.json
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "5f7163dc-e8ca-4bd2-ab41-6b244d02aaf7",
|
||||||
|
"prevId": "a68f3965-0dd6-46c6-af01-4f96061e8b11",
|
||||||
|
"tables": {
|
||||||
|
"cv": {
|
||||||
|
"name": "cv",
|
||||||
|
"columns": {
|
||||||
|
"uuid": {
|
||||||
|
"name": "uuid",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"company_name": {
|
||||||
|
"name": "company_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"name": "created",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "author",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"purpose": {
|
||||||
|
"name": "purpose",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tooling": {
|
||||||
|
"name": "tooling",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
"sha256": {
|
||||||
|
"name": "sha256",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pgp_signature": {
|
||||||
|
"name": "pgp_signature",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,13 @@
|
||||||
"when": 1735735966188,
|
"when": 1735735966188,
|
||||||
"tag": "0001_confused_wendell_rand",
|
"tag": "0001_confused_wendell_rand",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1735887287143,
|
||||||
|
"tag": "0002_naive_revanche",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="/admin/api/verifications"
|
|
||||||
class="flex flex-col gap-4 max-w-lg mx-auto p-6 rounded-lg shadow-md
|
class="flex flex-col gap-4 max-w-lg mx-auto p-6 rounded-lg shadow-md
|
||||||
bg-white dark:bg-gray-800 transition-colors"
|
bg-white dark:bg-gray-800 transition-colors"
|
||||||
>
|
>
|
||||||
|
|
@ -69,7 +68,7 @@
|
||||||
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
|
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
|
||||||
focus:outline-none
|
focus:outline-none
|
||||||
placeholder:text-gray-400 dark:placeholder:text-gray-500"
|
placeholder:text-gray-400 dark:placeholder:text-gray-500"
|
||||||
>Application
|
>Job Application
|
||||||
</textarea>
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
101
src/components/verification/AddHashAndSig.astro
Normal file
101
src/components/verification/AddHashAndSig.astro
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
---
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
|
||||||
|
const { uid } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="max-w-lg mx-auto p-6 rounded-lg shadow-md bg-white dark:bg-gray-800 transition-colors mt-8"
|
||||||
|
>
|
||||||
|
<h2 class="text-2xl font-bold mb-4 text-gray-800 dark:text-white">
|
||||||
|
Generated UID
|
||||||
|
</h2>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<code
|
||||||
|
class="uid-code bg-gray-50 dark:bg-gray-900 px-3 py-2 rounded-md text-gray-900 dark:text-white flex-grow font-mono"
|
||||||
|
>
|
||||||
|
{uid}
|
||||||
|
</code>
|
||||||
|
<button
|
||||||
|
class="copy-button p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white
|
||||||
|
bg-gray-50 dark:bg-gray-900 rounded-md
|
||||||
|
transition-colors duration-200
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||||
|
title="Copy to clipboard"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:content-copy" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-200 mb-4 mt-6">
|
||||||
|
Embed this uid in the CV typst document. After that, calculate the sha256sum
|
||||||
|
and enter it below along with your GPG signature
|
||||||
|
</p>
|
||||||
|
<form method="POST" class="flex flex-col gap-4">
|
||||||
|
<input type="hidden" name="uid" value={uid} />
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="sha256" class="font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
SHA256 Hash
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="sha256"
|
||||||
|
name="sha256"
|
||||||
|
required
|
||||||
|
class="border rounded-md p-2
|
||||||
|
bg-gray-50 dark:bg-gray-900
|
||||||
|
text-gray-900 dark:text-white
|
||||||
|
border-gray-300 dark:border-gray-600
|
||||||
|
focus:border-blue-500 dark:focus:border-blue-400
|
||||||
|
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
|
||||||
|
focus:outline-none
|
||||||
|
placeholder:text-gray-400 dark:placeholder:text-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="gpg" class="font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
PGP Signature
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="pgp_signature"
|
||||||
|
name="pgp_signature"
|
||||||
|
required
|
||||||
|
rows="4"
|
||||||
|
placeholder="Paste your PGP signature here"
|
||||||
|
class="border rounded-md p-2
|
||||||
|
bg-gray-50 dark:bg-gray-900
|
||||||
|
text-gray-900 dark:text-white
|
||||||
|
border-gray-300 dark:border-gray-600
|
||||||
|
focus:border-blue-500 dark:focus:border-blue-400
|
||||||
|
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
|
||||||
|
focus:outline-none
|
||||||
|
placeholder:text-gray-400 dark:placeholder:text-gray-500
|
||||||
|
resize-y"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600
|
||||||
|
text-white py-2 px-4 rounded-md
|
||||||
|
transition-colors duration-200
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Submit Hash and Signature
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const buttons = document.querySelectorAll(".copy-button");
|
||||||
|
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
const uid = document.querySelectorAll(".uid-code")[0]?.textContent;
|
||||||
|
if (uid) {
|
||||||
|
await navigator.clipboard.writeText(uid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
import type { InferSelectModel } from "drizzle-orm";
|
import type { InferSelectModel } from "drizzle-orm";
|
||||||
import type { cvTable } from "../../db/schema";
|
import type { cvTable } from "../../db/schema";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cv: InferSelectModel<typeof cvTable>;
|
cv: InferSelectModel<typeof cvTable>;
|
||||||
|
|
@ -9,7 +10,7 @@ interface Props {
|
||||||
const { cv } = Astro.props;
|
const { cv } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-6">
|
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-6 mb-4">
|
||||||
<ul class="space-y-4 mb-0">
|
<ul class="space-y-4 mb-0">
|
||||||
<li class="flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-2">
|
<li class="flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-2">
|
||||||
<span
|
<span
|
||||||
|
|
@ -61,5 +62,131 @@ const { cv } = Astro.props;
|
||||||
{cv.purpose}
|
{cv.purpose}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-2">
|
||||||
|
<span
|
||||||
|
class="text-gray-500 dark:text-gray-400 sm:min-w-32 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
SHA256:
|
||||||
|
</span>
|
||||||
|
<span class="text-gray-800 dark:text-gray-200 font-medium break-all">
|
||||||
|
{cv.sha256}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<pre
|
||||||
|
id="signature"
|
||||||
|
class="p-6 rounded-lg overflow-x-auto text-sm font-mono whitespace-pre-wrap mb-0">{cv.pgp_signature}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<details class="group bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
|
<summary class="flex items-center justify-between p-4 cursor-pointer">
|
||||||
|
<h3
|
||||||
|
class="flex items-center text-xl font-semibold text-gray-900 dark:text-gray-100 mb-0"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:information-outline" class="mr-2" />
|
||||||
|
|
||||||
|
Verification Instructions
|
||||||
|
</h3>
|
||||||
|
<span class="relative ml-4 flex-shrink-0">
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-gray-500 dark:text-gray-400 transform group-open:rotate-180 transition-transform"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="p-6 pt-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-xl font-bold mb-2 text-gray-800 dark:text-gray-200">
|
||||||
|
1. Verify SHA256 Hash
|
||||||
|
</h4>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
Compare the output of following command with the SHA256 hash shown
|
||||||
|
above.
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 p-3 rounded text-sm font-mono">shasum -a 256 cv.pdf</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="text-xl font-bold mb-2 text-gray-800 dark:text-gray-200">
|
||||||
|
2. Verify PGP Signature
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<ol
|
||||||
|
class="list-decimal list-inside space-y-2 text-gray-600 dark:text-gray-400 mb-2"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
Download Alexander Daichendt's public pgp key from <a
|
||||||
|
href="https://daichendt.one/pub.key"
|
||||||
|
class="text-blue-500 dark:text-blue-400 hover:underline">here</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Import the key into your keyring with
|
||||||
|
<pre
|
||||||
|
class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-sm font-mono sm:ml-6 mb-2">gpg --import ~/Downloads/pub.key</pre>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Download the above PGP signature by clicking <button
|
||||||
|
class="text-blue-500 dark:text-blue-400 hover:underline"
|
||||||
|
id="download-pgp"
|
||||||
|
>here
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>Run the following command:</li>
|
||||||
|
</li>
|
||||||
|
<pre
|
||||||
|
class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-sm font-mono sm:ml-6">gpg --verify signature.asc cv.pdf</pre>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mt-2">
|
||||||
|
If the verification is successful, GPG will indicate so.
|
||||||
|
<pre
|
||||||
|
class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-xs font-mono mt-2">gpg: Good signature from "Alexander Daichendt <alexander@daichendt.one>" [ultimate]</pre>
|
||||||
|
</p>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
details summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const pgpElements = document.querySelectorAll("#download-pgp");
|
||||||
|
|
||||||
|
pgpElements.forEach((pgpElement) => {
|
||||||
|
pgpElement.addEventListener("click", () => {
|
||||||
|
console.log("Downloading PGP signature");
|
||||||
|
|
||||||
|
const pgpSignature = document.getElementById("signature")?.innerText;
|
||||||
|
if (!pgpSignature) {
|
||||||
|
console.error("No PGP signature found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blob = new Blob([pgpSignature], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "signature.asc";
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@
|
||||||
d="M6 18L18 6M6 6l12 12"></path>
|
d="M6 18L18 6M6 6l12 12"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-bold text-red-600 dark:text-red-400">Revoked CV</h2>
|
<h2 class="text-2xl font-bold text-red-600 dark:text-red-400 mb-0">
|
||||||
|
Revoked CV
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-red-600 dark:text-red-400 text-lg">
|
<p class="text-red-600 dark:text-red-400 text-lg">
|
||||||
This CV has been revoked and is no longer valid.
|
This CV has been revoked and is no longer valid.
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const { cv } = Astro.props;
|
||||||
d="M5 13l4 4L19 7"></path>
|
d="M5 13l4 4L19 7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100">
|
<h2 class="text-3xl font-bold text-gray-800 dark:text-gray-100 mb-0">
|
||||||
Verified CV
|
Verified CV
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,6 @@ export const cvTable = sqliteTable("cv", {
|
||||||
status: text("status", {
|
status: text("status", {
|
||||||
enum: ["active", "revoked"],
|
enum: ["active", "revoked"],
|
||||||
}).default("active"),
|
}).default("active"),
|
||||||
|
sha256: text("sha256"),
|
||||||
|
pgp_signature: text("pgp_signature"),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import type { APIContext } from "astro";
|
|
||||||
import { drizzle } from "drizzle-orm/d1";
|
|
||||||
import { cvTable } from "../../../../db/schema";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export const prerender = false;
|
|
||||||
|
|
||||||
export async function POST(context: APIContext) {
|
|
||||||
const runtime = context.locals.runtime;
|
|
||||||
const d1 = runtime.env.DB;
|
|
||||||
const db = drizzle(d1, { schema: { cvTable } });
|
|
||||||
|
|
||||||
// parse form data
|
|
||||||
const formData = await context.request.formData();
|
|
||||||
const company_name = formData.get("company_name") as string;
|
|
||||||
const author = formData.get("author") as string;
|
|
||||||
const purpose = formData.get("purpose") as string;
|
|
||||||
const tooling = formData.get("tooling") as string;
|
|
||||||
const created = new Date();
|
|
||||||
|
|
||||||
let uuid;
|
|
||||||
let existing;
|
|
||||||
let maxAttempts = 10;
|
|
||||||
|
|
||||||
do {
|
|
||||||
uuid = nanoid(8);
|
|
||||||
|
|
||||||
// check if uuid already exists
|
|
||||||
existing = await db
|
|
||||||
.select()
|
|
||||||
.from(cvTable)
|
|
||||||
.where(eq(cvTable.uuid, uuid))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
maxAttempts--;
|
|
||||||
|
|
||||||
// Safety check to prevent infinite loops
|
|
||||||
if (maxAttempts <= 0) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: "Failed to generate unique UUID after multiple attempts",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} while (existing.length > 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db
|
|
||||||
.insert(cvTable)
|
|
||||||
.values({ company_name, author, purpose, tooling, created, uuid })
|
|
||||||
.execute();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return new Response(JSON.stringify({ success: false, error }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(JSON.stringify({ success: true, uuid }));
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,104 @@
|
||||||
---
|
---
|
||||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||||
import AddCVVerification from "../../components/verification/AddCVVerification.astro";
|
import AddCVVerification from "../../components/verification/AddCVVerification.astro";
|
||||||
|
import { cvTable } from "../../db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { drizzle } from "drizzle-orm/d1";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import AddHashAndSig from "../../components/verification/AddHashAndSig.astro";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
let uid = undefined;
|
||||||
|
|
||||||
|
if (Astro.request.method === "POST") {
|
||||||
|
try {
|
||||||
|
const data = await Astro.request.formData();
|
||||||
|
const company_name = data.get("company_name")?.toString();
|
||||||
|
const author = data.get("author")?.toString();
|
||||||
|
const purpose = data.get("purpose")?.toString();
|
||||||
|
const tooling = data.get("tooling")?.toString();
|
||||||
|
|
||||||
|
const _uid = data.get("uid")?.toString();
|
||||||
|
const sha256 = data.get("sha256")?.toString();
|
||||||
|
const pgp_signature = data.get("pgp_signature")?.toString();
|
||||||
|
|
||||||
|
const d1 = Astro.locals.runtime.env.DB;
|
||||||
|
const db = drizzle(d1, { schema: { cvTable } });
|
||||||
|
|
||||||
|
if (company_name && author && purpose && tooling) {
|
||||||
|
const created = new Date();
|
||||||
|
|
||||||
|
let uuid: string;
|
||||||
|
let existing;
|
||||||
|
let maxAttempts = 10;
|
||||||
|
|
||||||
|
do {
|
||||||
|
uuid = nanoid(8);
|
||||||
|
|
||||||
|
// check if uuid already exists
|
||||||
|
existing = await db
|
||||||
|
.select()
|
||||||
|
.from(cvTable)
|
||||||
|
.where(eq(cvTable.uuid, uuid))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
maxAttempts--;
|
||||||
|
|
||||||
|
// Safety check to prevent infinite loops
|
||||||
|
if (maxAttempts <= 0) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Failed to generate unique UUID after multiple attempts",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} while (existing.length > 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.insert(cvTable)
|
||||||
|
.values({
|
||||||
|
company_name,
|
||||||
|
author,
|
||||||
|
purpose,
|
||||||
|
tooling,
|
||||||
|
created,
|
||||||
|
uuid,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return new Response(JSON.stringify({ success: false, error }));
|
||||||
|
}
|
||||||
|
|
||||||
|
uid = uuid;
|
||||||
|
} else if (_uid && sha256) {
|
||||||
|
// Update the record with the sha256sum
|
||||||
|
try {
|
||||||
|
console.log(pgp_signature);
|
||||||
|
await db
|
||||||
|
.update(cvTable)
|
||||||
|
.set({ sha256, pgp_signature })
|
||||||
|
.where(eq(cvTable.uuid, _uid))
|
||||||
|
.execute();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return new Response(JSON.stringify({ success: false, error }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Missing data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<h1>Admin</h1>
|
<h1>Admin</h1>
|
||||||
|
|
||||||
<AddCVVerification />
|
<AddCVVerification />
|
||||||
|
{uid && <AddHashAndSig uid={uid} />}
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ const cv = id
|
||||||
cv.length === 0 ? (
|
cv.length === 0 ? (
|
||||||
<NoCV />
|
<NoCV />
|
||||||
) : cv[0].status !== "active" ? (
|
) : cv[0].status !== "active" ? (
|
||||||
<div class="text-center p-8 bg-red-50 dark:bg-red-950 rounded-lg shadow-sm">
|
<div class="text-center p-1 sm:p-8 bg-red-50 dark:bg-red-950 rounded-lg shadow-sm">
|
||||||
<Revoked />
|
<Revoked />
|
||||||
<DataTable cv={cv[0]} />
|
<DataTable cv={cv[0]} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-lg">
|
<div class="p-6 sm:p-8 bg-white dark:bg-gray-800 rounded-lg shadow-lg">
|
||||||
<Verified cv={cv[0]} />
|
<Verified cv={cv[0]} />
|
||||||
<DataTable cv={cv[0]} />
|
<DataTable cv={cv[0]} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue