Use tabs for indentation, run prettier

This commit is contained in:
Erisa A 2023-03-04 18:12:47 +00:00
parent c6b5d4d61c
commit 7383aa990f
No known key found for this signature in database
5 changed files with 234 additions and 236 deletions

View file

@ -2,6 +2,6 @@
"singleQuote": true, "singleQuote": true,
"semi": false, "semi": false,
"trailingComma": "all", "trailingComma": "all",
"tabWidth": 2, "useTabs": true,
"printWidth": 80 "printWidth": 80
} }

View file

@ -1,5 +1,3 @@
{ {
"recommendations": [ "recommendations": ["bungcip.better-toml"]
"bungcip.better-toml" }
]
}

View file

@ -1,21 +1,21 @@
{ {
"name": "worker-links", "name": "worker-links",
"version": "2.0.0", "version": "2.0.0",
"description": "Simple link shortener for Cloudflare Workers.", "description": "Simple link shortener for Cloudflare Workers.",
"author": "Erisa A", "author": "Erisa A",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"format": "prettier --write '**/*.{ts,js,css,json,md}'", "format": "prettier --write '**/*.{ts,js,css,json,md}'",
"deploy": "wrangler publish", "deploy": "wrangler publish",
"dev": "wrangler dev", "dev": "wrangler dev",
"addsecret": "wrangler secret put WORKERLINKS_SECRET" "addsecret": "wrangler secret put WORKERLINKS_SECRET"
}, },
"dependencies": { "dependencies": {
"hono": "^3.0.2" "hono": "^3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20221111.1", "@cloudflare/workers-types": "^4.20221111.1",
"prettier": "^2.8.4", "prettier": "^2.8.4",
"wrangler": "^2.12.0" "wrangler": "^2.12.0"
} }
} }

View file

@ -1,260 +1,260 @@
import { Context, Hono } from "hono"; import { Context, Hono } from 'hono'
type Variables = { type Variables = {
path: string; path: string
key: string; key: string
shortUrl: string; shortUrl: string
}; }
type Bindings = { type Bindings = {
WORKERLINKS_SECRET: string; WORKERLINKS_SECRET: string
KV: KVNamespace; KV: KVNamespace
kv: KVNamespace; kv: KVNamespace
}; }
type BulkUpload = { type BulkUpload = {
[id: string]: string; [id: string]: string
}; }
const app = new Hono<{ Variables: Variables; Bindings: Bindings }>(); const app = new Hono<{ Variables: Variables; Bindings: Bindings }>()
// store the path, key and short url for reference in requeests // store the path, key and short url for reference in requeests
// e.g. c.get('key') // e.g. c.get('key')
app.use("*", async (c, next) => { app.use('*', async (c, next) => {
const path = new URL(c.req.url).pathname; const path = new URL(c.req.url).pathname
let key = path !== "/" ? path.replace(/\/$/, "") : path; let key = path !== '/' ? path.replace(/\/$/, '') : path
let shortUrl = new URL(key, c.req.url).origin; let shortUrl = new URL(key, c.req.url).origin
c.set("path", path); c.set('path', path)
c.set("key", key); c.set('key', key)
c.set("shortUrl", shortUrl); c.set('shortUrl', shortUrl)
// backwards compat // backwards compat
// i could have left env.kv alone, but i like them being CAPITALS. // i could have left env.kv alone, but i like them being CAPITALS.
// what can i say? // what can i say?
if (c.env.KV == undefined && c.env.kv !== undefined){ if (c.env.KV == undefined && c.env.kv !== undefined) {
c.env.KV = c.env.kv c.env.KV = c.env.kv
console.warn('WARN: Please change your kv binding to be called KV.') console.warn('WARN: Please change your kv binding to be called KV.')
} }
await next(); await next()
}); })
// handle auth // handle auth
app.use("*", async (c, next) => { app.use('*', async (c, next) => {
if (c.env.WORKERLINKS_SECRET === undefined) { if (c.env.WORKERLINKS_SECRET === undefined) {
return c.text("Secret is not defined. Please add WORKERLINKS_SECRET."); return c.text('Secret is not defined. Please add WORKERLINKS_SECRET.')
} }
if ( if (
!["GET", "HEAD"].includes(c.req.method) && !['GET', 'HEAD'].includes(c.req.method) &&
c.req.headers.get("Authorization") !== c.env.WORKERLINKS_SECRET c.req.headers.get('Authorization') !== c.env.WORKERLINKS_SECRET
) { ) {
return c.json({ code: "401 Unauthorized", message: "Unauthorized" }, 401); return c.json({ code: '401 Unauthorized', message: 'Unauthorized' }, 401)
} }
await next(); await next()
}); })
// retrieve key // retrieve key
app.get("*", handleGetHead); app.get('*', handleGetHead)
// same but for HEAD // same but for HEAD
app.head("*", handleGetHead); app.head('*', handleGetHead)
// handle both GET and HEAD // handle both GET and HEAD
async function handleGetHead(c: Context) { async function handleGetHead(c: Context) {
// actual logic goes here // actual logic goes here
const urlResult = await c.env.KV.get(c.get("key")); const urlResult = await c.env.KV.get(c.get('key'))
if (urlResult == null) { if (urlResult == null) {
return c.json( return c.json(
{ code: "404 Not Found", message: " Key does not exist." }, { code: '404 Not Found', message: ' Key does not exist.' },
404 404,
); )
} else { } else {
const searchParams = new URL(c.req.url).searchParams; const searchParams = new URL(c.req.url).searchParams
const newUrl = new URL(urlResult); const newUrl = new URL(urlResult)
searchParams.forEach((value, key) => { searchParams.forEach((value, key) => {
newUrl.searchParams.append(key, value); newUrl.searchParams.append(key, value)
}); })
if (c.env.PLAUSIBLE_HOST !== undefined) { if (c.env.PLAUSIBLE_HOST !== undefined) {
c.executionCtx.waitUntil(sendToPlausible(c)); c.executionCtx.waitUntil(sendToPlausible(c))
} }
return c.redirect(newUrl.toString(), 302); return c.redirect(newUrl.toString(), 302)
} }
} }
// delete specific key // delete specific key
app.delete("*", async (c) => { app.delete('*', async (c) => {
const urlResult = await c.env.KV.get(c.get("key")); const urlResult = await c.env.KV.get(c.get('key'))
if (urlResult == null) { if (urlResult == null) {
return c.json( return c.json(
{ code: "404 Not Found", message: " Key does not exist." }, { code: '404 Not Found', message: ' Key does not exist.' },
404 404,
); )
} else { } else {
await c.env.KV.delete(c.get("key")); await c.env.KV.delete(c.get('key'))
return c.json({ return c.json({
message: "Short URL deleted succesfully.", message: 'Short URL deleted succesfully.',
key: c.get("key").slice(1), key: c.get('key').slice(1),
shorturl: c.get("shortUrl"), shorturl: c.get('shortUrl'),
longurl: urlResult, longurl: urlResult,
}); })
} }
}); })
// add specific key // add specific key
app.put("*", createLink); app.put('*', createLink)
// add random key // add random key
app.post("/", async (c) => { app.post('/', async (c) => {
const body = await c.req.text(); const body = await c.req.text()
if (!body) { if (!body) {
c.set("key", "/" + Math.random().toString(36).slice(5)); c.set('key', '/' + Math.random().toString(36).slice(5))
c.set('shortUrl', new URL(c.get('key'), c.req.url)) c.set('shortUrl', new URL(c.get('key'), c.req.url))
return await createLink(c); return await createLink(c)
} else { } else {
// bulk upload from body // bulk upload from body
// REMOVE THIS when bulk upload has been migrated // REMOVE THIS when bulk upload has been migrated
return c.json({ return c.json({
code: "501 Not Implemented", code: '501 Not Implemented',
message: "Bulk upload has not been reimplemented yet.", message: 'Bulk upload has not been reimplemented yet.',
}); })
// what type should this be??? // what type should this be???
let json: any; let json: any
try { try {
json = JSON.parse(body); json = JSON.parse(body)
} catch { } catch {
return c.json( return c.json(
{ {
code: "400 Bad Request", code: '400 Bad Request',
message: "Body must be valid JSON, or none at all.", message: 'Body must be valid JSON, or none at all.',
}, },
400 400,
); )
} }
// change this // change this
const valid = true; //validateBulkBody(json); const valid = true //validateBulkBody(json);
if (!valid) { if (!valid) {
return c.json( return c.json(
{ {
code: "400 Bad Request", code: '400 Bad Request',
message: "Body must be a standard JSON object mapping keys to urls.", message: 'Body must be a standard JSON object mapping keys to urls.',
example: { example: {
"/short": "https://example.com/really-really-really-long-1", '/short': 'https://example.com/really-really-really-long-1',
"/other": "https://subdomain.example.com/and-some-long-path", '/other': 'https://subdomain.example.com/and-some-long-path',
}, },
}, },
400 400,
); )
} }
for (const [key, url] of json.entries) { for (const [key, url] of json.entries) {
await c.env.KV.put(key, url); await c.env.KV.put(key, url)
} }
return Response.json( return Response.json(
{ {
message: "URLs created successfully", message: 'URLs created successfully',
entries: Object.entries(json).map(([key, longurl]) => ({ entries: Object.entries(json).map(([key, longurl]) => ({
key: key.slice(1), key: key.slice(1),
shorturl: new URL(key, c.req.url), shorturl: new URL(key, c.req.url),
longurl, longurl,
})), })),
}, },
{ status: 200 } { status: 200 },
); )
} }
}); })
async function createLink(c: Context) { async function createLink(c: Context) {
const url = c.req.headers.get("URL"); const url = c.req.headers.get('URL')
if (url == null || !validateUrl(url)) { if (url == null || !validateUrl(url)) {
return c.json( return c.json(
{ {
code: "400 Bad Request", code: '400 Bad Request',
message: "No valid URL given. Please set a 'URL' header.", message: "No valid URL given. Please set a 'URL' header.",
}, },
400 400,
); )
} }
await c.env.KV.put(c.get("key"), url); await c.env.KV.put(c.get('key'), url)
return Response.json( return Response.json(
{ {
message: "URL created succesfully.", message: 'URL created succesfully.',
key: c.get("key").slice(1), key: c.get('key').slice(1),
shorturl: c.get("shortUrl"), shorturl: c.get('shortUrl'),
longurl: url, longurl: url,
}, },
{ {
status: 200, status: 200,
} },
); )
} }
app.post("/*", async (c) => app.post('/*', async (c) =>
c.json( c.json(
{ {
code: "405 Method Not Allowed", code: '405 Method Not Allowed',
message: "POST not valid for individual keys. Did you mean PUT?", message: 'POST not valid for individual keys. Did you mean PUT?',
}, },
405 405,
) ),
); )
app.all("*", (c) => app.all('*', (c) =>
c.json( c.json(
{ {
code: "405 Method Not Allowed", code: '405 Method Not Allowed',
message: message:
"Unsupported method. Please use one of GET, PUT, POST, DELETE, HEAD.", 'Unsupported method. Please use one of GET, PUT, POST, DELETE, HEAD.',
}, },
405 405,
) ),
); )
export default app; export default app
// PLAUSIBLE_HOST should be the full URL to your Plausible Analytics instance // PLAUSIBLE_HOST should be the full URL to your Plausible Analytics instance
// e.g. https://plausible.io/ // e.g. https://plausible.io/
async function sendToPlausible(c: Context) { async function sendToPlausible(c: Context) {
const url = c.env.PLAUSIBLE_HOST + "api/event"; const url = c.env.PLAUSIBLE_HOST + 'api/event'
const headers = new Headers(); const headers = new Headers()
headers.append("User-Agent", c.req.headers.get("User-Agent") || ""); headers.append('User-Agent', c.req.headers.get('User-Agent') || '')
headers.append("X-Forwarded-For", c.req.headers.get("X-Forwarded-For") || ""); headers.append('X-Forwarded-For', c.req.headers.get('X-Forwarded-For') || '')
headers.append("Content-Type", "application/json"); headers.append('Content-Type', 'application/json')
const data = { const data = {
name: "pageview", name: 'pageview',
url: c.req.url, url: c.req.url,
domain: new URL(c.req.url).hostname, domain: new URL(c.req.url).hostname,
referrer: c.req.referrer, referrer: c.req.referrer,
}; }
await fetch(url, { method: "POST", headers, body: JSON.stringify(data) }); await fetch(url, { method: 'POST', headers, body: JSON.stringify(data) })
} }
function validateUrl(url: string) { function validateUrl(url: string) {
// quick and dirty validation // quick and dirty validation
if (url == "") { if (url == '') {
return false; return false
} }
try { try {
new URL(url); new URL(url)
} catch (TypeError) { } catch (TypeError) {
return false; return false
} }
return true; return true
} }
// zod and other validation libs too // zod and other validation libs too

View file

@ -1,14 +1,14 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"lib": ["esnext"], "lib": ["esnext"],
"types": ["@cloudflare/workers-types"], "types": ["@cloudflare/workers-types"],
"jsx": "react-jsx", "jsx": "react-jsx",
// "jsxFragmentFactory": "Fragment", // "jsxFragmentFactory": "Fragment",
"jsxImportSource": "hono/jsx" "jsxImportSource": "hono/jsx"
} }
} }