feat: cv verifier

This commit is contained in:
Alexander Daichendt 2025-01-01 14:00:05 +01:00
parent 4dd699f08c
commit 194b4b0808
24 changed files with 2199 additions and 70 deletions

11
.prettierrc Normal file
View file

@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-astro"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

View file

@ -9,13 +9,17 @@ import tailwind from "@astrojs/tailwind";
import icon from "astro-icon"; import icon from "astro-icon";
import { remarkReadingTime } from "./src/remark/remark-reading-time"; import { remarkReadingTime } from "./src/remark/remark-reading-time";
import cloudflare from "@astrojs/cloudflare";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
prefetch: { prefetch: {
defaultStrategy: "hover", defaultStrategy: "hover",
prefetchAll: true, prefetchAll: true,
}, },
site: "https://daichendt.one", site: "https://daichendt.one",
integrations: [ integrations: [
mdx({ mdx({
remarkPlugins: [remarkEmoji, remarkReadingTime], remarkPlugins: [remarkEmoji, remarkReadingTime],
@ -24,4 +28,10 @@ export default defineConfig({
tailwind(), tailwind(),
icon(), icon(),
], ],
adapter: cloudflare({
platformProxy: {
enabled: true,
},
}),
}); });

24
drizzle.config.ts Normal file
View file

@ -0,0 +1,24 @@
import type { Config } from "drizzle-kit";
const { LOCAL_DB_PATH, DB_ID, D1_TOKEN, CF_ACCOUNT_ID } = process.env;
// Use better-sqlite driver for local development
export default LOCAL_DB_PATH
? ({
schema: "./src/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: LOCAL_DB_PATH,
},
} satisfies Config)
: ({
schema: "./src/db/schema.ts",
out: "./migrations",
dialect: "sqlite",
driver: "d1-http",
dbCredentials: {
databaseId: DB_ID!,
token: D1_TOKEN!,
accountId: CF_ACCOUNT_ID!,
},
} satisfies Config);

View file

@ -0,0 +1,8 @@
CREATE TABLE `cv` (
`uuid` text PRIMARY KEY NOT NULL,
`company_name` text NOT NULL,
`created` integer,
`author` text NOT NULL,
`created_for` text NOT NULL,
`status` text DEFAULT 'pending'
);

View file

@ -0,0 +1,15 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_cv` (
`uuid` text PRIMARY KEY NOT NULL,
`company_name` text NOT NULL,
`created` integer,
`author` text NOT NULL,
`purpose` text NOT NULL,
`tooling` text NOT NULL,
`status` text DEFAULT 'active'
);
--> statement-breakpoint
INSERT INTO `__new_cv`("uuid", "company_name", "created", "author", "purpose", "tooling", "status") SELECT "uuid", "company_name", "created", "author", "purpose", "tooling", "status" FROM `cv`;--> statement-breakpoint
DROP TABLE `cv`;--> statement-breakpoint
ALTER TABLE `__new_cv` RENAME TO `cv`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View file

@ -0,0 +1,71 @@
{
"version": "6",
"dialect": "sqlite",
"id": "ed238646-2f39-4026-8075-4ddf059cc6f7",
"prevId": "00000000-0000-0000-0000-000000000000",
"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
},
"created_for": {
"name": "created_for",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'pending'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -0,0 +1,80 @@
{
"version": "6",
"dialect": "sqlite",
"id": "a68f3965-0dd6-46c6-af01-4f96061e8b11",
"prevId": "ed238646-2f39-4026-8075-4ddf059cc6f7",
"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'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {
"\"cv\".\"created_for\"": "\"cv\".\"purpose\""
}
},
"internal": {
"indexes": {}
}
}

View file

@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1735731743025,
"tag": "0000_naive_tarantula",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1735735966188,
"tag": "0001_confused_wendell_rand",
"breakpoints": true
}
]
}

View file

@ -6,19 +6,29 @@
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro",
"db:generate": "drizzle-kit generate",
"db:migrate:local": "wrangler d1 migrations apply cv-verification --local",
"db:migrate:prod": "wrangler d1 migrations apply cv-verification --remote",
"db:migrate:preview": "wrangler d1 migrations apply --env preview d1-demo-preview-db --remote",
"db:studio:local": "LOCAL_DB_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) drizzle-kit studio",
"db:studio:preview": "source .drizzle.env && DB_ID='yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' drizzle-kit studio",
"db:studio:prod": "source .drizzle.env && DB_ID='ae0b9867-26f2-4a5b-8aa6-805a86792662' drizzle-kit studio"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/cloudflare": "^12.1.0",
"@astrojs/mdx": "^4.0.1", "@astrojs/mdx": "^4.0.1",
"@astrojs/rss": "^4.0.10", "@astrojs/rss": "^4.0.10",
"@astrojs/sitemap": "^3.2.1", "@astrojs/sitemap": "^3.2.1",
"@astrojs/tailwind": "^5.1.3", "@astrojs/tailwind": "^5.1.3",
"@cloudflare/workers-types": "^4.20241230.0",
"@fontsource/ubuntu": "^5.1.0", "@fontsource/ubuntu": "^5.1.0",
"@iconify-json/mdi": "^1.2.1", "@iconify-json/mdi": "^1.2.1",
"@iconify-json/simple-icons": "^1.2.14", "@iconify-json/simple-icons": "^1.2.14",
"astro": "^5.0.3", "astro": "^5.0.3",
"astro-icon": "^1.1.4", "astro-icon": "^1.1.4",
"drizzle-orm": "^0.38.3",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"sharp": "^0.33.5", "sharp": "^0.33.5",
@ -27,6 +37,12 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwind-plugin/expose-colors": "^1.1.8", "@tailwind-plugin/expose-colors": "^1.1.8",
"@types/node": "^22.10.3",
"better-sqlite3": "^11.7.0",
"cross-env": "^7.0.3",
"drizzle-kit": "^0.30.1",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.14.1",
"remark-emoji": "^5.0.1" "remark-emoji": "^5.0.1"
} }
} }

1737
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
---
import type { InferSelectModel } from "drizzle-orm";
import type { cvTable } from "../../db/schema";
interface Props {
cv: InferSelectModel<typeof cvTable>;
}
const { cv } = Astro.props;
---
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-6">
<ul class="space-y-4 mb-0">
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32"> UUID: </span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.uuid}
</span>
</li>
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32">
Issued by:
</span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.author}
</span>
</li>
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32">
Issued to:
</span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.company_name}
</span>
</li>
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32">
Issue date:
</span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.created?.toLocaleString()}
</span>
</li>
<li class="flex items-start">
<span class="text-gray-500 dark:text-gray-400 min-w-32"> Purpose: </span>
<span class="text-gray-800 dark:text-gray-200 font-medium">
{cv.purpose}
</span>
</li>
</ul>
</div>

View file

@ -0,0 +1,5 @@
<div class="text-center p-8 bg-red-50 dark:bg-red-950 rounded-lg shadow-sm">
<p class="text-red-600 dark:text-red-400 text-lg font-medium">
No CV found with this UUID.
</p>
</div>

View file

@ -0,0 +1,20 @@
<div class="flex items-center justify-center gap-2 mb-4">
<div class="bg-red-100 dark:bg-red-900 p-2 rounded-full">
<svg
class="w-6 h-6 text-red-600 dark:text-red-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-red-600 dark:text-red-400">Revoked CV</h2>
</div>
<p class="text-red-600 dark:text-red-400 text-lg">
This CV has been revoked and is no longer valid.
</p>

View file

@ -0,0 +1,36 @@
---
import type { InferSelectModel } from "drizzle-orm";
import type { cvTable } from "../../db/schema";
interface Props {
cv: InferSelectModel<typeof cvTable>;
}
const { cv } = Astro.props;
---
<div class="flex items-center gap-2 mb-6">
<div class="bg-green-100 dark:bg-green-900 p-2 rounded-full">
<svg
class="w-6 h-6 text-green-600 dark:text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100">
Verified CV
</h2>
</div>
<p class="text-gray-600 dark:text-gray-300 mb-6">
This CV was issued and verified by
<span class="font-semibold">{cv.author}</span>, for{" "}
<span class="font-semibold">{cv.company_name}</span>
</p>

15
src/db/schema.ts Normal file
View file

@ -0,0 +1,15 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const cvTable = sqliteTable("cv", {
uuid: text("uuid").primaryKey(),
company_name: text("company_name").notNull(),
created: integer("created", {
mode: "timestamp_ms",
}),
author: text("author").notNull(),
purpose: text("purpose").notNull(),
tooling: text("tooling").notNull(),
status: text("status", {
enum: ["active", "revoked"],
}).default("active"),
});

10
src/env.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
// src/env.d.ts
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
type Runtime = import("@astrojs/cloudflare").Runtime<Env>;
declare namespace App {
interface Locals extends Runtime {}
}

View file

@ -0,0 +1,39 @@
---
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/d1";
import { cvTable } from "../../db/schema";
import BaseLayout from "../../layouts/BaseLayout.astro";
import DataTable from "../../components/verification/DataTable.astro";
import NoCV from "../../components/verification/NoCV.astro";
import Verified from "../../components/verification/Verified.astro";
import Revoked from "../../components/verification/Revoked.astro";
export const prerender = false;
const uuid = Astro.url.searchParams.get("uuid");
const db = drizzle(Astro.locals.runtime.env.DB);
const cv = uuid
? await db.select().from(cvTable).where(eq(cvTable.uuid, uuid))
: [];
---
<BaseLayout title="CV Verification">
<section class="max-w-4xl mx-auto min-h-screen">
{
cv.length === 0 ? (
<NoCV />
) : cv[0].status !== "active" ? (
<div class="text-center p-8 bg-red-50 dark:bg-red-950 rounded-lg shadow-sm">
<Revoked />
<DataTable cv={cv[0]} />
</div>
) : (
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-lg">
<Verified cv={cv[0]} />
<DataTable cv={cv[0]} />
</div>
)
}
</section>
</BaseLayout>

View file

@ -4,7 +4,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
--- ---
<BaseLayout> <BaseLayout>
<h1>Hi, my name is Alex!</h1> <h1 class="text-4xl sm:text-5xl">Hi, my name is Alex!</h1>
<p> <p>
I am a software engineer, Linux enthusiast and a friend of lightweight, I am a software engineer, Linux enthusiast and a friend of lightweight,
resilient systems. resilient systems.

View file

@ -5,15 +5,15 @@
*/ */
body { body {
font-family: "Ubuntu"; font-family: "Ubuntu";
margin: 0; margin: 0;
padding: 0; padding: 0;
text-align: left; text-align: left;
background-size: 100% 600px; background-size: 100% 600px;
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
font-size: 20px; font-size: 20px;
line-height: 1.7; line-height: 1.7;
} }
h1, h1,
@ -22,83 +22,83 @@ h3,
h4, h4,
h5, h5,
h6 { h6 {
margin: 0.5rem 0 0.5rem 0; margin: 0.5rem 0 0.5rem 0;
line-height: 1.2; line-height: 1.2;
} }
h1 { h1 {
font-size: 3.052em; font-size: 3.052em;
} }
h2 { h2 {
font-size: 2.441em; font-size: 2.441em;
} }
h3 { h3 {
font-size: 1.953em; font-size: 1.953em;
} }
h4 { h4 {
font-size: 1.563em; font-size: 1.563em;
} }
h5 { h5 {
font-size: 1.25em; font-size: 1.25em;
} }
strong, strong,
b { b {
font-weight: 700; font-weight: 700;
} }
p { p {
margin-bottom: 0.75em; margin-bottom: 0.75em;
} }
.prose p { .prose p {
margin-bottom: 2em; margin-bottom: 2em;
} }
textarea { textarea {
width: 100%; width: 100%;
font-size: 16px; font-size: 16px;
} }
input { input {
font-size: 16px; font-size: 16px;
} }
table { table {
width: 100%; width: 100%;
} }
img { img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
border-radius: 8px; border-radius: 8px;
} }
code { code {
padding: 2px 5px; padding: 2px 5px;
background-color: rgb(var(--gray-light)); background-color: rgb(var(--gray-light));
border-radius: 2px; border-radius: 2px;
} }
pre { pre {
padding: 0.5em; padding: 0.5em;
border-radius: 8px; border-radius: 8px;
margin-bottom: 2em; margin-bottom: 2em;
} }
pre > code { pre > code {
all: unset; all: unset;
} }
blockquote { blockquote {
border-left: 4px solid var(--accent); border-left: 4px solid var(--accent);
padding: 0 0 0 20px; padding: 0 0 0 20px;
margin: 0px; margin: 0px;
font-size: 1.333em; font-size: 1.333em;
} }
hr { hr {
border: none; border: none;
border-top: 1px solid rgb(var(--gray-light)); border-top: 1px solid rgb(var(--gray-light));
} }
ul:not(ul ul, ol ul), ul:not(ul ul, ol ul),
ol:not(ul ol, ol ol) { ol:not(ul ol, ol ol) {
margin-bottom: 2em; margin-bottom: 2em;
} }
.footnotes { .footnotes {
margin-left: 2rem; margin-left: 2rem;
} }
.footnotes ol { .footnotes ol {
list-style: outside; list-style: outside;
} }
.footnotes li { .footnotes li {
margin-top: -3rem; margin-top: -3rem;
} }

5
worker-configuration.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
// Generated by Wrangler by running `wrangler types`
interface Env {
DB: D1Database;
}

4
wrangler.toml Normal file
View file

@ -0,0 +1,4 @@
[[d1_databases]]
binding = "DB"
database_name = "cv-verification"
database_id = "ae0b9867-26f2-4a5b-8aa6-805a86792662"