add webfinger and routes boilerplate
This commit is contained in:
		
							parent
							
								
									cad4c59d51
								
							
						
					
					
						commit
						9f74db9857
					
				
					 8 changed files with 589 additions and 17 deletions
				
			
		|  | @ -9,3 +9,10 @@ export const DATABASE_PORT = Number(process.env.DATABASE_PORT) || 5432; | |||
| export const DATABASE_USER = process.env.DATABASE_USER || "postgres"; | ||||
| export const DATABASE_PASS = process.env.DATABASE_PASS || "postgres"; | ||||
| export const DATABASE_NAME = process.env.DATABASE_NAME || "postgres"; | ||||
| 
 | ||||
| export const HTTPS = process.env.HTTPS === "true"; | ||||
| export const DOMAIN = process.env.DOMAIN; | ||||
| 
 | ||||
| if (!DOMAIN) throw "$DOMAIN is empty"; | ||||
| 
 | ||||
| export const BASE_URL = `${HTTPS ? "https" : "http"}://${DOMAIN}`; | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import * as crypto from "node:crypto"; | ||||
| import * as util from "node:util"; | ||||
| import { promisify }from "node:util"; | ||||
| 
 | ||||
| const generate = util.promisify(crypto.generateKeyPair); | ||||
| const generate = promisify(crypto.generateKeyPair); | ||||
| 
 | ||||
| export async function generateKeyPair() { | ||||
|   return await generate("rsa", { | ||||
|  |  | |||
							
								
								
									
										31
									
								
								src/routes.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/routes.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| import { dirname, join } from "node:path"; | ||||
| import { fileURLToPath } from "node:url"; | ||||
| import { glob } from "glob"; | ||||
| import type { FastifyInstance, RouteOptions } from "fastify"; | ||||
| 
 | ||||
| import log from "./log.js"; | ||||
| 
 | ||||
| export default async function getRoutes() { | ||||
|   const rootDir = dirname(fileURLToPath(import.meta.url)); | ||||
| 
 | ||||
|   const routes: RouteOptions[] = []; | ||||
|   const matches = await glob(join(rootDir, "/routes/**/*.{js,ts}")); | ||||
| 
 | ||||
|   for (const filename of matches) { | ||||
|     try { | ||||
|       const mod = await import(filename); | ||||
|       routes.push(mod.default as RouteOptions); | ||||
|     } catch (e) { | ||||
|       log.error("Importing route %s", filename, e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return routes; | ||||
| } | ||||
| 
 | ||||
| export function mountRoutes(app: FastifyInstance, routes: RouteOptions[]) { | ||||
|   for (const route of routes) { | ||||
|     log.trace("Mounting route %s %s", route.method, route.url); | ||||
|     app.route(route); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										80
									
								
								src/routes/well-known/webfinger.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/routes/well-known/webfinger.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,80 @@ | |||
| import type { FastifyReply, FastifyRequest, RouteOptions } from "fastify"; | ||||
| import { IsNull } from "typeorm"; | ||||
| 
 | ||||
| import log from "../../log.js"; | ||||
| import MercuryDataSource from "../../db/index.js"; | ||||
| import { Blog } from "../../db/entities/blog.js"; | ||||
| import { BASE_URL } from "../../config.js"; | ||||
| 
 | ||||
| const route: RouteOptions = { | ||||
|   method: "GET", | ||||
|   url: "/.well-known/webfinger", | ||||
|   handler: async (req, res) => { | ||||
|     // TypeScript complains if we just use plain `req.query` :(
 | ||||
|     const encodedResource = (req.query as { resource: string }).resource; | ||||
| 
 | ||||
|     if (!encodedResource || typeof encodedResource !== "string") { | ||||
|       res.status(400).send({ | ||||
|         error: "resource query parameter is missing or invalid", | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     const resource = decodeURIComponent(encodedResource); | ||||
| 
 | ||||
|     log.debug("Handling WebFinger request for %s", resource); | ||||
| 
 | ||||
|     if (resource.startsWith("acct:")) { | ||||
|       await handleAcctWebfinger(req, res, resource); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     res.status(500).send({ error: "Unhandled WebFinger resource type" }); | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export default route; | ||||
| 
 | ||||
| async function handleAcctWebfinger( | ||||
|   req: FastifyRequest, | ||||
|   res: FastifyReply, | ||||
|   resource: string | ||||
| ): Promise<void> { | ||||
|   const [username, domain] = resource.slice("acct:".length).split("@"); | ||||
|   if (domain !== process.env.DOMAIN) { | ||||
|     res.status(404).send({ | ||||
|       error: "Account not found", | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   const blog = await MercuryDataSource.getRepository(Blog).findOneBy({ | ||||
|     username: username, | ||||
|     host: IsNull(), | ||||
|   }); | ||||
|   if (!blog) { | ||||
|     res.status(404).send({ | ||||
|       error: "Account not found", | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   res.send({ | ||||
|     subject: resource, | ||||
|     aliases: [ | ||||
|       `${BASE_URL}/@${blog.username}`, | ||||
|       `${BASE_URL}/blogs/${blog.username}`, | ||||
|     ], | ||||
|     links: [ | ||||
|       { | ||||
|         rel: "http://webfinger.net/rel/profile-page", | ||||
|         type: "text/html", | ||||
|         href: `${BASE_URL}/@${blog.username}`, | ||||
|       }, | ||||
|       { | ||||
|         rel: "self", | ||||
|         type: "application/activity+json", | ||||
|         href: `${BASE_URL}/blogs/${blog.username}`, | ||||
|       }, | ||||
|     ], | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										22
									
								
								src/start.ts
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								src/start.ts
									
										
									
									
									
								
							|  | @ -1,9 +1,10 @@ | |||
| import "reflect-metadata"; // Required for TypeORM
 | ||||
| import express from "express"; | ||||
| import Fastify from "fastify"; | ||||
| 
 | ||||
| import MercuryDataSource from "./db/index.js"; | ||||
| import log from "./log.js"; | ||||
| import { Blog } from "./db/entities/blog.js"; | ||||
| import { PORT } from "./config.js"; | ||||
| import getRoutes, { mountRoutes } from "./routes.js"; | ||||
| 
 | ||||
| export default async function start() { | ||||
|   log.info("Initializing database"); | ||||
|  | @ -17,17 +18,12 @@ export default async function start() { | |||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   log.debug("Setting up routes") | ||||
|   const app = express(); | ||||
|   log.debug("Setting up routes"); | ||||
|   const app = Fastify(); | ||||
| 
 | ||||
|   app.get("/", async (_, res) => { | ||||
|     const blogRepository = MercuryDataSource.getRepository(Blog); | ||||
|   const routes = await getRoutes(); | ||||
|   mountRoutes(app, routes); | ||||
| 
 | ||||
|     const resp = await blogRepository.find(); | ||||
| 
 | ||||
|     return res.json(resp) | ||||
|   }) | ||||
| 
 | ||||
|   log.info("Listening on port %d", PORT) | ||||
|   app.listen(PORT); | ||||
|   log.info("Listening on port %d", PORT); | ||||
|   await app.listen({ port: PORT }); | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue