feat: implement authentication routes with login, logout, and user info retrieval

feat: add S3 bucket versioning support and related XML response handling

refactor: rename temporary file paths from 'teleuploader' to 'filedrop' for consistency

fix: update Swagger documentation to reflect new API name and descriptions

test: add unit tests for authentication routes and utilities

test: implement end-to-end tests for S3 bucket configuration and versioning

chore: update environment variable defaults for new service name
This commit is contained in:
asepharyana
2026-07-07 19:58:15 +07:00
parent 144ebe6dd3
commit 340c12d671
28 changed files with 898 additions and 52 deletions
+64
View File
@@ -0,0 +1,64 @@
import { config } from '../env';
import {
checkBearerToken,
clearSessionCookie,
createSessionCookie,
getAuthSession,
isAuthEnabled,
timingSafeCompare,
} from '../utils/auth';
const json = (data: unknown, status = 200, headers: Record<string, string> = {}): Response =>
Response.json(data, { status, headers });
const notFound = (): Response => json({ error: 'Not found' }, 404);
const readLoginBody = async (req: Request): Promise<{ token: string } | null> => {
try {
const body = (await req.json()) as { token?: unknown };
if (typeof body.token !== 'string' || body.token.length === 0) return null;
return { token: body.token };
} catch {
return null;
}
};
export const handleLogin = async (req: Request): Promise<Response> => {
if (!isAuthEnabled()) return notFound();
const body = await readLoginBody(req);
if (!body) return json({ error: 'Token is required' }, 400);
if (!timingSafeCompare(body.token, config.adminApiToken)) {
return json({ error: 'Invalid token' }, 401);
}
return json({ username: 'admin' }, 200, {
'set-cookie': createSessionCookie('admin'),
});
};
export const handleLogout = async (): Promise<Response> =>
json({ success: true }, 200, {
'set-cookie': clearSessionCookie(),
});
export const handleMe = async (req: Request): Promise<Response> => {
if (!isAuthEnabled()) return notFound();
const session = getAuthSession(req);
if (!session && !checkBearerToken(req.headers.get('authorization'))) {
return json({ error: 'Unauthorized' }, 401);
}
const activeSession = session ?? {
username: 'admin',
expiresAt: null,
method: 'bearer' as const,
};
return json({
username: activeSession.username,
expiresAt: activeSession.expiresAt?.toISOString() ?? null,
});
};
+1 -1
View File
@@ -74,7 +74,7 @@ export const handleFileRedirect = async (req: RequestWithParams): Promise<Respon
return fail(500, 'Server error');
}
const tempZipPath = `/tmp/teleuploader-dl-${nanoid()}.zip`;
const tempZipPath = `/tmp/filedrop-dl-${nanoid()}.zip`;
await Bun.write(tempZipPath, archiveResponse);
const loc = await locateZipEntry(tempZipPath, archiveEntryName);
+22 -2
View File
@@ -26,6 +26,7 @@ import { S3_CORS_HEADERS, s3Headers } from '../utils/s3/headers';
import { createGetObjectResponse, type ObjectPartSource } from '../utils/s3/object-stream';
import { parseRangeHeader, unsatisfiedContentRange } from '../utils/s3/range';
import {
bucketVersioningConfigurationXml,
completeMultipartUploadXml,
copyObjectResultXml,
deleteResultXml,
@@ -147,6 +148,9 @@ export const handleS3Request = async (
// Bucket-level operations
if (!key) {
if (method === 'GET') {
if (searchParams.has('versioning')) {
return handleGetBucketVersioning(bucket, reqId);
}
if (searchParams.has('uploads')) {
return handleListMultipartUploads(bucket, searchParams, reqId);
}
@@ -291,6 +295,22 @@ const handleDeleteBucket = async (bucketName: string, reqId: string): Promise<Re
return s3Response(null, 204, reqId);
};
const handleGetBucketVersioning = async (bucketName: string, reqId: string): Promise<Response> => {
const bucket = await findBucketByName(bucketName);
if (!bucket) {
return s3ErrorResponse(
'NoSuchBucket',
'The specified bucket does not exist.',
`/${bucketName}`,
404,
reqId,
);
}
return s3Response(bucketVersioningConfigurationXml(), 200, reqId, {
'content-type': 'application/xml',
});
};
// ─────── Object Operations ───────
const handleGetObject = async (
@@ -564,7 +584,7 @@ const storeFileToTelegram = async (
contentType: string,
reqId: string,
): Promise<Response> => {
const tempPath = `/tmp/teleuploader-s3-${nanoid()}`;
const tempPath = `/tmp/filedrop-s3-${nanoid()}`;
await Bun.write(tempPath, buffer);
const signatureBuffer = buffer.subarray(0, 16);
@@ -953,7 +973,7 @@ const handleUploadPart = async (
);
}
const tempPath = `/tmp/teleuploader-mp-${nanoid()}`;
const tempPath = `/tmp/filedrop-mp-${nanoid()}`;
await Bun.write(tempPath, buffer);
const forwardResult = await forwardToStorage(
+3 -3
View File
@@ -49,9 +49,9 @@ export const handleSwaggerJson = async (): Promise<Response> => {
const spec = {
openapi: '3.0.0',
info: {
title: 'TeleUploader API',
title: 'FileDrop API',
version: '1.0.0',
description: 'Telegram-backed file uploader API with stream-based downloads.',
description: 'File upload API with stream-based downloads.',
},
servers: [
{
@@ -215,7 +215,7 @@ export const handleSwaggerHtml = async (): Promise<Response> => {
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TeleUploader API Documentation</title>
<title>FileDrop API Documentation</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui.css">
<style>
html { box-sizing: border-box; overflow-y: scroll; }
+2 -2
View File
@@ -59,7 +59,7 @@ const rejectOversizedRequest = (req: Request): Response | null => {
};
const streamFileToTemp = async (file: File, maxSizeBytes: number): Promise<PreparedUpload> => {
const tempPath = `/tmp/teleuploader-${nanoid()}`;
const tempPath = `/tmp/filedrop-${nanoid()}`;
const writer = createWriteStream(tempPath);
const hasher = new Bun.CryptoHasher('sha256');
const reader = file.stream().getReader();
@@ -123,7 +123,7 @@ const streamFileToTemp = async (file: File, maxSizeBytes: number): Promise<Prepa
};
const writeBufferToTemp = async (fileBuffer: Buffer, fileHash: string): Promise<PreparedUpload> => {
const tempPath = `/tmp/teleuploader-${nanoid()}`;
const tempPath = `/tmp/filedrop-${nanoid()}`;
try {
await Bun.write(tempPath, fileBuffer);
return {
+1 -1
View File
@@ -117,7 +117,7 @@ export const handleUploadObjectV1 = async (
const buffer = Buffer.from(await file.arrayBuffer());
const hash = computeHash(buffer);
const tempPath = `/tmp/teleuploader-web-${nanoid()}`;
const tempPath = `/tmp/filedrop-web-${nanoid()}`;
await Bun.write(tempPath, buffer);
const signatureBuffer = buffer.subarray(0, 16);