Skip to content

Commit

Permalink
Api cleanups, added ability to create api keys and send emails.
Browse files Browse the repository at this point in the history
  • Loading branch information
mdellabitta committed Jun 12, 2024
1 parent 5c6f74c commit 534d8d4
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 15 deletions.
53 changes: 44 additions & 9 deletions src/aggregation/api_key_repository.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,7 +38,7 @@ export default class ApiKeyRepository {
this.db = db;
}

db: Pool;
private db: Pool;

isApiKeyValid(apiKey: string): boolean {
return apiKeyRegex.test(apiKey);
Expand All @@ -34,21 +48,42 @@ export default class ApiKeyRepository {
return isEmail(email) && email.length <= 100;
}

async findUserByApiKey(apiKey: string, db: Pool): Promise<User | unknown> {
const results = await db.query(findUserByApiKeyQuery, [apiKey]);
async findUserByApiKey(apiKey: string): Promise<User | unknown> {
const results = await this.db.query(findUserByApiKeyQuery, [apiKey]);
if (results.rows.length > 0) {
return results.rows[0];
}

return null;
}

async findUserByEmail(email: string, db: Pool): Promise<User | unknown> {
const results = await db.query(findUserByEmailQuery, [email]);
async findUserByEmail(email: string): Promise<User | null> {
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<void | InternalErrorResponse> {
const results = await this.db.query(createApiKeyQuery, [
key,
email,
enabled,
staff,
]);

console.log(
"Created account for email:",
email,
" rows affected: ",
results.rowCount,
);
}
}
63 changes: 61 additions & 2 deletions src/aggregation/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -82,4 +95,50 @@ export default class SearchController {

return mapSearchResponse(response.body, query);
}

public async createApiKey(
email: string,
): Promise<InvalidEmail | InternalErrorResponse | EmailSent> {
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());
}
}
}
29 changes: 25 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 || "[email protected]";

const app: Application = express();
app.use(morgan("tiny")); //http request logger
Expand All @@ -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;
Expand All @@ -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({
Expand All @@ -85,6 +91,8 @@ function worker() {
query_timeout: dbQueryTimeout,
});

const emailer = new Emailer(ses, emailFrom);

const apiKeyRepository = new ApiKeyRepository(dbPool);

const authMiddleware = async (
Expand All @@ -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" });
Expand Down Expand Up @@ -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)) {
Expand All @@ -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),
Expand All @@ -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);
});
Expand Down

0 comments on commit 534d8d4

Please sign in to comment.