WIP Resolve username via npub, LDAP

This commit is contained in:
Râu Cao 2024-10-20 22:52:47 +02:00
parent b618c6a1a1
commit 9a19f7249c
Signed by: raucao
GPG Key ID: 37036C356E56CC51
10 changed files with 248 additions and 13 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.env

View File

@ -1,12 +1,14 @@
{
"tasks": {
"dev": "deno run --allow-net --deny-env --watch main.ts"
"dev": "deno run --allow-net --allow-read --allow-env --deny-env --watch main.ts"
},
"imports": {
"@deno/gfm": "jsr:@deno/gfm@^0.9.0",
"@nostr/tools": "jsr:@nostr/tools@^2.3.1",
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.1",
"@oak/oak": "jsr:@oak/oak@^17.1.0",
"@std/assert": "jsr:@std/assert@1"
"@std/assert": "jsr:@std/assert@1",
"@std/dotenv": "jsr:@std/dotenv@^0.225.2",
"ldapts": "npm:ldapts@^7.2.1"
}
}

78
deno.lock generated
View File

@ -12,6 +12,7 @@
"jsr:@std/bytes@1": "1.0.2",
"jsr:@std/bytes@^1.0.2": "1.0.2",
"jsr:@std/crypto@1": "1.0.3",
"jsr:@std/dotenv@~0.225.2": "0.225.2",
"jsr:@std/encoding@1": "1.0.5",
"jsr:@std/encoding@^1.0.5": "1.0.5",
"jsr:@std/encoding@~0.224.1": "0.224.3",
@ -29,6 +30,7 @@
"npm:github-slugger@2": "2.0.0",
"npm:he@^1.2.0": "1.2.0",
"npm:katex@0.16": "0.16.11",
"npm:ldapts@^7.2.1": "7.2.1",
"npm:lru-cache@^10.2.0": "10.2.2",
"npm:marked-alert@2": "2.1.0_marked@12.0.2",
"npm:marked-footnote@^1.2.0": "1.2.4_marked@12.0.2",
@ -123,6 +125,9 @@
"@std/crypto@1.0.3": {
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
},
"@std/dotenv@0.225.2": {
"integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23"
},
"@std/encoding@0.224.3": {
"integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf"
},
@ -218,9 +223,33 @@
"@scure/base@1.1.7"
]
},
"@types/asn1@0.2.4": {
"integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==",
"dependencies": [
"@types/node"
]
},
"@types/node@22.5.4": {
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dependencies": [
"undici-types"
]
},
"asn1@0.2.6": {
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": [
"safer-buffer"
]
},
"commander@8.3.0": {
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
},
"debug@4.3.7": {
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": [
"ms"
]
},
"deepmerge@4.3.1": {
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
},
@ -279,6 +308,17 @@
"commander"
]
},
"ldapts@7.2.1": {
"integrity": "sha512-2NSA9drjHdRiApF+TO18c+Hy/uyBLs96OS6Gia4+dPQWPxvqDbu3Ji2beCbNCXTvvgxDj4cLZ0WoOZLt5ojfAg==",
"dependencies": [
"@types/asn1",
"asn1",
"debug",
"strict-event-emitter-types",
"uuid",
"whatwg-url"
]
},
"lru-cache@10.2.2": {
"integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ=="
},
@ -304,6 +344,9 @@
"marked@12.0.2": {
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q=="
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"nanoid@3.3.7": {
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
},
@ -342,6 +385,12 @@
"prismjs@1.29.0": {
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="
},
"punycode@2.3.1": {
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
},
"safer-buffer@2.1.2": {
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sanitize-html@2.13.1": {
"integrity": "sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==",
"dependencies": [
@ -356,9 +405,34 @@
"source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
"strict-event-emitter-types@2.0.0": {
"integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA=="
},
"tr46@5.0.0": {
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
"dependencies": [
"punycode"
]
},
"undici-types@6.19.8": {
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"uuid@10.0.0": {
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
},
"webidl-conversions@7.0.0": {
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
},
"websocket-ts@2.1.5": {
"integrity": "sha512-rCNl9w6Hsir1azFm/pbjBEFzLD/gi7Th5ZgOxMifB6STUfTSovYAzryWw0TRvSZ1+Qu1Z5Plw4z42UfTNA9idA=="
},
"whatwg-url@14.0.0": {
"integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
"dependencies": [
"tr46",
"webidl-conversions"
]
},
"zod@3.23.8": {
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="
}
@ -381,7 +455,9 @@
"jsr:@nostr/tools@^2.3.1",
"jsr:@nostrify/nostrify@~0.36.1",
"jsr:@oak/oak@^17.1.0",
"jsr:@std/assert@1"
"jsr:@std/assert@1",
"jsr:@std/dotenv@~0.225.2",
"npm:ldapts@^7.2.1"
]
}
}

View File

@ -1,4 +1,4 @@
import { ctx } from "@oak/oak";
import { Context } from "@oak/oak";
import { nip19 } from "@nostr/tools";
import { log } from "../log.ts";
import { articleHtml } from "../html.ts"
@ -7,12 +7,12 @@ import {
fetchProfileEvent
} from "../nostr.ts";
const naddrHandler = async function (ctx: ctx) {
const naddrHandler = async function (ctx: Context) {
const { request } = ctx;
const { path } = ctx.params;
const naddr = ctx.params.path;
try {
const r = nip19.decode(path);
const r = nip19.decode(naddr);
const articleEvent = await fetchReplaceableEvent(r.data.pubkey, r.data.identifier);
const profileEvent = await fetchProfileEvent(r.data.pubkey);
let profile;

View File

@ -1,10 +1,30 @@
import { Context } from "@oak/oak";
import { nip19 } from "@nostr/tools";
import { log } from "../log.ts";
import { fetchProfileEvent } from "../nostr.ts";
import { profilePageHtml } from "../html.ts"
const nprofileHandler = function (context: Context) {
const { request, response } = context;
const fullPath = request.url.pathname;
const nprofileHandler = async function (ctx: Context) {
const { request } = ctx;
const nprofile = ctx.params.path;
response.body = `You are viewing an nprofile with address: ${fullPath}`;
try {
const r = nip19.decode(nprofile);
const profileEvent = await fetchProfileEvent(r.data.pubkey);
if (profileEvent) {
const html = profilePageHtml(profileEvent);
ctx.response.body = html;
} else {
ctx.response.status = 404;
ctx.response.body = "Not Found";
}
} catch (e) {
log(e, "yellow");
ctx.response.status = 404;
ctx.response.body = "Not Found";
}
};
export default nprofileHandler;

30
handlers/npub.ts Normal file
View File

@ -0,0 +1,30 @@
import { Context } from "@oak/oak";
import { nip19 } from "@nostr/tools";
import { log } from "../log.ts";
import { fetchProfileEvent } from "../nostr.ts";
import { profilePageHtml } from "../html.ts"
const npubHandler = async function (ctx: Context) {
const { request } = ctx;
const npub = ctx.params.path;
try {
const r = nip19.decode(npub);
const profileEvent = await fetchProfileEvent(r.data);
if (profileEvent) {
const html = profilePageHtml(profileEvent);
ctx.response.body = html;
} else {
ctx.response.status = 404;
ctx.response.body = "Not Found";
}
} catch (e) {
log(e, "yellow");
ctx.response.status = 404;
ctx.response.body = "Not Found";
}
};
export default npubHandler;

31
handlers/username.ts Normal file
View File

@ -0,0 +1,31 @@
import { Context } from "@oak/oak";
import { nip19 } from "@nostr/tools";
import { log } from "../log.ts";
import { lookupPubkeyByUsername } from "../ldap.ts";
import { fetchProfileEvent } from "../nostr.ts";
import { profilePageHtml } from "../html.ts"
const usernameHandler = async function (ctx: Context) {
const { request } = ctx;
const username = ctx.params.path.replace(/^@/, '');;
const pubkey = await lookupPubkeyByUsername(username);
try {
const profileEvent = await fetchProfileEvent(pubkey);
if (profileEvent) {
const html = profilePageHtml(profileEvent);
ctx.response.body = html;
} else {
ctx.response.status = 404;
ctx.response.body = "Not Found";
}
} catch (e) {
log(e, "yellow");
ctx.response.status = 404;
ctx.response.body = "Not Found";
}
};
export default usernameHandler;

34
html.ts
View File

@ -31,6 +31,11 @@ export function htmlLayout(title: string, body: string) {
border-radius: 50%;
}
.profile-page img.avatar {
height: 128px;
width: 128px;
}
a.anchor {
display: none;
}
@ -143,3 +148,32 @@ export function articleHtml(articleEvent: object, profile: object) {
return htmlLayout(title, body);
}
export function profilePageHtml(profileEvent: object) {
const profile = JSON.parse(profileEvent.content);
const name = profile.name || "Anonymous";
const title = `${name} on Nostr`
// const date = new Date(articleEvent.created_at * 1000);
// const formattedDate = date.toLocaleDateString("en-US", {
// year: "numeric",
// month: "long",
// day: "numeric",
// });
const body = `
<main class="profile-page">
<header>
<img class="avatar" src="${profile.picture}" alt="User Avatar" />
<div class="bio">
<h1>${profile.name}</h1>
<p class="about">
${profile.about}
</p>
</div>
</header>
<h2>Articles</h2>
</main>
`;
return htmlLayout(title, body);
}

35
ldap.ts Normal file
View File

@ -0,0 +1,35 @@
import { load } from "@std/dotenv";
import { Client } from 'ldapts';
import { log } from "./log.ts";
const dirname = new URL('.', import.meta.url).pathname;
await load({ envPath: `${dirname}/.env`, export: true });
const config = {
url: Deno.env.get("LDAP_URL"),
bindDN: Deno.env.get("LDAP_BIND_DN"),
password: Deno.env.get("LDAP_PASSWORD"),
searchDN: Deno.env.get("LDAP_SEARCH_DN")
}
const client = new Client({ url: config.url });
export async function lookupPubkeyByUsername (username) {
let pubkey;
try {
await client.bind(config.bindDN, config.password);
const { searchEntries } = await client.search(config.searchDN, {
filter: `(cn=${username})`,
attributes: ['nostrKey']
});
pubkey = searchEntries[0]?.nostrKey;
} catch (ex) {
log(ex, "red");
} finally {
await client.unbind();
return pubkey;
}
}

10
main.ts
View File

@ -2,16 +2,22 @@ import { Application, Router } from "@oak/oak";
import { log } from "./log.ts";
import naddrHandler from "./handlers/naddr.ts";
import nprofileHandler from "./handlers/nprofile.ts";
import npubHandler from "./handlers/npub.ts";
import usernameHandler from "./handlers/username.ts";
const router = new Router();
router.get("/:path", async (ctx: ctx) => {
const { path } = ctx.params;
if (path && path.startsWith("naddr")) {
if (path.startsWith("naddr")) {
await naddrHandler(ctx);
} else if (prefix && prefix.startsWith("nprofile")) {
} else if (path.startsWith("nprofile")) {
await nprofileHandler(ctx);
} else if (path.startsWith("npub")) {
await npubHandler(ctx);
} else if (path.startsWith("@")) {
await usernameHandler(ctx);
} else {
ctx.response.status = 404;
ctx.response.body = "Not Found";