kopia lustrzana https://github.com/badgen/badgen.net
feat: new /memo badge built on Vercel KV
rodzic
6016e85e70
commit
548b1c3d74
49
api-/memo.ts
49
api-/memo.ts
|
@ -1,49 +0,0 @@
|
|||
import got from '../libs/got'
|
||||
import { createBadgenHandler, PathArgs } from '../libs/create-badgen-handler'
|
||||
|
||||
const help = `
|
||||
A badge with memories
|
||||
|
||||
## Usage
|
||||
|
||||
Update a badge with a \`PUT\` request:
|
||||
|
||||
curl -X PUT https://badgen.net/memo/a-public-writable-badge/coverage/75%25/orange
|
||||
|
||||
Then you have it:
|
||||
|
||||
https://badgen.net/memo/a-public-writable-badge
|
||||
|
||||

|
||||
|
||||
## Limits
|
||||
|
||||
Up to 1 write per second per badge.
|
||||
|
||||
## Caveat
|
||||
|
||||
Since everyone can write to any badge, it's recommended to add a uuid suffix to badge name:
|
||||
|
||||
https://badgen.net/memo/my-coverage-badge-df3ff1af-4703-435a-b4ea-20a38e711c7d
|
||||
|
||||
For uuid, you may grab one at https://uuid.now.sh
|
||||
`
|
||||
|
||||
export default createBadgenHandler({
|
||||
title: 'Memo',
|
||||
help,
|
||||
examples: {
|
||||
'/memo/deployed': 'memoized badge for deploy status',
|
||||
},
|
||||
handlers: {
|
||||
'/memo/:name': handler
|
||||
}
|
||||
})
|
||||
|
||||
async function handler ({ name }: PathArgs) {
|
||||
const endpoint = `https://badgen-store.amio.workers.dev/${name}`
|
||||
const data = await got(endpoint).json<any>()
|
||||
data.subject = data.subject || data.label
|
||||
|
||||
return data
|
||||
}
|
|
@ -3,7 +3,7 @@ import http from 'http'
|
|||
import matchRoute from 'my-way'
|
||||
|
||||
import { serveBadgeNext } from './serve-badge-next'
|
||||
import serveDoc from './serve-doc'
|
||||
import serveDoc from './serve-doc-next'
|
||||
import sentry from './sentry'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
@ -11,13 +11,14 @@ import type { BadgenParams } from './types'
|
|||
import { HTTPError } from 'got'
|
||||
|
||||
export type PathArgs = NonNullable<ReturnType<typeof matchRoute>>
|
||||
export type BadgenResult = Promise<BadgenParams>
|
||||
export type BadgenResponse = BadgenParams | string
|
||||
export type BadgenHandler = (pathArgs: PathArgs, req: NextApiRequest, res: NextApiResponse) => Promise<BadgenResponse>
|
||||
|
||||
export interface BadgenServeConfig {
|
||||
title: string;
|
||||
help?: string;
|
||||
examples: { [url: string]: string };
|
||||
handlers: { [pattern: string]: (pathArgs: PathArgs) => BadgenResult };
|
||||
handlers: { [pattern: string]: BadgenHandler };
|
||||
}
|
||||
|
||||
export function createBadgenHandler (badgenServerConfig: BadgenServeConfig) {
|
||||
|
@ -30,27 +31,32 @@ export function createBadgenHandler (badgenServerConfig: BadgenServeConfig) {
|
|||
return res.end()
|
||||
}
|
||||
|
||||
// Match badge handlers
|
||||
if (matchRoute('/:name', pathname)) {
|
||||
return serveDoc(badgenServerConfig)(req, res)
|
||||
}
|
||||
|
||||
// Find matched badgen handler
|
||||
let matchedArgs: PathArgs | null = null
|
||||
const matchedScheme = Object.keys(handlers).find(scheme => {
|
||||
return matchedArgs = matchRoute(scheme, decodeURI(pathname))
|
||||
})
|
||||
|
||||
// Invoke badge handler
|
||||
if (matchedArgs !== null && matchedScheme !== undefined) {
|
||||
return await handlers[matchedScheme](matchedArgs).then(params => {
|
||||
return serveBadgeNext(req, res, { params })
|
||||
}).catch(error => {
|
||||
const meta = { matchedArgs, matchedScheme }
|
||||
return onBadgeHandlerError(meta, error, req, res)
|
||||
})
|
||||
if (matchedArgs === null || matchedScheme === undefined) {
|
||||
res.status(404).end()
|
||||
return
|
||||
}
|
||||
|
||||
if (matchRoute('/:name', pathname)) {
|
||||
return serveDoc(badgenServerConfig)(req, res)
|
||||
// Invoke matched badgen handler
|
||||
const badgenHandler = handlers[matchedScheme]
|
||||
const badgenResponse = await badgenHandler(matchedArgs, req, res)
|
||||
.catch(error => parseBadgenHandlerError(error, req, res))
|
||||
|
||||
if (typeof badgenResponse === 'string') {
|
||||
res.end(badgenResponse)
|
||||
return
|
||||
}
|
||||
|
||||
res.status(404).end()
|
||||
serveBadgeNext(req, res, { params: badgenResponse })
|
||||
}
|
||||
|
||||
nextHandler.meta = { title, examples, help, handlers }
|
||||
|
@ -58,31 +64,31 @@ export function createBadgenHandler (badgenServerConfig: BadgenServeConfig) {
|
|||
return nextHandler
|
||||
}
|
||||
|
||||
function onBadgeHandlerError (meta: any, err: Error | HTTPError, req: NextApiRequest, res: NextApiResponse) {
|
||||
sentry.captureException(err)
|
||||
function parseBadgenHandlerError (error: Error | HTTPError, req: NextApiRequest, res: NextApiResponse): BadgenResponse {
|
||||
sentry.captureException(error)
|
||||
|
||||
console.error('BADGE_HANDLER_ERROR', err.message, req.url)
|
||||
console.error('BADGE_HANDLER_ERROR', req.url, error.message, error.stack)
|
||||
|
||||
// Send user friendly response
|
||||
const badgeName = req.url?.split('/')[1]
|
||||
|
||||
// Send user friendly badge response
|
||||
const errorBadgeParams = {
|
||||
subject: 'error',
|
||||
subject: badgeName || 'error',
|
||||
status: '500',
|
||||
color: 'red',
|
||||
}
|
||||
|
||||
if (err instanceof HTTPError) {
|
||||
errorBadgeParams.status = err.response.statusCode.toString()
|
||||
if (error instanceof HTTPError) {
|
||||
errorBadgeParams.status = error.response.statusCode.toString()
|
||||
}
|
||||
|
||||
if (err instanceof BadgenError) {
|
||||
errorBadgeParams.status = err.status
|
||||
if (error instanceof BadgenError) {
|
||||
errorBadgeParams.status = error.status
|
||||
}
|
||||
|
||||
res.setHeader('Error-Message', err.message)
|
||||
return serveBadgeNext(req, res, {
|
||||
code: 200,
|
||||
params: errorBadgeParams,
|
||||
})
|
||||
res.setHeader('Error-Message', error.message)
|
||||
|
||||
return errorBadgeParams
|
||||
}
|
||||
|
||||
function getBadgeStyle (req: http.IncomingMessage): string | undefined {
|
||||
|
|
|
@ -34,8 +34,11 @@ export function serveBadgeNext (req: NextApiRequest, res: NextApiResponse, optio
|
|||
const badgeSVGString = badgen(badgeParams)
|
||||
|
||||
// Minimum s-maxage is set to 300s(5m)
|
||||
const cacheMaxAge = cache ? Math.max(parseInt(String(cache)), 300) : sMaxAge
|
||||
res.setHeader('Cache-Control', `public, max-age=86400, s-maxage=${cacheMaxAge}, stale-while-revalidate=86400`)
|
||||
if (res.getHeader('cache-control') === undefined) {
|
||||
const cacheMaxAge = cache ? Math.max(parseInt(String(cache)), 300) : sMaxAge
|
||||
res.setHeader('cache-control', `public, max-age=86400, s-maxage=${cacheMaxAge}, stale-while-revalidate=86400`)
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||
res.statusCode = code
|
||||
res.send(badgeSVGString)
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
import http from 'http'
|
||||
import matchRoute from 'my-way'
|
||||
import { serveMarked } from 'serve-marked'
|
||||
import serve404 from './serve-404'
|
||||
import { BadgenServeConfig } from './create-badgen-handler-next'
|
||||
|
||||
const { GA_MEASUREMENT_ID = 'G-PD7EFJDYFV' } = process.env
|
||||
|
||||
export default function serveDoc (conf: BadgenServeConfig): http.RequestListener {
|
||||
return (req, res) => {
|
||||
const helpMarkdown = generateHelpMarkdown(conf)
|
||||
|
||||
if (helpMarkdown) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400')
|
||||
|
||||
return serveMarked(helpMarkdown, {
|
||||
title: `${conf.title} badge | Badgen`,
|
||||
inlineCSS,
|
||||
beforeHeadEnd: `
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${GA_MEASUREMENT_ID}');
|
||||
</script>
|
||||
`,
|
||||
beforeBodyEnd: helpFooter,
|
||||
})(req, res)
|
||||
}
|
||||
|
||||
serve404(req, res)
|
||||
}
|
||||
}
|
||||
|
||||
function generateHelpMarkdown ({ title, help, examples, handlers }: BadgenServeConfig): string {
|
||||
const mainTitle = `# ${title} Badge`
|
||||
|
||||
const customHelp = help || ''
|
||||
|
||||
const exampleTitle = `## Examples`
|
||||
|
||||
const routes = Object.keys(handlers)
|
||||
const categorizedExamples = Object.entries(examples).reduce((accu, [url, desc]) => {
|
||||
const scheme = routes.find(route => matchRoute(route, url))
|
||||
if (scheme) {
|
||||
accu[scheme] ? accu[scheme].push({ url, desc }) : accu[scheme] = [{ url, desc }]
|
||||
}
|
||||
return accu
|
||||
}, {})
|
||||
|
||||
const examplesSection = Object.entries(categorizedExamples).reduce((accu, [header, list]) => {
|
||||
const hash = hashify(header)
|
||||
const h4 = `<h4 id="${hash}"><a href="#${hash}"><code>${header.replace(/</g, '<')}</code></a></h4>`
|
||||
const ul = (list as Array<any>).reduce((acc, { url, desc }) => {
|
||||
return `${acc}\n-  [${url}](${url}) <i>${desc}</i>`
|
||||
}, '')
|
||||
return `${accu}\n\n${h4}\n\n${ul}`
|
||||
}, '')
|
||||
|
||||
return [mainTitle, customHelp, exampleTitle, examplesSection].join('\n\n')
|
||||
}
|
||||
|
||||
// turn `/github/:topic<commits|last-commit>/:owner/:repo/:ref?`
|
||||
// into `github-topic-commits-last-commit-owner-repo-ref`
|
||||
function hashify (str: string) {
|
||||
// return str.replace(/[^\w]/g, '')
|
||||
return str.split(/[^\w]+/).filter(Boolean).join('-')
|
||||
}
|
||||
|
||||
const inlineCSS = `
|
||||
html, body { scroll-behavior: smooth }
|
||||
.markdown-body { max-width: 960px; min-height: calc(100vh - 348px) }
|
||||
.markdown-body h1 { margin-bottom: 42px }
|
||||
li > img { vertical-align: middle; margin: 0.2em 0; font-size: 12px; float: right }
|
||||
li > img + a { font-family: monospace; font-size: 0.9em }
|
||||
li > img + a + i { color: #AAA }
|
||||
h4 a code { color: #333; font-size: 1rem }
|
||||
h4 a:hover { text-decoration: none !important }
|
||||
h4 { padding: 4px 0 }
|
||||
`
|
||||
|
||||
const helpFooter = `
|
||||
<footer>
|
||||
<div class='footer-content'>
|
||||
<div>
|
||||
<h3><img src='/statics/badgen-logo-w.svg' />Badgen Service</h3>
|
||||
<div class='sitemap'>
|
||||
<a href='https://badgen.net'>Classic</a>
|
||||
<em>/</em>
|
||||
<a href='https://flat.badgen.net'>Flat</a>
|
||||
<em>/</em>
|
||||
<a href='/builder'>Builder</a>
|
||||
<em>/</em>
|
||||
<a href='https://github.com/badgen/badgen.net'>GitHub</a>
|
||||
<em>/</em>
|
||||
<a href='https://twitter.com/badgen_net'>Twitter</a>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class='bottom'>
|
||||
<div>
|
||||
Built with ♥ by <a href='https://github.com/amio'>Amio</a> and awesome <a href='https://github.com/badgen/badgen.net/graphs/contributors'>contributors</a>. Powered by <a href='https://vercel.com'>Vercel</a>. License under <a href='https://github.com/badgen/badgen.net/blob/master/LICENSE.md'>ISC</a>.
|
||||
</div>
|
||||
<div class='links'>
|
||||
<a href='https://twitter.com/badgen_net'>
|
||||
<img src='https://simpleicons.now.sh/twitter/fff' />
|
||||
</a>
|
||||
<a href='https://github.com/badgen/badgen.net'>
|
||||
<img src='https://simpleicons.now.sh/github/fff' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
footer {
|
||||
margin-top: 5rem;
|
||||
background-color: #222;
|
||||
padding: 2rem 2rem;
|
||||
color: #777;
|
||||
font-size: 16px;
|
||||
}
|
||||
footer a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer-content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
footer h3 {
|
||||
font: 24px/32px Merriweather, serif;
|
||||
letter-spacing: 0.5px;
|
||||
color: #DDD;
|
||||
}
|
||||
footer h3 img {
|
||||
height: 21px;
|
||||
opacity: 0.8;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
footer .sitemap {
|
||||
line-height: 26px;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
footer .sitemap a {
|
||||
color: #999;
|
||||
font-family: Merriweather;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
footer a:hover {
|
||||
color: #EEE;
|
||||
text-decoration: underline;
|
||||
}
|
||||
footer .sitemap em {
|
||||
color: #555;
|
||||
margin: 0 0.6rem;
|
||||
}
|
||||
footer .bottom {
|
||||
margin-top: 2rem;
|
||||
border-top: 1px solid #444;
|
||||
padding-top: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 100px;
|
||||
}
|
||||
footer .bottom a {
|
||||
color: #999;
|
||||
}
|
||||
footer .links {
|
||||
text-align: right;
|
||||
}
|
||||
footer .links a {
|
||||
margin-left: 1em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
footer .links a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
footer .links img {
|
||||
height: 22px;
|
||||
width: 22px
|
||||
}
|
||||
</style>
|
||||
</footer>
|
||||
`
|
|
@ -24,27 +24,12 @@ const nextConfig = {
|
|||
},
|
||||
|
||||
async rewrites() {
|
||||
const liveBadgeRedirects = badgeList.live.map(badge => {
|
||||
return {
|
||||
source: `/${badge.id}/:path*`,
|
||||
destination: `/api/${badge.id}/:path*`,
|
||||
}
|
||||
})
|
||||
const staticBadgeRedirects = [{
|
||||
source: `/badge/:path*`,
|
||||
destination: `/api/badge/:path*`,
|
||||
}]
|
||||
|
||||
const badgeRedirects = [
|
||||
{ source: '/badge/:path*', destination: '/api/static' },
|
||||
{ source: '/badge', destination: '/api/static' },
|
||||
]
|
||||
|
||||
const badgeApis = [
|
||||
'/static',
|
||||
'/github',
|
||||
'/gitlab',
|
||||
'/https',
|
||||
'/memo',
|
||||
// registry
|
||||
'/amo',
|
||||
'/npm',
|
||||
|
@ -76,12 +61,14 @@ const nextConfig = {
|
|||
'/david',
|
||||
]
|
||||
|
||||
badgeApis.forEach(badge => {
|
||||
badgeRedirects.push({ source: `${badge}/:path*`, destination: `/api${badge}` }) // badges
|
||||
badgeRedirects.push({ source: badge, destination: `/api${badge}` }) // doc pages
|
||||
})
|
||||
let badgeRedirects = [
|
||||
{ source: '/badge/:path*', destination: '/api/static' },
|
||||
{ source: '/badge', destination: '/api/static' },
|
||||
]
|
||||
|
||||
// const badgeRedirects = liveBadgeRedirects.concat(staticBadgeRedirects)
|
||||
badgeRedirects = badgeRedirects
|
||||
.concat(badgeApis.map(badge => ({ source: `${badge}/:path*`, destination: `/api${badge}` }))) // badges
|
||||
.concat(badgeApis.map(badge => ({ source: badge, destination: `/api${badge}` }))) // doc pages
|
||||
|
||||
return badgeRedirects
|
||||
},
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@sentry/nextjs": "^7.37.1",
|
||||
"@sentry/tracing": "^7.36.0",
|
||||
"@vercel/analytics": "^1.0.1",
|
||||
"@vercel/kv": "^0.2.2",
|
||||
"badgen": "^3.2.2",
|
||||
"badgen-icons": "^0.22.0",
|
||||
"byte-size": "^8.1.0",
|
||||
|
@ -1373,11 +1374,30 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@upstash/redis": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.21.0.tgz",
|
||||
"integrity": "sha512-c6M+cl0LOgGK/7Gp6ooMkIZ1IDAJs8zFR+REPkoSkAq38o7CWFX5FYwYEqGZ6wJpUGBuEOr/7hTmippXGgL25A==",
|
||||
"dependencies": {
|
||||
"isomorphic-fetch": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/analytics": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.0.1.tgz",
|
||||
"integrity": "sha512-Ux0c9qUfkcPqng3vrR0GTrlQdqNJ2JREn/2ydrVuKwM3RtMfF2mWX31Ijqo1opSjNAq6rK76PwtANw6kl6TAow=="
|
||||
},
|
||||
"node_modules/@vercel/kv": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/kv/-/kv-0.2.2.tgz",
|
||||
"integrity": "sha512-mqnQOB6bkp4h5eObxfLNIlhlVqOGSH8cWOlC5pDVWTjX3zL8dETO1ZBl6M74HBmeBjbD5+J7wDJklRigY6UNKw==",
|
||||
"dependencies": {
|
||||
"@upstash/redis": "1.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||
|
@ -3804,6 +3824,15 @@
|
|||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/isomorphic-fetch": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
|
||||
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.1",
|
||||
"whatwg-fetch": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
@ -5638,6 +5667,11 @@
|
|||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-fetch": {
|
||||
"version": "3.6.17",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.17.tgz",
|
||||
"integrity": "sha512-c4ghIvG6th0eudYwKZY5keb81wtFz9/WeAHAoy8+r18kcWlitUIrmGFQ2rWEl4UCKUilD3zCLHOIPheHx5ypRQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"@sentry/nextjs": "^7.37.1",
|
||||
"@sentry/tracing": "^7.36.0",
|
||||
"@vercel/analytics": "^1.0.1",
|
||||
"@vercel/kv": "^0.2.2",
|
||||
"badgen": "^3.2.2",
|
||||
"badgen-icons": "^0.22.0",
|
||||
"byte-size": "^8.1.0",
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import { kv } from '@vercel/kv'
|
||||
import { createBadgenHandler, PathArgs, BadgenResponse } from '../../libs/create-badgen-handler-next'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
const help = `
|
||||
A badge with memory.
|
||||
|
||||
## Usage (public badge)
|
||||
|
||||
For any <code>/memo/:key</code> badge, like:
|
||||
|
||||
https://badgen.net/memo/my-badge-with-memory
|
||||
|
||||
you may update it with a <code>PUT</code> request:
|
||||
|
||||
curl -X PUT https://badgen.net/memo/my-badge-with-memory/:label/:status/:color
|
||||
|
||||
WARNING: anyone can update this badge, so use it with caution.
|
||||
|
||||
## Usage (protected badge)
|
||||
|
||||
If you want a protected badge (only you can update it), you may add an <code>Authorization: Bearer XXXXXX</code> header while setting it:
|
||||
|
||||
curl -X PUT --header "Authorization: Bearer <b>XXXXXX</b> https://badgen.net/memo/my-badge-with-memory/:label/:status/:color
|
||||
|
||||
A memo badge created with token can only be updated with the same token. Until it's expired.
|
||||
|
||||
## Expiration
|
||||
|
||||
A memo badge will be expired after <b>30 days</b> since it's modified, unless it get updated again within the period.
|
||||
|
||||
- When it's updated, it gets another 30 days lifespan.
|
||||
- When it's expired, it gets cleared like never exists.
|
||||
`
|
||||
|
||||
export default createBadgenHandler({
|
||||
title: 'Memo',
|
||||
help,
|
||||
examples: {
|
||||
'/memo/deployed': 'memoized badge for deploy status',
|
||||
},
|
||||
handlers: {
|
||||
'/memo/:key': handler,
|
||||
'/memo/:key/:label/:status/:color': putHandler
|
||||
}
|
||||
})
|
||||
|
||||
const MEMOIZED_TTL_SECONDS = 60 // One Week
|
||||
|
||||
type MemoizedBadgeItem = {
|
||||
token: string;
|
||||
params: {
|
||||
label: string;
|
||||
status: string;
|
||||
color: string;
|
||||
}
|
||||
}
|
||||
|
||||
async function handler ({ key }: PathArgs, req: NextApiRequest, res: NextApiResponse): Promise<BadgenResponse> {
|
||||
const storedData = await kv.get<MemoizedBadgeItem>(key)
|
||||
|
||||
if (storedData === null || storedData === undefined) {
|
||||
res.setHeader('cache-control', `s-maxage=1, stale-while-revalidate=1`)
|
||||
|
||||
return {
|
||||
subject: key,
|
||||
status: '404',
|
||||
color: 'grey'
|
||||
}
|
||||
} else {
|
||||
const ttl = await kv.ttl(key)
|
||||
res.setHeader('cache-control', `max-age=${ttl}, s-maxage=300, stale-while-revalidate=86400`)
|
||||
|
||||
const { label, status, color } = storedData.params
|
||||
return { subject: label, status, color }
|
||||
}
|
||||
}
|
||||
|
||||
async function putHandler (args: PathArgs, req: NextApiRequest, res: NextApiResponse): Promise<BadgenResponse> {
|
||||
// Only accept PUT request
|
||||
if (req.method !== 'PUT') {
|
||||
res.setHeader('Allow', 'PUT')
|
||||
res.status(405)
|
||||
return 'Method Not Allowed'
|
||||
}
|
||||
|
||||
// If no token(authorization) is provided,
|
||||
// we will use a default one, which means this badge is public writable.
|
||||
const token = req.headers['authorization'] || 'Bearer NO_TOKEN_PROVIDED'
|
||||
|
||||
// Validate token format
|
||||
if (!token.startsWith('Bearer ')) {
|
||||
res.status(401)
|
||||
return 'Invalid token format'
|
||||
}
|
||||
|
||||
const { key, label, status, color } = args
|
||||
|
||||
const newData: MemoizedBadgeItem = { token, params: { label, status, color }}
|
||||
|
||||
const storedData = await kv.get<MemoizedBadgeItem>(key)
|
||||
|
||||
if (storedData === null || storedData.token === token) {
|
||||
// If the key is not found, or found and token is validate, ser/update data and ttl
|
||||
await kv.set(key, newData, { ex: MEMOIZED_TTL_SECONDS })
|
||||
return JSON.stringify(newData.params)
|
||||
} else {
|
||||
// The key is found but token is invalid, refuse to update the data
|
||||
res.status(401)
|
||||
return 'Unauthorized'
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,7 @@ import { basename, extname } from 'path'
|
|||
import { version, versionColor } from '../../libs/utils'
|
||||
import { createBadgenHandler } from '../../libs/create-badgen-handler-next'
|
||||
|
||||
import type { PathArgs, BadgenResult } from '../../libs/create-badgen-handler-next'
|
||||
import type { PathArgs, BadgenResponse } from '../../libs/create-badgen-handler-next'
|
||||
|
||||
const WINGET_GITHUB_REPO = 'microsoft/winget-pkgs'
|
||||
|
||||
|
@ -106,7 +106,7 @@ export default createBadgenHandler({
|
|||
}
|
||||
})
|
||||
|
||||
async function handler ({ topic, appId }: PathArgs): BadgenResult {
|
||||
async function handler ({ topic, appId }: PathArgs): Promise<BadgenResponse> {
|
||||
switch (topic) {
|
||||
case 'v': {
|
||||
const versions = await fetchVersions(appId)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import got from '../../libs/got'
|
||||
import { createBadgenHandler } from '../../libs/create-badgen-handler-next'
|
||||
|
||||
import type { PathArgs, BadgenResult } from '../../libs/create-badgen-handler-next'
|
||||
import type { PathArgs, BadgenResponse } from '../../libs/create-badgen-handler-next'
|
||||
|
||||
export default createBadgenHandler({
|
||||
title: 'XO',
|
||||
|
@ -24,7 +24,7 @@ const getIndent = space => {
|
|||
return `${space} spaces`
|
||||
}
|
||||
|
||||
async function handler ({ topic, scope, name }: PathArgs): BadgenResult {
|
||||
async function handler ({ topic, scope, name }: PathArgs): Promise<BadgenResponse> {
|
||||
const pkg = scope ? `${scope}/${name}` : name
|
||||
const endpoint = `https://cdn.jsdelivr.net/npm/${pkg}/package.json`
|
||||
const data = await got(endpoint).json<any>()
|
||||
|
|
|
@ -108,10 +108,6 @@
|
|||
"source": "/melpa/:match*",
|
||||
"destination": "https://v2022.badgen.net/melpa/:match*"
|
||||
},
|
||||
{
|
||||
"source": "/memo/:match*",
|
||||
"destination": "https://v2022.badgen.net/memo/:match*"
|
||||
},
|
||||
{
|
||||
"source": "/nuget/:match*",
|
||||
"destination": "https://v2022.badgen.net/nuget/:match*"
|
||||
|
|
Ładowanie…
Reference in New Issue