From 534d8d41836d1d684ccf52a182aae32cbc19952e Mon Sep 17 00:00:00 2001 From: Michael Della Bitta Date: Wed, 12 Jun 2024 14:58:20 -0400 Subject: [PATCH] Api cleanups, added ability to create api keys and send emails. --- src/aggregation/api_key_repository.ts | 53 ++++++++++++++++++---- src/aggregation/search.ts | 63 ++++++++++++++++++++++++++- src/index.ts | 29 ++++++++++-- 3 files changed, 130 insertions(+), 15 deletions(-) diff --git a/src/aggregation/api_key_repository.ts b/src/aggregation/api_key_repository.ts index 3bd74d5..10c091f 100644 --- a/src/aggregation/api_key_repository.ts +++ b/src/aggregation/api_key_repository.ts @@ -1,13 +1,27 @@ import { isEmail } from "validator"; import { Pool } from "pg"; +import { InternalErrorResponse } from "./responses"; const apiKeyRegex = /^[a-zA-Z0-9-]{32}$/; -const findUserByApiKeyQuery = - "select id, key, email, enabled, staff, created_at, updated_at from account where key = $1"; +const findUserByApiKeyQuery = ` + select id, key, email, enabled, staff, created_at, updated_at + from account + where key = $1 + `; -const findUserByEmailQuery = - "select id, key, email, enabled, staff, created_at, updated_at from account where email = $1"; +const findUserByEmailQuery = ` + select id, key, email, enabled, staff, created_at, updated_at + from account + where email = $1 + `; + +const createApiKeyQuery = ` + insert into account (key, email, enabled, staff) + select $1, $2::varchar(100), $3, $4 + where not exists (select id from account where email = $2) + returning id, key, email, enabled, staff + `; interface User { id: number; @@ -24,7 +38,7 @@ export default class ApiKeyRepository { this.db = db; } - db: Pool; + private db: Pool; isApiKeyValid(apiKey: string): boolean { return apiKeyRegex.test(apiKey); @@ -34,8 +48,8 @@ export default class ApiKeyRepository { return isEmail(email) && email.length <= 100; } - async findUserByApiKey(apiKey: string, db: Pool): Promise { - const results = await db.query(findUserByApiKeyQuery, [apiKey]); + async findUserByApiKey(apiKey: string): Promise { + const results = await this.db.query(findUserByApiKeyQuery, [apiKey]); if (results.rows.length > 0) { return results.rows[0]; } @@ -43,12 +57,33 @@ export default class ApiKeyRepository { return null; } - async findUserByEmail(email: string, db: Pool): Promise { - const results = await db.query(findUserByEmailQuery, [email]); + async findUserByEmail(email: string): Promise { + const results = await this.db.query(findUserByEmailQuery, [email]); if (results.rows.length > 0) { return results.rows[0]; } return null; } + + async createAccount( + key: string, + email: string, + enabled: boolean, + staff: boolean, + ): Promise { + const results = await this.db.query(createApiKeyQuery, [ + key, + email, + enabled, + staff, + ]); + + console.log( + "Created account for email:", + email, + " rows affected: ", + results.rowCount, + ); + } } diff --git a/src/aggregation/search.ts b/src/aggregation/search.ts index cbf79dd..9f38928 100644 --- a/src/aggregation/search.ts +++ b/src/aggregation/search.ts @@ -11,13 +11,26 @@ import { UnrecognizedParameters, DPLADocList, FiveHundredResponse, + InvalidEmail, + EmailSent, } from "./responses"; +import ApiKeyRepository from "./api_key_repository"; +import { createHash, getRandomValues } from "node:crypto"; +import { Emailer } from "./Emailer"; export default class SearchController { - esClient: Client; + private esClient: Client; + private apiKeyRepository: ApiKeyRepository; + private emailer: Emailer; - constructor(esClient: Client) { + constructor( + esClient: Client, + apiKeyRepository: ApiKeyRepository, + emailer: Emailer, + ) { this.esClient = esClient; + this.apiKeyRepository = apiKeyRepository; + this.emailer = emailer; } public async getItem( @@ -82,4 +95,50 @@ export default class SearchController { return mapSearchResponse(response.body, query); } + + public async createApiKey( + email: string, + ): Promise { + if (!this.apiKeyRepository.isValidEmail(email)) { + return new InvalidEmail(); + } + const lookupUser = await this.apiKeyRepository.findUserByEmail(email); + let user = null; + if (lookupUser !== null) { + user = lookupUser; + } else { + const hash = createHash("md5"); + hash.update(email); + hash.update(getRandomValues(new Uint8Array(32))); + const key = hash.digest("hex"); + const staff = email.endsWith("@dp.la"); + try { + await this.apiKeyRepository.createAccount(key, email, true, staff); + } catch (e: any) { + console.log("Caught error creating account for:", email, e); + return Promise.resolve(new InternalErrorResponse()); + } + user = { + key, + email, + enabled: true, + staff, + created_at: new Date(), + updated_at: new Date(), + }; + } + + try { + await this.emailer.sendEmail( + email, + "Your DPLA API Key", + `Your DPLA API key is: ${user.key}`, + ); + + return Promise.resolve(new EmailSent(email)); + } catch (e: any) { + console.log("Caught error sending email to:", email, e); + return Promise.resolve(new InternalErrorResponse()); + } + } } diff --git a/src/index.ts b/src/index.ts index 9fff89f..584651b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,8 +16,11 @@ import { DPLADocList, FourHundredResponse, FiveHundredResponse, + EmailSent, } from "./aggregation/responses"; import ApiKeyRepository from "./aggregation/api_key_repository"; +import { SESClient } from "@aws-sdk/client-ses"; +import { Emailer } from "./aggregation/Emailer"; const mustFork = process.env.MUST_FORK === "true" || process.env.NODE_ENV === "production"; @@ -56,6 +59,7 @@ function worker() { const elasticsearchUrl = process.env.ELASTIC_URL || "http://search.internal.dp.la:9200/"; const elasticsearchIndex = process.env.ELASTIC_INDEX || "dpla_alias"; + const emailFrom = process.env.EMAIL_FROM || "info@dp.la"; const app: Application = express(); app.use(morgan("tiny")); //http request logger @@ -65,6 +69,7 @@ function worker() { let s3 = new S3Client(awsOptions); let sqs = new SQSClient(awsOptions); + let ses = new SESClient(awsOptions); if (xray) { const XRayExpress = AWSXRay.express; @@ -74,6 +79,7 @@ function worker() { AWSXRay.captureHTTPsGlobal(https, true); sqs = AWSXRay.captureAWSClient(sqs); s3 = AWSXRay.captureAWSClient(s3); + ses = AWSXRay.captureAWSClient(ses); } const dbPool: Pool = new Pool({ @@ -85,6 +91,8 @@ function worker() { query_timeout: dbQueryTimeout, }); + const emailer = new Emailer(ses, emailFrom); + const apiKeyRepository = new ApiKeyRepository(dbPool); const authMiddleware = async ( @@ -109,7 +117,7 @@ function worker() { if (!apiKeyRepository.isApiKeyValid(apiKey)) { return res.status(401).json({ message: "Unauthorized" }); } - const user = await apiKeyRepository.findUserByApiKey(apiKey, dbPool); + const user = await apiKeyRepository.findUserByApiKey(apiKey); if (!user) { return res.status(401).json({ message: "Unauthorized" }); @@ -162,9 +170,17 @@ function worker() { }); //SEARCH - const searchController = new SearchController(esClient); + const searchController = new SearchController( + esClient, + apiKeyRepository, + emailer, + ); const handleJsonResponses = ( - response: DPLADocList | FourHundredResponse | FiveHundredResponse, + response: + | DPLADocList + | EmailSent + | FourHundredResponse + | FiveHundredResponse, res: express.Response, ) => { for (const [header, value] of Object.entries(securityHeaders)) { @@ -181,7 +197,6 @@ function worker() { }; app.get(["/v2/items/:id", "/items/:id"], authMiddleware, async (req, res) => { - console.log("IN: /v2/items/:id"); const response = await searchController.getItem( req.params.id, queryParams(req), @@ -200,6 +215,12 @@ function worker() { handleJsonResponses(response, res); }); + app.post(["/v2/api_key/:email", "/api_key:email"], async (req, res) => { + const email = req.params.email; + const response = await searchController.createApiKey(email); + handleJsonResponses(response, res); + }); + app.listen(PORT, () => { console.log("Server is running on port", PORT); });