Initial commit
This commit is contained in:
35
server/api/auth/login.post.ts
Normal file
35
server/api/auth/login.post.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createError, defineEventHandler, setResponseStatus } from "h3";
|
||||
import {
|
||||
authConfigured,
|
||||
issueSession,
|
||||
requireSameOrigin,
|
||||
verifyPasscode,
|
||||
} from "../../utils/auth";
|
||||
import { readJson } from "../../utils/http";
|
||||
export default defineEventHandler(async (event) => {
|
||||
requireSameOrigin(event);
|
||||
const body = await readJson<{ passcode?: unknown }>(event, 1024);
|
||||
if (
|
||||
!body ||
|
||||
typeof body !== "object" ||
|
||||
Object.keys(body).some((key) => key !== "passcode")
|
||||
)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Malformed login request.",
|
||||
});
|
||||
if (!authConfigured())
|
||||
throw createError({
|
||||
statusCode: 503,
|
||||
statusMessage: "Authentication is not configured.",
|
||||
});
|
||||
if (!verifyPasscode(body?.passcode)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Incorrect passcode.",
|
||||
});
|
||||
}
|
||||
issueSession(event);
|
||||
setResponseStatus(event, 204);
|
||||
});
|
||||
7
server/api/auth/logout.post.ts
Normal file
7
server/api/auth/logout.post.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineEventHandler, setResponseStatus } from "h3";
|
||||
import { clearAuthCookie, requireSameOrigin } from "../../utils/auth";
|
||||
export default defineEventHandler((event) => {
|
||||
requireSameOrigin(event);
|
||||
clearAuthCookie(event);
|
||||
setResponseStatus(event, 204);
|
||||
});
|
||||
5
server/api/auth/session.get.ts
Normal file
5
server/api/auth/session.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { defineEventHandler } from "h3";
|
||||
import { isAuthenticated } from "../../utils/auth";
|
||||
export default defineEventHandler((event) => ({
|
||||
authenticated: isAuthenticated(event),
|
||||
}));
|
||||
25
server/api/drops/[id].get.ts
Normal file
25
server/api/drops/[id].get.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
createError,
|
||||
defineEventHandler,
|
||||
getRequestHeader,
|
||||
getRouterParam,
|
||||
} from "h3";
|
||||
import { dropStore, DropStoreError } from "../../utils/drop-store";
|
||||
import { publicHeaders } from "../../utils/http";
|
||||
export default defineEventHandler(async (event) => {
|
||||
publicHeaders(event);
|
||||
const id = String(getRouterParam(event, "id") || "").toUpperCase();
|
||||
const access = getRequestHeader(event, "x-quickdrop-access") || "";
|
||||
try {
|
||||
return await dropStore.descriptor(id, access);
|
||||
} catch (error) {
|
||||
if (error instanceof DropStoreError && error.code === "expired")
|
||||
throw createError({ statusCode: 410, statusMessage: "Drop expired." });
|
||||
if (error instanceof DropStoreError && error.code === "forbidden")
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Invalid access capability.",
|
||||
});
|
||||
throw createError({ statusCode: 404, statusMessage: "Drop not found." });
|
||||
}
|
||||
});
|
||||
72
server/api/drops/[id]/chunks/[index].get.ts
Normal file
72
server/api/drops/[id]/chunks/[index].get.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
createError,
|
||||
defineEventHandler,
|
||||
getRouterParam,
|
||||
sendStream,
|
||||
setHeaders,
|
||||
} from "h3";
|
||||
import { dropStore, DropStoreError } from "../../../../utils/drop-store";
|
||||
import { bearerToken, publicHeaders } from "../../../../utils/http";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
publicHeaders(event);
|
||||
const token = bearerToken(event);
|
||||
if (!token)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "A valid lease is required.",
|
||||
});
|
||||
const id = String(getRouterParam(event, "id") || "").toUpperCase();
|
||||
let handle:
|
||||
| Awaited<ReturnType<typeof dropStore.readChunk>>["handle"]
|
||||
| undefined;
|
||||
let stream:
|
||||
| ReturnType<NonNullable<typeof handle>["createReadStream"]>
|
||||
| undefined;
|
||||
let closed = false;
|
||||
const close = async () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
stream?.destroy();
|
||||
await handle?.close().catch(() => undefined);
|
||||
};
|
||||
const abort = () => {
|
||||
if (!event.node.res.writableEnded && stream && !stream.destroyed)
|
||||
stream.destroy(new Error("Client disconnected."));
|
||||
void close();
|
||||
};
|
||||
try {
|
||||
const chunk = await dropStore.readChunk(
|
||||
id,
|
||||
Number(getRouterParam(event, "index")),
|
||||
token,
|
||||
);
|
||||
handle = chunk.handle;
|
||||
stream = handle.createReadStream();
|
||||
event.node.res.once("close", abort);
|
||||
event.node.res.once("finish", close);
|
||||
setHeaders(event, {
|
||||
"content-type": "application/octet-stream",
|
||||
"content-length": chunk.size,
|
||||
"cache-control": "private, no-store",
|
||||
});
|
||||
return await sendStream(event, stream);
|
||||
} catch (error) {
|
||||
await close();
|
||||
if (error instanceof DropStoreError)
|
||||
throw createError({
|
||||
statusCode:
|
||||
error.code === "expired"
|
||||
? 410
|
||||
: error.code === "not-found"
|
||||
? 404
|
||||
: 403,
|
||||
statusMessage: error.message,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
event.node.res.off("close", abort);
|
||||
event.node.res.off("finish", close);
|
||||
await close();
|
||||
}
|
||||
});
|
||||
30
server/api/drops/[id]/chunks/[index].put.ts
Normal file
30
server/api/drops/[id]/chunks/[index].put.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
createError,
|
||||
defineEventHandler,
|
||||
getRouterParam,
|
||||
setResponseStatus,
|
||||
} from "h3";
|
||||
import { requireAuth } from "../../../../utils/auth";
|
||||
import { dropStore, DropStoreError } from "../../../../utils/drop-store";
|
||||
import { readBounded, requireContentType } from "../../../../utils/http";
|
||||
export default defineEventHandler(async (event) => {
|
||||
requireAuth(event);
|
||||
requireContentType(event, "application/octet-stream");
|
||||
const id = String(getRouterParam(event, "id") || "").toUpperCase();
|
||||
const index = Number(getRouterParam(event, "index"));
|
||||
try {
|
||||
const result = await dropStore.putChunk(
|
||||
id,
|
||||
index,
|
||||
await readBounded(event, 8388624),
|
||||
);
|
||||
setResponseStatus(event, result === "created" ? 201 : 204);
|
||||
} catch (error) {
|
||||
if (error instanceof DropStoreError) {
|
||||
const status =
|
||||
error.code === "not-found" ? 404 : error.code === "expired" ? 410 : 409;
|
||||
throw createError({ statusCode: status, statusMessage: error.message });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
32
server/api/drops/[id]/complete.post.ts
Normal file
32
server/api/drops/[id]/complete.post.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
createError,
|
||||
defineEventHandler,
|
||||
getRouterParam,
|
||||
setResponseStatus,
|
||||
} from "h3";
|
||||
import { dropStore, DropStoreError } from "../../../utils/drop-store";
|
||||
import { bearerToken, publicHeaders, readBounded } from "../../../utils/http";
|
||||
export default defineEventHandler(async (event) => {
|
||||
publicHeaders(event);
|
||||
if ((await readBounded(event, 0)).byteLength)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Completion body must be empty.",
|
||||
});
|
||||
const token = bearerToken(event);
|
||||
if (!token)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "A valid lease is required.",
|
||||
});
|
||||
try {
|
||||
await dropStore.complete(
|
||||
String(getRouterParam(event, "id") || "").toUpperCase(),
|
||||
token,
|
||||
);
|
||||
} catch (error) {
|
||||
if (!(error instanceof DropStoreError))
|
||||
throw error; /* Completion is deliberately idempotent: an already completed, expired, or unknown lease has the desired state. */
|
||||
}
|
||||
setResponseStatus(event, 204);
|
||||
});
|
||||
37
server/api/drops/[id]/finalize.post.ts
Normal file
37
server/api/drops/[id]/finalize.post.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createError, defineEventHandler, getRouterParam } from "h3";
|
||||
import { requireAuth } from "../../../utils/auth";
|
||||
import { dropStore, DropStoreError } from "../../../utils/drop-store";
|
||||
import { readBounded, requireContentType } from "../../../utils/http";
|
||||
import { validateManifestBodyLength } from "../../../../shared/utils/drop-contract";
|
||||
export default defineEventHandler(async (event) => {
|
||||
requireAuth(event);
|
||||
requireContentType(event, "application/octet-stream");
|
||||
const body = await readBounded(event, 65552);
|
||||
try {
|
||||
validateManifestBodyLength(body.byteLength);
|
||||
} catch {
|
||||
throw createError({ statusCode: 400, statusMessage: "Invalid manifest." });
|
||||
}
|
||||
const id = String(getRouterParam(event, "id") || "").toUpperCase();
|
||||
try {
|
||||
const result = await dropStore.finalize(id, body);
|
||||
return {
|
||||
id,
|
||||
expiresAt: new Date(result.expiresAt!).toISOString(),
|
||||
maxReads: result.maxReads,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DropStoreError) {
|
||||
const status =
|
||||
error.code === "not-found"
|
||||
? 404
|
||||
: error.code === "expired"
|
||||
? 410
|
||||
: error.code === "incomplete"
|
||||
? 409
|
||||
: 409;
|
||||
throw createError({ statusCode: status, statusMessage: error.message });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
39
server/api/drops/[id]/leases.post.ts
Normal file
39
server/api/drops/[id]/leases.post.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
createError,
|
||||
defineEventHandler,
|
||||
getRequestHeader,
|
||||
getRouterParam,
|
||||
setResponseStatus,
|
||||
} from "h3";
|
||||
import { dropStore, DropStoreError } from "../../../utils/drop-store";
|
||||
import { bearerToken, publicHeaders, readBounded } from "../../../utils/http";
|
||||
export default defineEventHandler(async (event) => {
|
||||
publicHeaders(event);
|
||||
if ((await readBounded(event, 0)).byteLength)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Lease claim body must be empty.",
|
||||
});
|
||||
const id = String(getRouterParam(event, "id") || "").toUpperCase();
|
||||
const access = getRequestHeader(event, "x-quickdrop-access") || "";
|
||||
try {
|
||||
const result = await dropStore.claim(id, access, bearerToken(event));
|
||||
setResponseStatus(event, result.reused ? 200 : 201);
|
||||
return {
|
||||
token: result.token,
|
||||
leaseExpiresAt: new Date(result.leaseExpiresAt).toISOString(),
|
||||
expiresAt: new Date(result.expiresAt).toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DropStoreError) {
|
||||
const status =
|
||||
error.code === "exhausted" || error.code === "expired"
|
||||
? 410
|
||||
: error.code === "forbidden"
|
||||
? 403
|
||||
: 404;
|
||||
throw createError({ statusCode: status, statusMessage: error.message });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
26
server/api/drops/index.post.ts
Normal file
26
server/api/drops/index.post.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createError, defineEventHandler, setResponseStatus } from "h3";
|
||||
import { requireAuth } from "../../utils/auth";
|
||||
import { dropStore } from "../../utils/drop-store";
|
||||
import { readJson } from "../../utils/http";
|
||||
import { validateReservation } from "../../../shared/utils/drop-contract";
|
||||
export default defineEventHandler(async (event) => {
|
||||
requireAuth(event);
|
||||
const body = readJson(event, 4096);
|
||||
let request;
|
||||
try {
|
||||
request = validateReservation(await body);
|
||||
} catch (error) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage:
|
||||
error instanceof Error ? error.message : "Invalid reservation.",
|
||||
});
|
||||
}
|
||||
const result = await dropStore.reserve(request);
|
||||
setResponseStatus(event, 201);
|
||||
return {
|
||||
id: result.id,
|
||||
uploadExpiresAt: new Date(result.uploadExpiresAt).toISOString(),
|
||||
access: result.access,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user