progress-tracker-api-v2/src/server.ts
2025-04-06 02:11:42 +02:00

153 lines
4.5 KiB
TypeScript

import { env } from "bun";
import homepage from './index.html';
import robotsTxt from './static/robots.txt';
import sitemapTxt from './static/sitemap.txt';
// @ts-ignore ts2307
import icon from './static/favicon.png' with { type: 'file' };
import { entry } from "./db/schema";
import { desc, eq } from "drizzle-orm";
const favicon = await Bun.file(icon).bytes();
const development = env.NODE_ENV !== 'production';
Bun.serve({
routes: {
'/': homepage,
'/robots.txt': new Response(robotsTxt, {
headers: { 'Content-Type': 'text/plain' },
}),
'/sitemap.txt': new Response(sitemapTxt, {
headers: { 'Content-Type': 'text/plain' },
}),
'/favicon.ico': new Response(favicon, {
headers: { 'Content-Type': 'image/png' },
}),
'/health': new Response('OK'),
'/api/entries': {
async GET(req) {
if (!isAuthenticated(req)) return unauthorizedResp();
const entries = await drizzleDB.select().from(entry).orderBy(desc(entry.updatedAt));
return new Response(JSON.stringify(entries), {
headers: { 'Content-Type': 'application/json' }
});
},
async POST(req) {
if (!isAuthenticated(req)) return unauthorizedResp();
const body = await getEntryFromReq(req);
if (!body) return new Response('Invalid data', { status: 400 });
const result = await drizzleDB.select({ id: entry.id }).from(entry).where(eq(entry.name, body.name));
let created = true;
if (result.length === 0) {
await drizzleDB.insert(entry).values(body).execute();
} else if (result.length === 1) {
const row = result[0];
created = false;
await drizzleDB.update(entry).set(body).where(eq(entry.id, row.id)).execute();
} else {
return new Response('Invalid data, multiple matches?', { status: 400 });
}
return new Response(JSON.stringify({ created }), {
headers: { 'Content-Type': 'application/json' }
});
},
},
'/api/entries/:id': {
async GET(req) {
if (!isAuthenticated(req)) return unauthorizedResp();
const id = Number.parseInt(req.params.id, 10);
if (Number.isNaN(id)) return new Response('Invalid id', { status: 400 });
const result = await drizzleDB.select().from(entry).where(eq(entry.id, id));
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' }
});
},
async PUT(req) {
if (!isAuthenticated(req)) return unauthorizedResp();
const id = Number.parseInt(req.params.id, 10);
if (Number.isNaN(id)) return new Response('Invalid id', { status: 400 });
const body = await getEntryFromReq(req);
if (!body) return new Response('Invalid data', { status: 400 });
await drizzleDB.update(entry).set(body).where(eq(entry.id, id)).execute();
let created = false;
return new Response(JSON.stringify({ created: false }), {
headers: { 'Content-Type': 'application/json' }
});
},
async DELETE(req) {
if (!isAuthenticated(req)) return unauthorizedResp();
const id = Number.parseInt(req.params.id, 10);
if (Number.isNaN(id)) return new Response('Invalid id', { status: 400 });
await drizzleDB.delete(entry).where(eq(entry.id, id)).execute();
return new Response('OK');
},
},
'/api/entries/:id/check/:finished': {
async PUT(req) {
if (!isAuthenticated(req)) return unauthorizedResp();
const id = Number.parseInt(req.params.id, 10);
if (Number.isNaN(id)) return new Response('Invalid id', { status: 400 });
const finished = req.params.finished === 'true';
await drizzleDB
.update(entry)
.set({ finished: finished })
.where(eq(entry.id, id))
.execute();
return new Response('OK');
},
},
},
development,
reusePort: true,
port: env.PORT || 3000,
// async fetch(req, server) {
// return new Response("Not found", { status: 404 });
// },
});
async function getEntryFromReq(req: Request) {
const json = await req.json();
const body = json as { name: string, href: string }
if (!body.name || !body.href || typeof body.name !== 'string' || typeof body.href !== 'string') {
return null;
}
// Trim and lowercase the name to ensure consistency and uniqueness
body.name = body.name.trim().toLocaleLowerCase();
return body;
}
function isAuthenticated(req: Request) {
const bearer = req.headers.get('bearer');
if (!bearer) return false;
return bearer === env.BEARER_TOKEN;
}
const unauthorizedHeaders = new Headers({
'WWW-Authenticate': `Bearer realm='sign', error="invalid_request"`
});
function unauthorizedResp() {
return new Response('Unauthorized', { status: 401, headers: unauthorizedHeaders });
}