148 lines
3.8 KiB
TypeScript
148 lines
3.8 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' };
|
|
const favicon = await Bun.file(icon).bytes();
|
|
const development = env.NODE_ENV !== 'production';
|
|
import { randomUUIDv7 } from 'bun';
|
|
import ptApi from './server/pt-api';
|
|
|
|
declare global {
|
|
var loginTokens: Set<string>;
|
|
}
|
|
|
|
if (!globalThis.loginTokens) {
|
|
globalThis.loginTokens = new Set<string>();
|
|
}
|
|
|
|
const authCookie = 'pt-auth';
|
|
|
|
let entriesCache = '';
|
|
let entriesCacheTime = 0;
|
|
const entriesCacheTimeLimit = 1000 * 15;
|
|
|
|
async function getEntriesCached() {
|
|
if (entriesCacheTime + entriesCacheTimeLimit < Date.now()) {
|
|
const data = await ptApi.query();
|
|
entriesCache = JSON.stringify(data);
|
|
entriesCacheTime = Date.now();
|
|
}
|
|
return entriesCache;
|
|
}
|
|
|
|
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 (!isLoggedIn(req)) return unauthorizedResp();
|
|
|
|
return new Response(await getEntriesCached(), {
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
},
|
|
},
|
|
'/auth/logout': {
|
|
async POST() {
|
|
return new Response('Logout successful', { headers: logoutHeaders() });
|
|
},
|
|
},
|
|
'/auth/login': {
|
|
async POST(req) {
|
|
const json = await req.json();
|
|
const data = json as Record<string, any>;
|
|
const email = data.email;
|
|
const password = data.password;
|
|
|
|
if (
|
|
typeof email !== 'string' ||
|
|
typeof password !== 'string' ||
|
|
email.length < 3 ||
|
|
password.length === 0
|
|
) {
|
|
return new Response('Missing email or password', { status: 400 });
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
if (email === env.EMAIL && password === env.PASSWORD) {
|
|
let token = randomUUIDv7('base64url');
|
|
while (globalThis.loginTokens.has(token)) {
|
|
// generate a new token if it already exists
|
|
// this is unlikely to happen, but just in case
|
|
token = randomUUIDv7();
|
|
}
|
|
|
|
const cookie = new Bun.Cookie({
|
|
name: authCookie,
|
|
value: token,
|
|
path: '/',
|
|
maxAge: 60 * 60 * 24 * 31,
|
|
secure: true,
|
|
sameSite: 'strict',
|
|
});
|
|
|
|
const headers = new Headers({ 'Set-Cookie': cookie.toString() });
|
|
|
|
globalThis.loginTokens.add(token);
|
|
|
|
setTimeout(getEntriesCached, 1);
|
|
|
|
return new Response('Login successful', { headers });
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 900));
|
|
|
|
return new Response('Incorrect email or password', {
|
|
status: 400,
|
|
});
|
|
},
|
|
},
|
|
},
|
|
development,
|
|
reusePort: true,
|
|
// async fetch(req, server) {
|
|
// return new Response("Not found", { status: 404 });
|
|
// },
|
|
});
|
|
|
|
function isLoggedIn(req: Request) {
|
|
const cookie = new Bun.CookieMap(req.headers.get('cookie') || '');
|
|
const token = cookie.get(authCookie);
|
|
|
|
if (!token) {
|
|
return false;
|
|
}
|
|
|
|
return globalThis.loginTokens.has(token);
|
|
}
|
|
|
|
function logoutHeaders() {
|
|
return new Headers({
|
|
'Set-Cookie': new Bun.Cookie({
|
|
name: authCookie,
|
|
path: '/',
|
|
maxAge: -1,
|
|
secure: true,
|
|
sameSite: 'strict',
|
|
}).toString(),
|
|
});
|
|
}
|
|
|
|
function unauthorizedResp() {
|
|
return new Response('Unauthorized', { status: 401, headers: logoutHeaders() });
|
|
}
|