WIP Resolve username via npub, LDAP
This commit is contained in:
parent
b618c6a1a1
commit
9a19f7249c
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.env
|
@ -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
78
deno.lock
generated
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
30
handlers/npub.ts
Normal 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
31
handlers/username.ts
Normal 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
34
html.ts
@ -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
35
ldap.ts
Normal 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
10
main.ts
@ -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";
|
||||
|
Loading…
x
Reference in New Issue
Block a user