Use tabs for indentation, run prettier
This commit is contained in:
parent
c6b5d4d61c
commit
7383aa990f
5 changed files with 234 additions and 236 deletions
|
|
@ -2,6 +2,6 @@
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"tabWidth": 2,
|
"useTabs": true,
|
||||||
"printWidth": 80
|
"printWidth": 80
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
|
|
@ -1,5 +1,3 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["bungcip.better-toml"]
|
||||||
"bungcip.better-toml"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
228
src/index.ts
228
src/index.ts
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue