Merge pull request #8 from Ovyerus/feat/bulk-create

Feat/bulk create
This commit is contained in:
Erisa A 2023-01-26 20:14:15 +00:00 committed by GitHub
commit 5556cea72f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 114 additions and 16 deletions

1
.gitignore vendored
View file

@ -9,3 +9,4 @@ worker/
node_modules/ node_modules/
.cargo-ok .cargo-ok
wrangler.prod.toml wrangler.prod.toml
.mf

View file

@ -55,7 +55,7 @@ server: cloudflare
..other ephemeral headers.. ..other ephemeral headers..
``` ```
To create or update a custom short URl, send a `PUT` to the intended target URL: To create or update a custom short URL, send a `PUT` to the intended target URL:
```json ```json
erisa@Tuturu:~$ curl -X PUT -H "Authorization: mysecret" -H "URL: https://erisa.uk" https://erisa.link/mywebsite erisa@Tuturu:~$ curl -X PUT -H "Authorization: mysecret" -H "URL: https://erisa.uk" https://erisa.link/mywebsite
@ -79,15 +79,38 @@ erisa@Tuturu:~$ curl -X DELETE -H "Authorization: mysecret" https://erisa.link/k
} }
``` ```
You can also bulk create multiple shortlinks at once by sending a `POST` to `/` with no `URL` header and with a JSON body instead:
```json
erisa@Tuturu:~$ curl -X POST -H "Authorization: mysecret" https://erisa.link/ \
-H 'Content-Type: application/json' \
-d '{ "/short1": "https://example.com", "/mywebsite": "https://erisa.uk" }'
{
"message": "URLs created successfully",
"entries": [
{
"key": "short1",
"shorturl": "https://erisa.link/short1",
"longurl": "https://example.com"
},
{
"key": "mywebsite",
"shorturl": "http://erisa.link/mywebsite",
"longurl": "https://erisa.uk"
}
]
}
```
It is a planned feature to be able to list all URLs via a `GET` on `/` with `Authorization`. It is a planned feature to be able to list all URLs via a `GET` on `/` with `Authorization`.
For the time being you can view them from your Cloudflare Dashboard: For the time being you can view them from your Cloudflare Dashboard:
Cloudflare Dashboard -> Workers -> KV -> View on the namespace. Cloudflare Dashboard -> Workers -> KV -> View on the namespace.
## Plausible Analytics ## Plausible Analytics
To get statistics for your short URLs with Plausible Analytics, define a `PLAUSIBLE_HOST` secret set to the URL of your Plausible instance. For example, `https://plausible.io/`. To get statistics for your short URLs with Plausible Analytics, define a `PLAUSIBLE_HOST` secret set to the URL of your Plausible instance. For example, `https://plausible.io/`.
## Security ## Security
This code is relatively simple but still, if you find any security issues that can be exploited publicly, please reach out to me via email: `erisa (at) erisa.uk` with any relevant details. This code is relatively simple but still, if you find any security issues that can be exploited publicly, please reach out to me via email: `erisa (at) erisa.uk` with any relevant details.

100
index.js
View file

@ -19,8 +19,8 @@ async function handleRequest(event) {
let path = new URL(request.url).pathname let path = new URL(request.url).pathname
// Trim trailing slash // Trim trailing slash
let key = path !== '/' ? path.replace(/\/$/, '') : path; let key = path !== '/' ? path.replace(/\/$/, '') : path
let shorturl = new URL(request.url).origin + key let shorturl = new URL(key, request.url).origin
if (request.method == 'PUT') { if (request.method == 'PUT') {
return await putLink( return await putLink(
@ -41,14 +41,77 @@ async function handleRequest(event) {
}, },
) )
} }
key = '/' + Math.random().toString(36).slice(5) const body = await request.text()
shorturl = new URL(request.url).origin + key
return await putLink( if (!body) {
request.headers.get('Authorization'), key = '/' + Math.random().toString(36).slice(5)
shorturl, shorturl = new URL(key, request.url)
key, return await putLink(
request.headers.get('URL'), request.headers.get('Authorization'),
) shorturl,
key,
request.headers.get('URL'),
)
} else {
if (request.headers.get('Authorization') != secret) {
return Response.json(
{
code: '401 Unauthorized',
message: 'Unauthorized.',
},
{
status: 401,
},
)
}
let json
try {
json = JSON.parse(body)
} catch {
return Response.json(
{
code: '400 Bad Request',
message: 'Body must be valid JSON, or none at all.',
},
{
status: 400,
},
)
}
const valid = validateBulkBody(json)
if (!valid) {
return Response.json(
{
code: '400 Bad Request',
message:
'Body must be a standard JSON object mapping keys to urls.',
example: {
'/short': 'https://example.com/really-really-really-long-1',
'/other': 'https://subdomain.example.com/and-some-long-path',
},
},
{ status: 400 },
)
}
for (const [key, url] of Object.entries(json)) {
await kv.put(key, url)
}
return Response.json(
{
message: 'URLs created successfully',
entries: Object.entries(json).map(([key, longurl]) => ({
key: key.slice(1),
shorturl: new URL(key, request.url),
longurl,
})),
},
{ status: 200 },
)
}
} else if (request.method == 'GET' || request.method == 'HEAD') { } else if (request.method == 'GET' || request.method == 'HEAD') {
let url = await kv.get(key) let url = await kv.get(key)
if (url == null) { if (url == null) {
@ -106,7 +169,7 @@ async function handleRequest(event) {
) )
} }
shorturl = new URL(request.url).origin + key shorturl = new URL(key, request.url)
let url = await kv.get(key) let url = await kv.get(key)
if (url == null) { if (url == null) {
return Response.json( return Response.json(
@ -123,7 +186,7 @@ async function handleRequest(event) {
return Response.json( return Response.json(
{ {
message: 'Short URL deleted succesfully.', message: 'Short URL deleted succesfully.',
key: key.substr(1), key: key.slice(1),
shorturl: shorturl, shorturl: shorturl,
longurl: url, longurl: url,
}, },
@ -159,6 +222,17 @@ function validateUrl(url) {
return true return true
} }
// zod and other validation libs too
function validateBulkBody(body) {
// Starting `/` and no ending `/`
const keyRe = /^\/.*?[^\/]$/
if (!body || typeof body !== 'object' || Array.isArray(body)) return false
return Object.entries(body).every(
([key, url]) => keyRe.test(key) && validateUrl(url),
)
}
async function putLink(givenSecret, shorturl, key, url) { async function putLink(givenSecret, shorturl, key, url) {
if (givenSecret != secret) { if (givenSecret != secret) {
return Response.json( return Response.json(
@ -188,7 +262,7 @@ async function putLink(givenSecret, shorturl, key, url) {
return Response.json( return Response.json(
{ {
message: 'URL created succesfully.', message: 'URL created succesfully.',
key: key.substr(1), key: key.slice(1),
shorturl: shorturl, shorturl: shorturl,
longurl: url, longurl: url,
}, },