157 lines
5.2 KiB
TypeScript
157 lines
5.2 KiB
TypeScript
import homepage from '@routes/index.html';
|
|
import { drizzleDB } from "@server/db";
|
|
import { entry } from "@server/db/schema";
|
|
import favicon from '@static/favicon.png';
|
|
import robotsTxt from '@static/robots.txt';
|
|
import sitemapTxt from '@static/sitemap.txt';
|
|
import { env } from "bun";
|
|
import { desc, eq } from "drizzle-orm";
|
|
|
|
const development = env.NODE_ENV !== 'production';
|
|
|
|
const faviconInit = { headers: new Headers({ 'Content-Type': 'image/png' }) };
|
|
|
|
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, faviconInit),
|
|
'/health': new Response('OK'),
|
|
'/api/entries': {
|
|
async GET(req) {
|
|
if (!isAuthenticated(req)) return unauthorizedResp();
|
|
|
|
const entries = await drizzleDB.select({ id: entry.id, name: entry.name, href: entry.href, finished: entry.finished, updated_at: entry.updatedAt }).from(entry).orderBy(desc(entry.updatedAt));
|
|
|
|
return new Response(JSON.stringify(entries), {
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
},
|
|
async PUT(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] as NonNullable<typeof result[number]>;
|
|
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) {
|
|
// console.log(req);
|
|
// return new Response("Not found", { status: 404 });
|
|
// },
|
|
});
|
|
|
|
console.log('Server started on port:', env.PORT ? Number.parseInt(env.PORT, 10) : 3000);
|
|
|
|
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 auth = req.headers.get('authorization');
|
|
if (!auth) return false;
|
|
const [type, bearer] = auth.split(' ');
|
|
if (type !== 'Bearer') return false;
|
|
// Check if the token is valid
|
|
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 });
|
|
}
|