feat: add tor service and style

feat: add tor service
This commit is contained in:
Jet 2026-03-18 13:30:29 -07:00
parent f48390b15e
commit 7b842b3342
No known key found for this signature in database
12 changed files with 203 additions and 24 deletions

84
flake.lock generated
View file

@ -1,8 +1,53 @@
{
"nodes": {
"agenix": {
"inputs": {
"darwin": "darwin",
"home-manager": "home-manager",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems"
},
"locked": {
"lastModified": 1770165109,
"narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=",
"owner": "ryantm",
"repo": "agenix",
"rev": "b027ee29d959fda4b60b57566d64c98a202e0feb",
"type": "github"
},
"original": {
"owner": "ryantm",
"repo": "agenix",
"type": "github"
}
},
"darwin": {
"inputs": {
"nixpkgs": [
"agenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744478979,
"narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "43975d782b418ebf4969e9ccba82466728c2851b",
"type": "github"
},
"original": {
"owner": "lnl7",
"ref": "master",
"repo": "nix-darwin",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
@ -18,6 +63,27 @@
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"agenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1745494811,
"narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1772542754,
@ -52,6 +118,7 @@
},
"root": {
"inputs": {
"agenix": "agenix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
@ -89,6 +156,21 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View file

@ -4,8 +4,10 @@
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
agenix.url = "github:ryantm/agenix";
agenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
outputs = { self, nixpkgs, rust-overlay, flake-utils, agenix }:
(flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
@ -81,7 +83,9 @@
pname = "jetpham-qa-api";
version = "0.1.0";
src = ./api;
cargoHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
cargoHash = "sha256-/EgiCn5N3E1tCcBWI3Sm3NGQt2h8l8yPwi/ZjporiVs=";
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.openssl ];
};
in {

View file

@ -80,7 +80,7 @@
</head>
<body style="background:#000">
<canvas id="canvas" class="fixed top-0 left-0 -z-10 h-screen w-screen" aria-hidden="true"></canvas>
<nav class="flex justify-center px-4">
<nav aria-label="Main navigation" class="flex justify-center px-4">
<div class="relative px-[2ch] py-[1ch] mt-[2ch] w-full max-w-[66.666667%] min-w-fit">
<div
class="pointer-events-none absolute inset-0"
@ -88,9 +88,8 @@
aria-hidden="true"
></div>
<div class="absolute inset-0 border-2 border-white" aria-hidden="true"></div>
<div class="relative z-10 flex gap-[2ch]">
<div class="relative z-10 flex justify-center gap-[2ch]">
<a href="/">[HOME]</a>
<a href="/projects">[PROJECTS]</a>
<a href="/qa">[Q&A]</a>
</div>
</div>

View file

@ -16,6 +16,8 @@ in
description = "Domain to serve the website on.";
};
tor.enable = lib.mkEnableOption "Tor hidden service for the website";
envFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
@ -36,6 +38,55 @@ in
};
config = lib.mkIf cfg.enable {
age.secrets.webhook-secret = {
file = "${self}/secrets/webhook-secret.age";
mode = "0400";
};
age.secrets.tor-onion-secret-key = lib.mkIf cfg.tor.enable {
file = "${self}/secrets/tor-onion-secret-key.age";
owner = "tor";
group = "tor";
mode = "0400";
};
age.secrets.tor-onion-public-key = lib.mkIf cfg.tor.enable {
file = "${self}/secrets/tor-onion-public-key.age";
owner = "tor";
group = "tor";
mode = "0444";
};
age.secrets.tor-onion-hostname = lib.mkIf cfg.tor.enable {
file = "${self}/secrets/tor-onion-hostname.age";
owner = "tor";
group = "tor";
mode = "0444";
};
services.tor = lib.mkIf cfg.tor.enable {
enable = true;
relay.onionServices.jetpham-website = {
map = [{ port = 80; target = { addr = "127.0.0.1"; port = 8888; }; }];
};
};
systemd.services.tor-onion-keys = lib.mkIf cfg.tor.enable {
description = "Copy Tor onion keys into place";
after = [ "agenix.service" ];
before = [ "tor.service" ];
wantedBy = [ "tor.service" ];
serviceConfig.Type = "oneshot";
script = ''
dir="/var/lib/tor/onion/jetpham-website"
mkdir -p "$dir"
cp ${config.age.secrets.tor-onion-secret-key.path} "$dir/hs_ed25519_secret_key"
cp ${config.age.secrets.tor-onion-public-key.path} "$dir/hs_ed25519_public_key"
cp ${config.age.secrets.tor-onion-hostname.path} "$dir/hostname"
chown -R tor:tor "$dir"
chmod 700 "$dir"
chmod 400 "$dir/hs_ed25519_secret_key"
chmod 444 "$dir/hs_ed25519_public_key" "$dir/hostname"
'';
};
# Q&A API systemd service
systemd.services.jetpham-qa-api = {
description = "Jet Pham Q&A API";
@ -47,14 +98,11 @@ in
Environment = [ "QA_DB_PATH=/var/lib/jetpham-qa/qa.db" ];
Restart = "on-failure";
RestartSec = 5;
} // lib.optionalAttrs (cfg.webhookSecretFile == null) {
ExecStart = "${qaApi}/bin/jetpham-qa-api";
LoadCredential = "webhook-secret:${config.age.secrets.webhook-secret.path}";
} // lib.optionalAttrs (cfg.envFile != null) {
EnvironmentFile = cfg.envFile;
} // lib.optionalAttrs (cfg.webhookSecretFile != null) {
LoadCredential = "webhook-secret:${cfg.webhookSecretFile}";
};
script = lib.mkIf (cfg.webhookSecretFile != null) ''
script = ''
export WEBHOOK_SECRET="$(cat $CREDENTIALS_DIRECTORY/webhook-secret)"
exec ${qaApi}/bin/jetpham-qa-api
'';
@ -76,5 +124,22 @@ in
}
'';
};
services.caddy.virtualHosts."http://127.0.0.1:8888" = lib.mkIf cfg.tor.enable {
extraConfig = ''
header Cross-Origin-Opener-Policy "same-origin"
header Cross-Origin-Embedder-Policy "require-corp"
handle /api/* {
reverse_proxy 127.0.0.1:3001
}
handle {
root * ${package}
try_files {path} /index.html
file_server
}
'';
};
};
}

10
secrets.nix Normal file
View file

@ -0,0 +1,10 @@
let
laptop = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu";
server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB";
in
{
"secrets/tor-onion-secret-key.age".publicKeys = [ laptop server ];
"secrets/tor-onion-public-key.age".publicKeys = [ laptop server ];
"secrets/tor-onion-hostname.age".publicKeys = [ laptop server ];
"secrets/webhook-secret.age".publicKeys = [ laptop server ];
}

Binary file not shown.

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 Ziw7aw 2u0CVE/rQWpJNSRW/1xeWBKpShWT4ckke+Ih4j3WbRk
xeE2xTlSPEDPeC4BNkaSoOckwuOhyCQWqtXkwuhBiRo
-> ssh-ed25519 uKftJg Tt1mbTWHyXRDjvGWFBqmyrMl/PtUs45N1032luY88x8
A51wD3tiZ0lV1TSub+Pz7hZ+kndiEpnmBliP59qYzkY
--- 7X+mgLxb3uYfiYebJnAUwl/4jhGJJSweaolMttmoEIQ
3à‡’Í.Úu˼ÞZ"Àä9Ö<39>'AëâªK~.@ŽÌ‰7¾lW„÷ pý{CôÁAŠ&@¸|ÕËšV%55˜Ot¼Šr9Çîºúñ<C3BA>h»L²‡@®GõLä˜gû€

View file

@ -0,0 +1,8 @@
age-encryption.org/v1
-> ssh-ed25519 Ziw7aw AZTbqWYmTaudHZ8PiTZlwpf7VzwaP921guVV1iQi8WM
7CGUyEjZoAPCBX2pqNHLd2P1KLnj/Y5nnBVToWYCjWg
-> ssh-ed25519 uKftJg RfAkte/jcx+/SvZiUNH07cnBJcvl0Sjt7zSxdSCsXE0
TdMsQ2u5WXw3KAi7Tk4JOdbiFStT8F88xjDlRN8LH2Q
--- pgQnGRRVjVK02tbMgDoh3SatJFxxFLazqy5ieHu96tk
žÞxó}ûë¥T¡½<11>P^©kH ×/æôœõþݳU~ókÅ;Ö•äÕ†=•ÁºNå«\PZï|óO¨/Y íV6Ìx=„ ¹¹Â<gR+2á³C5n
©ãÃàÚ/J…k¤;5ÚNÈ—Ü#q&Øxš|­6²ƒŸ>H

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 Ziw7aw zDI2q+wM70YbQFw7hEQbU58XrgkWnM8fura+ovG0bTw
0Aznts9gOT8ADxFY0SIv/VT0ExEEq9wZLTnlC0j15JU
-> ssh-ed25519 uKftJg dFQxnrMwQHJ2lVBMi6P4XugljREjAHPV9bEFmLhuiBc
VWKLaM3peiELoJigapdZv0N4Z4lRZZ8eGMEFy+WtdxA
--- zJghsuTUoVH8bKynqq29kzEuNDuEjvFf5v/s98jQdaA
𡂝濮+<2B>i籤V2 *H儑齪<E58491><E9BDAA> c栛

View file

@ -2,14 +2,10 @@ import "~/styles/globals.css";
import init, { start } from "cgol";
import { route, initRouter } from "~/router";
import { homePage } from "~/pages/home";
import { projectsPage } from "~/pages/projects";
import { projectPage } from "~/pages/project";
import { qaPage } from "~/pages/qa";
import { notFoundPage } from "~/pages/not-found";
route("/", homePage);
route("/projects", projectsPage);
route("/projects/:slug", projectPage);
route("/qa", qaPage);
route("*", notFoundPage);

View file

@ -11,14 +11,13 @@ export async function qaPage(outlet: HTMLElement) {
outlet.innerHTML = `
<div class="flex flex-col items-center justify-start px-4">
${frostedBox(`
<h1 style="color: var(--yellow);">Q&A</h1>
<form id="qa-form" class="mt-[2ch]">
<fieldset class="border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0">
<form id="qa-form">
<fieldset class="border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-[1ch]">
<legend class="-mx-[0.5ch] px-[0.5ch] text-white">Ask a Question</legend>
<textarea id="qa-input" maxlength="200" rows="3"
class="qa-textarea"
placeholder="Type your question..."></textarea>
<div class="flex justify-between mt-[0.5ch]">
<div class="flex justify-between mt-[1ch]">
<span id="char-count" style="color: var(--dark-gray);">0/200</span>
<button type="submit" class="qa-button">[SUBMIT]</button>
</div>
@ -83,5 +82,6 @@ export async function qaPage(outlet: HTMLElement) {
} catch {
list.textContent = "Failed to load questions.";
list.style.color = "var(--light-red)";
list.style.textAlign = "center";
}
}

View file

@ -80,14 +80,15 @@
}
a:hover {
color: var(--blue);
background-color: var(--light-blue);
color: var(--black);
}
/* Form inputs */
.qa-textarea {
width: 100%;
background-color: var(--black);
border: 1px solid var(--white);
border: 2px solid var(--white);
color: var(--light-gray);
padding: 1ch;
resize: none;
@ -97,8 +98,8 @@
}
.qa-button {
border: 1px solid var(--white);
padding: 0 1ch;
border: none;
padding: 0.25ch 1ch;
color: var(--yellow);
background: transparent;
font-family: inherit;
@ -139,7 +140,7 @@
}
.project-content pre {
border: 1px solid var(--dark-gray);
border: 2px solid var(--dark-gray);
padding: 1ch;
overflow-x: auto;
margin-bottom: 1ch;