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:
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user