-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: proxy resend * feat: no dev * feat: script to set token * fix: mumbo jumbo import firebase * chore: tmp * fix: module imports * feat: mail * feat: send email to resend * feat: proxy body * feat: proxy text * feat: remove unused email from
- Loading branch information
1 parent
d8e5b15
commit ef1eaba
Showing
9 changed files
with
290 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
#!/bin/sh | ||
|
||
firebase functions:config:set notifications.token="secret" | ||
|
||
firebase functions:config:set mail.resend.api_key="secret" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import cors from "cors"; | ||
import express from "express"; | ||
import * as functions from "firebase-functions"; | ||
import {sendEmail} from "./email.js"; | ||
import {proxy} from "./proxy.js"; | ||
|
||
const app = express(); | ||
app.use(express.json()); | ||
app.use(cors({origin: true})); | ||
|
||
const token: string = functions.config().notifications.token; | ||
|
||
const assertToken = ({ | ||
req, | ||
res, | ||
}: { | ||
req: express.Request; | ||
res: express.Response; | ||
}): {valid: boolean} => { | ||
const authorization = req.get("authorization"); | ||
|
||
if (authorization !== `Bearer ${token}`) { | ||
res.status(500).send("Access restricted."); | ||
return {valid: false}; | ||
} | ||
|
||
return {valid: true}; | ||
}; | ||
|
||
app.post("/notifications/email", async (req, res) => { | ||
const {valid} = assertToken({req, res}); | ||
if (!valid) { | ||
return; | ||
} | ||
|
||
await proxy({req, res, fn: sendEmail}); | ||
}); | ||
|
||
export {app}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import {firestore} from "firebase-admin"; | ||
import {getFirestore} from "firebase-admin/firestore"; | ||
import DocumentData = firestore.DocumentData; | ||
|
||
interface Query { | ||
status: "pending" | "success" | "error"; | ||
error?: string; | ||
} | ||
|
||
export const readQuery = async (key: string): Promise<Query | undefined> => { | ||
return await read({key, collection: "query"}); | ||
}; | ||
|
||
export const updateQuery = async ({ | ||
key, | ||
status, | ||
error, | ||
}: { | ||
key: string; | ||
status: "pending" | "success" | "error"; | ||
error?: string; | ||
}) => { | ||
await getFirestore() | ||
.collection("query") | ||
.doc(key) | ||
.update({status, ...(error !== undefined && {error})}); | ||
}; | ||
|
||
export const readCachedResponse = async ( | ||
key: string, | ||
): Promise<unknown | undefined> => { | ||
return await read({key, collection: "cache"}); | ||
}; | ||
|
||
const read = async <T>({ | ||
key, | ||
collection, | ||
}: { | ||
key: string; | ||
collection: "cache" | "query"; | ||
}): Promise<T | undefined> => { | ||
const doc = await getFirestore().collection(collection).doc(key).get(); | ||
|
||
if (!doc.exists) { | ||
return undefined; | ||
} | ||
|
||
return doc.data() as T; | ||
}; | ||
|
||
export const initPendingQuery = async ({ | ||
key, | ||
}: { | ||
key: string; | ||
}): Promise<{success: boolean}> => { | ||
const db = getFirestore(); | ||
const ref = db.collection("query").doc(key); | ||
|
||
try { | ||
await db.runTransaction(async (t) => { | ||
const doc = await t.get(ref); | ||
|
||
if (doc.exists) { | ||
throw new Error("Document already exists."); | ||
} | ||
|
||
t.set(ref, { | ||
status: "pending", | ||
}); | ||
}); | ||
|
||
return {success: true}; | ||
} catch (_err: unknown) { | ||
return {success: false}; | ||
} | ||
}; | ||
|
||
export const writeCacheResponse = async ({ | ||
key, | ||
data, | ||
}: { | ||
key: string; | ||
data: DocumentData; | ||
}) => { | ||
await getFirestore().collection("cache").doc(key).set(data); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import * as express from "express"; | ||
import {firestore} from "firebase-admin"; | ||
import * as functions from "firebase-functions"; | ||
import DocumentData = firestore.DocumentData; | ||
|
||
const resendApiKey: string = functions.config().mail.resend.api_key; | ||
|
||
export const sendEmail = async ({ | ||
req: {body}, | ||
}: { | ||
req: express.Request; | ||
}): Promise<DocumentData> => { | ||
const {from, to, subject, html, text} = body; | ||
|
||
const response = await fetch("https://api.resend.com/emails", { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Authorization: `Bearer ${resendApiKey}`, | ||
}, | ||
body: JSON.stringify({from, to, subject, html, text}), | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new Error( | ||
// eslint-disable-next-line max-len | ||
`Response not ok. Status ${response.status}. Message ${response.statusText}.`, | ||
); | ||
} | ||
|
||
return (await response.json()) as DocumentData; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,20 @@ | ||
import {initializeApp} from "firebase-admin/app"; | ||
import * as functionsV1 from "firebase-functions/v1"; | ||
import {app} from "./app.js"; | ||
import {collectStatuses} from "./statuses/statuses.js"; | ||
|
||
initializeApp(); | ||
|
||
const runtimeOpts = { | ||
timeoutSeconds: 30, | ||
}; | ||
|
||
export const statuses = functionsV1 | ||
.runWith({secrets: ["CRON_CONTROLLER", "MAIL_PWD"]}) | ||
.pubsub.schedule("every 15 minutes") | ||
.onRun(collectStatuses); | ||
|
||
export const observatory = functionsV1 | ||
.region("europe-west6") | ||
.runWith(runtimeOpts) | ||
.https.onRequest(app); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import {isNullish, nonNullish} from "@dfinity/utils"; | ||
import * as express from "express"; | ||
import {firestore} from "firebase-admin"; | ||
import { | ||
initPendingQuery, | ||
readCachedResponse, | ||
readQuery, | ||
updateQuery, | ||
writeCacheResponse, | ||
} from "./db.js"; | ||
import {waitOneSecond} from "./utils.js"; | ||
import DocumentData = firestore.DocumentData; | ||
|
||
type RequestParams = {req: express.Request}; | ||
|
||
export const proxy = async ({ | ||
req, | ||
res, | ||
fn, | ||
}: RequestParams & { | ||
res: express.Response; | ||
fn: (params: RequestParams) => Promise<DocumentData>; | ||
}) => { | ||
const key = req.get("idempotency-key"); | ||
|
||
if (isNullish(key)) { | ||
res.status(500).send( | ||
// eslint-disable-next-line max-len | ||
"An idempotency key is mandatory to provide same result no matter how many times it's applied.", | ||
); | ||
return; | ||
} | ||
|
||
const query = await readQuery(key); | ||
|
||
if (nonNullish(query)) { | ||
await pollCachedResponse({key, res}); | ||
return; | ||
} | ||
|
||
const {success} = await initPendingQuery({key}); | ||
|
||
if (!success) { | ||
await pollCachedResponse({key, res}); | ||
return; | ||
} | ||
|
||
try { | ||
const data = await fn({req}); | ||
|
||
await Promise.all([ | ||
writeCacheResponse({key, data}), | ||
updateQuery({key, status: "success"}), | ||
]); | ||
|
||
res.json(data); | ||
} catch (err: Error | unknown) { | ||
const error = | ||
err instanceof Error && err.message !== undefined | ||
? err.message | ||
: "An unexpected error was proxying the request."; | ||
|
||
await updateQuery({key, status: "error", error}); | ||
|
||
// Note: Since the function does not always return the same error, | ||
// the smart contract will interpret it as unable to replicate the response. | ||
res.status(500).send(err); | ||
} | ||
}; | ||
|
||
const pollCachedResponse = async ({ | ||
key, | ||
res, | ||
attempt = 1, | ||
}: { | ||
key: string; | ||
res: express.Response; | ||
attempt?: number; | ||
}): Promise<void> => { | ||
const cache = await readCachedResponse(key); | ||
|
||
if (nonNullish(cache)) { | ||
res.json(cache); | ||
return; | ||
} | ||
|
||
const query = await readQuery(key); | ||
if (nonNullish(query?.error)) { | ||
res.status(500).send("Proxying the request failed."); | ||
return; | ||
} | ||
|
||
if (attempt < 30) { | ||
await waitOneSecond(); | ||
return await pollCachedResponse({key, res, attempt: attempt + 1}); | ||
} | ||
|
||
res.status(500).send("No cached response found after 30 seconds."); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export const waitOneSecond = (): Promise<void> => { | ||
return new Promise<void>((resolve) => { | ||
setTimeout(resolve, 1000); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters