-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: switch to Google Sheets as backend
- Loading branch information
Showing
10 changed files
with
1,175 additions
and
210 deletions.
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
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,3 @@ | ||
import { route } from "./lib/router"; | ||
|
||
fly.http.respondWith(route); |
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,52 @@ | ||
import * as jwt from "jwt-simple"; | ||
|
||
const SERVICE_ENDPOINT = "https://sheets.googleapis.com"; | ||
|
||
// We only care about From, To, and Is Regex | ||
const A1_RANGE = "B2:D"; | ||
|
||
export type LinkRecord = { from: string; to: string; isRegex: boolean }; | ||
|
||
export const createAuthJwt = () => { | ||
const claims = { | ||
iss: app.config.googleServiceAccountEmail, | ||
scope: "https://www.googleapis.com/auth/spreadsheets.readonly", | ||
aud: "https://oauth2.googleapis.com/token", | ||
exp: Math.floor(Date.now() / 1000) + 5 * 60, | ||
iat: Math.floor(Date.now() / 1000) | ||
}; | ||
|
||
return jwt.encode(claims, app.config.googleServiceAccountKey, "RS256"); | ||
}; | ||
|
||
export const getAccessToken = async () => { | ||
const token = createAuthJwt(); | ||
const payload = { | ||
grant_type: encodeURI("urn:ietf:params:oauth:grant-type:jwt-bearer"), | ||
assertion: token | ||
}; | ||
const auth = await fetch("https://oauth2.googleapis.com/token", { | ||
method: "POST", | ||
body: JSON.stringify(payload) | ||
}); | ||
const { access_token } = await auth.json(); | ||
return access_token; | ||
}; | ||
|
||
export const fetchEntries = async (): Promise<LinkRecord[]> => { | ||
const accessToken = await getAccessToken(); | ||
|
||
const docId = app.config.googleSheetDocId; | ||
const url = `${SERVICE_ENDPOINT}/v4/spreadsheets/${docId}/values/${A1_RANGE}?access_token=${accessToken}`; | ||
const response = await fetch(url); | ||
const { values: rows }: { values: string[] } = await response.json(); | ||
|
||
// Make sure that record has From and To values | ||
return rows | ||
.map(row => ({ | ||
from: row[0], | ||
to: row[1], | ||
isRegex: row[2] === "TRUE" | ||
})) | ||
.filter(({ from, to }) => !!from && !!to); | ||
}; |
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,90 @@ | ||
import * as Url from "url-parse"; | ||
import * as queryString from "query-string"; | ||
import cache from "@fly/cache"; | ||
|
||
import { | ||
clearCache, | ||
refreshCache, | ||
lookupPath, | ||
CACHE_TAG, | ||
TTL_404 | ||
} from "./shortener"; | ||
|
||
export const parseReq = (req: Request) => { | ||
const url = new Url(req.url); | ||
const { pathname } = url; | ||
const params = queryString.parse(url.query); | ||
return { url, pathname, params }; | ||
}; | ||
|
||
const normalize = (str: string) => str.toLowerCase().trim(); | ||
|
||
/** | ||
* Handle admin routing | ||
* @param req The admin Request to be routed | ||
*/ | ||
const routeAdmin = async (req: Request): Promise<Response> => { | ||
const { pathname, params } = parseReq(req); | ||
|
||
if (params.secret !== app.config.adminSecret) { | ||
return new Response("Unauthorized", { status: 403 }); | ||
} | ||
|
||
if (pathname.includes("/clear")) { | ||
await clearCache(); | ||
return new Response("Cleared", { status: 200 }); | ||
} | ||
|
||
if (pathname.includes("/refresh")) { | ||
await clearCache(); | ||
await refreshCache(); | ||
return new Response("Refreshed", { status: 200 }); | ||
} | ||
|
||
return new Response("Not Found", { status: 404 }); | ||
}; | ||
|
||
/** | ||
* Handle short link redirects | ||
* @param req The short link request to be routed | ||
*/ | ||
const routeShortlink = async (req: Request): Promise<Response> => { | ||
const { pathname } = parseReq(req); | ||
const path = normalize(pathname); | ||
|
||
// Check for existing match | ||
let matchingResponse = await lookupPath(path); | ||
if (matchingResponse) { | ||
return matchingResponse; | ||
} | ||
|
||
// Refresh the cache if no match | ||
await refreshCache(); | ||
|
||
// Re-check for match | ||
matchingResponse = await lookupPath(path); | ||
if (matchingResponse) { | ||
return matchingResponse; | ||
} | ||
|
||
await cache.set(path, "404", { ttl: TTL_404, tags: [CACHE_TAG] }); | ||
return new Response("", { status: 404 }); | ||
}; | ||
|
||
/** | ||
* Handle routing | ||
* @param req The Request to be routed | ||
*/ | ||
export const route = async (req: Request): Promise<Response> => { | ||
const { pathname } = parseReq(req); | ||
|
||
if (pathname === "/favicon.ico") { | ||
return new Response("", { status: 404 }); | ||
} | ||
|
||
if (pathname.includes("/admin")) { | ||
return routeAdmin(req); | ||
} | ||
|
||
return routeShortlink(req); | ||
}; |
Oops, something went wrong.