diff --git a/src/app/api/aliases/temporary/route.ts b/src/app/api/aliases/temporary/route.ts new file mode 100644 index 0000000..4997753 --- /dev/null +++ b/src/app/api/aliases/temporary/route.ts @@ -0,0 +1,78 @@ +import apiAuth from "@/lib/apiAuth"; +import { auditLog } from "@/lib/audit"; +import { AliasEntry, createAliasEntry } from "@/lib/db"; +import { generateAliasEmail } from "@/lib/util"; +import { NextResponse } from "next/server"; + +/** + * @swagger + * /api/aliases/temporary: + * post: + * description: Creates a new temporary alias + * requestBody: + * description: The options for the alias + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * label: + * type: string + * example: "reddit" + * style: + * type: string + * enum: [words, random] + * labelAtEnd: + * type: boolean + * security: + * - api_key: [] + * responses: + * 200: + * description: The created alias + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: number + * address: + * type: string + * example: "your@primary.email" + * alias: + * type: string + * example: "reddit-whypark@t.amogus.cloud" + * pending: + * type: boolean + * temporary: + * type: boolean + */ +export async function POST(request: Request) { + return await apiAuth(request, async (request, user) => { + const body = await request.json(); + if (typeof body.label != "string") return new Response('"label" not provided', { status: 400 }); + if (!body.label.length || body.label.length > 16) return new Response('"label" must be between 1 and 16 characters long', { status: 400 }); + if (typeof body.style != "string") return new Response('"style" not provided', { status: 400 }); + if (typeof body.labelAtEnd != 'boolean') return new Response('"labelAtEnd" not provided', { status: 400 }); + + const email = await generateAliasEmail(body.label, body.style, body.labelAtEnd); + const id = await createAliasEntry( + user, + email, + false, + true, + ); + + const alias: AliasEntry = { + id: id, + address: user, + alias: email, + pending: false, + temporary: true, + }; + + auditLog("createTempAlias", alias); + return NextResponse.json(alias); + }); +} \ No newline at end of file diff --git a/src/app/api/test/route.ts b/src/app/api/test/route.ts deleted file mode 100644 index 475f931..0000000 --- a/src/app/api/test/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @swagger - * /api/test: - * get: - * description: Returns the hello world - * responses: - * 200: - * description: Hello World! - */ -export async function GET(_request: Request) { - // Do whatever you want - return new Response('Hello World!', { - status: 200, - }); -} \ No newline at end of file diff --git a/src/lib/actions.ts b/src/lib/actions.ts index 3e51624..f4d8479 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -3,11 +3,9 @@ import crypto from "crypto"; import fs from "fs/promises"; import { getServerSession } from "next-auth"; -import * as random_words from "random-words"; import { AuditLog, auditLog } from "./audit"; import { AliasEntry, AliasRequestEntry, ApiKeyEntry, approveAliasEntry, createAliasEntry, createApiKeyEntry, createTempAliasRequestEntry, createUserEntry, database, deleteAliasEntry, deleteApiKey, deleteTempAliasRequestEntry, getAlias, getAllAliases, getApiKeyById, getTempAliasRequestEntry, getUserAliases, getUserApiKeys, setUserPassword } from "./db"; -import { aliasesNeedApproval, anonymizeApiKey, isAdmin } from "./util"; -import { TEMP_EMAIL_DOMAIN } from "./constants"; +import { aliasesNeedApproval, anonymizeApiKey, generateAliasEmail, isAdmin } from "./util"; export async function fetchAllUsers(): Promise { return new Promise(async (resolve, reject) => { @@ -135,24 +133,7 @@ export async function requestTemporaryAlias( if (!label.length || label.length > 16) throw new Error("Malformed request"); - let email: string; - do { - let randomString: string; - switch (style) { - case 'words': - randomString = random_words.generate(2).join(''); - break; - case 'random': - randomString = crypto - .randomBytes(8) - .toString('base64') - .replace(/\W/, ''); // Delete special characters - break; - default: - throw new Error("Invalid style"); - } - email = `${labelAtEnd ? `${randomString}-${label}` : `${label}-${randomString}`}@${TEMP_EMAIL_DOMAIN}`; - } while (!await aliasAvailable(email, true)); + let email = await generateAliasEmail(label, style, labelAtEnd); const request: AliasRequestEntry = { key: crypto.randomBytes(12).toString('base64'), diff --git a/src/lib/apiAuth.ts b/src/lib/apiAuth.ts new file mode 100644 index 0000000..09b1eb1 --- /dev/null +++ b/src/lib/apiAuth.ts @@ -0,0 +1,19 @@ +import { getApiKeyByToken } from "./db"; + +// Returns the user's email address or throws an error if unauthenticated +export default async function apiAuth(request: Request, callback: (request: Request, user: string) => Response | Promise) { + let authToken = request.headers.get("authorization"); + if (!authToken) return new Response("Unauthorized", { status: 401 }); + const token = authToken.replace(/^Bearer /, ""); // the annoying prefix should be optional + + const key = await getApiKeyByToken(token); + if (!key) return new Response("Unauthorized", { status: 401 }); + + const res = callback(request, key.address);; + + if (res instanceof Promise) { + return await res; + } else { + return res; + } +}; diff --git a/src/lib/swagger.ts b/src/lib/swagger.ts index 5c94c37..e704460 100644 --- a/src/lib/swagger.ts +++ b/src/lib/swagger.ts @@ -11,10 +11,10 @@ export const getApiDocs = async () => { }, components: { securitySchemes: { - BearerAuth: { + api_key: { type: 'http', scheme: 'bearer', - bearerFormat: 'JWT', + name: 'API key', }, }, }, diff --git a/src/lib/util.ts b/src/lib/util.ts index 8def298..0f3b566 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,6 +1,9 @@ import { Session } from 'next-auth'; import crypto from 'crypto'; import { ApiKeyEntry } from './db'; +import * as random_words from "random-words"; +import { TEMP_EMAIL_DOMAIN } from './constants'; +import { aliasAvailable } from './actions'; export function sha256sum(input: any) { const hash = crypto.createHash('sha256'); @@ -23,3 +26,26 @@ export function anonymizeApiKey(key: ApiKeyEntry): ApiKeyEntry { token: key.token.substring(0, 6) + "********", }; } + +export async function generateAliasEmail(label: string, style: 'words' | 'random', labelAtEnd: boolean): Promise { + let email: string; + do { + let randomString: string; + switch (style) { + case 'words': + randomString = random_words.generate(2).join(''); + break; + case 'random': + randomString = crypto + .randomBytes(8) + .toString('base64') + .replace(/\W/, ''); // Delete special characters + break; + default: + throw new Error("Invalid style"); + } + email = `${labelAtEnd ? `${randomString}-${label}` : `${label}-${randomString}`}@${TEMP_EMAIL_DOMAIN}`; + } while (!await aliasAvailable(email, true)); + + return email; +}