Fix sharp in container, add admin downloads and anchor mark
- ship libvips with the runtime image so POST /api/v1/media stops failing - admin downloads openapi.json and the style guide via session, no token - copy rows for both API addresses - anchor mark in the wordmark and a favicon
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { openApiResponse } from '~/lib/api-docs'
|
||||
import { requireAdminArea } from '~/lib/auth-guards'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
await requireAdminArea()
|
||||
|
||||
const url = new URL(request.url)
|
||||
|
||||
return openApiResponse(process.env.BETTER_AUTH_URL ?? url.origin)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { readStyleGuide, styleGuideResponse } from '~/lib/api-docs'
|
||||
import { requireAdminArea } from '~/lib/auth-guards'
|
||||
|
||||
export async function GET() {
|
||||
await requireAdminArea()
|
||||
|
||||
const text = await readStyleGuide()
|
||||
|
||||
if (text === null) {
|
||||
return new Response('docs/style-guide.md fehlt.', { status: 500 })
|
||||
}
|
||||
|
||||
return styleGuideResponse(text, true)
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { headers } from 'next/headers'
|
||||
import Link from 'next/link'
|
||||
import { TbBook, TbDownload } from 'react-icons/tb'
|
||||
import { AdminHeading } from '~/components/admin/AdminHeading'
|
||||
import { ApiEndpoint } from '~/components/admin/ApiEndpoint'
|
||||
import { cellClass, headCellClass } from '~/components/admin/styles'
|
||||
import { CopyButton } from '~/components/ui/CopyButton'
|
||||
import { SectionLabel } from '~/components/ui/SectionLabel'
|
||||
import { Scroller } from '~/components/ui/Scroller'
|
||||
import { blockTypes } from '~/domain/blocks'
|
||||
import { listActivePostTypes } from '~/data/repositories/post-types'
|
||||
import { requireAdminArea } from '~/lib/auth-guards'
|
||||
import { adminClientsPath } from '~/lib/admin-routes'
|
||||
import { adminApiFilePath, adminClientsPath } from '~/lib/admin-routes'
|
||||
|
||||
export const metadata: Metadata = { title: 'API - Logbuch' }
|
||||
|
||||
@@ -35,6 +37,12 @@ export default async function AdminApiPage() {
|
||||
await requireAdminArea()
|
||||
|
||||
const types = await listActivePostTypes()
|
||||
const host = (await headers()).get('host')
|
||||
const origin = process.env.BETTER_AUTH_URL ?? (host ? `https://${host}` : '')
|
||||
const files = [
|
||||
{ label: 'OpenAPI', url: `${origin}/api/v1/openapi.json` },
|
||||
{ label: 'Leitfaden', url: `${origin}/api/v1/style-guide` },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 pt-12">
|
||||
@@ -70,25 +78,39 @@ export default async function AdminApiPage() {
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<a
|
||||
href="/api/v1/openapi.json"
|
||||
href={adminApiFilePath('openapi')}
|
||||
download
|
||||
className="inline-flex items-center gap-2 border border-rule bg-surface px-3 py-2 font-mono text-micro font-semibold uppercase tracking-label text-ink no-underline hover:border-ink-3"
|
||||
>
|
||||
<TbDownload aria-hidden="true" className="size-4" />
|
||||
openapi.json
|
||||
openapi.json laden
|
||||
</a>
|
||||
<a
|
||||
href="/api/v1/style-guide"
|
||||
href={adminApiFilePath('style-guide')}
|
||||
download
|
||||
className="inline-flex items-center gap-2 border border-rule bg-surface px-3 py-2 font-mono text-micro font-semibold uppercase tracking-label text-ink no-underline hover:border-ink-3"
|
||||
>
|
||||
<TbBook aria-hidden="true" className="size-4" />
|
||||
Redaktionsleitfaden
|
||||
Leitfaden laden
|
||||
</a>
|
||||
</div>
|
||||
<p className="m-0 text-small">
|
||||
Beide Adressen verlangen ein Token. In Postman oder Bruno trägst du es unter Authorization als
|
||||
Bearer ein.
|
||||
Angemeldet als Admin lädst du beide Dateien direkt herunter. Über die Schnittstelle verlangen
|
||||
sie ein Token, in Postman oder Bruno unter Authorization als Bearer.
|
||||
</p>
|
||||
<div className="flex flex-col divide-y divide-rule border-y border-rule">
|
||||
{files.map(file => (
|
||||
<div key={file.url} className="flex flex-wrap items-center justify-between gap-3 py-3">
|
||||
<span className="flex min-w-0 flex-col gap-1">
|
||||
<span className="font-display text-micro font-semibold uppercase tracking-label text-ink-3">
|
||||
{file.label}
|
||||
</span>
|
||||
<code className="block break-all font-mono text-small text-ink">{file.url}</code>
|
||||
</span>
|
||||
<CopyButton value={file.url} label="Adresse kopieren" doneLabel="Kopiert" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { resolveClient } from '~/lib/api-auth'
|
||||
import { openApiDocument } from '~/lib/openapi'
|
||||
import { openApiResponse } from '~/lib/api-docs'
|
||||
import { problem } from '~/lib/problem'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
@@ -10,12 +10,6 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
const url = new URL(request.url)
|
||||
const document = openApiDocument(process.env.BETTER_AUTH_URL ?? url.origin)
|
||||
|
||||
return new Response(JSON.stringify(document, null, '\t'), {
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'content-disposition': 'attachment; filename="logbuch-openapi.json"',
|
||||
},
|
||||
})
|
||||
return openApiResponse(process.env.BETTER_AUTH_URL ?? url.origin)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { resolveClient } from '~/lib/api-auth'
|
||||
import { readStyleGuide, styleGuideResponse } from '~/lib/api-docs'
|
||||
import { problem } from '~/lib/problem'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
@@ -10,13 +9,11 @@ export async function GET(request: Request) {
|
||||
return problem(401, 'unauthorized')
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await readFile(join(process.cwd(), 'docs', 'style-guide.md'), 'utf8')
|
||||
const text = await readStyleGuide()
|
||||
|
||||
return new Response(text, {
|
||||
headers: { 'content-type': 'text/markdown; charset=utf-8' },
|
||||
})
|
||||
} catch {
|
||||
if (text === null) {
|
||||
return problem(500, 'style_guide_missing', 'docs/style-guide.md fehlt.')
|
||||
}
|
||||
|
||||
return styleGuideResponse(text)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<rect width="32" height="32" fill="#14161a" />
|
||||
<g
|
||||
transform="translate(4 4)"
|
||||
fill="none"
|
||||
stroke="#f2f1ec"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 9v12m-8 -8a8 8 0 0 0 16 0m1 0h-2m-14 0h-2" />
|
||||
<path d="M9 6a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 394 B |
@@ -1,5 +1,6 @@
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { TbAnchor } from 'react-icons/tb'
|
||||
import { entryNumber } from '~/components/ui/Plate'
|
||||
import { overviewPath } from '~/lib/routes'
|
||||
|
||||
@@ -15,19 +16,24 @@ export function Wordmark({ number, className }: WordmarkProps) {
|
||||
return (
|
||||
<Link
|
||||
href={overviewPath()}
|
||||
className={`inline-flex items-baseline gap-2 no-underline ${className ?? ''}`}
|
||||
className={`inline-flex items-center gap-2.5 no-underline ${className ?? ''}`}
|
||||
>
|
||||
<span className="font-display text-lead font-bold uppercase leading-none tracking-tight text-ink">
|
||||
{app('name')}
|
||||
<span className="flex size-7 shrink-0 items-center justify-center bg-ink text-paper">
|
||||
<TbAnchor aria-hidden="true" className="size-4" />
|
||||
</span>
|
||||
{number !== undefined ? (
|
||||
<span
|
||||
title={nav('state', { number: entryNumber(number) })}
|
||||
className="font-mono text-micro leading-none text-ink-3 tabular-nums"
|
||||
>
|
||||
{entryNumber(number)}
|
||||
<span className="inline-flex items-baseline gap-2">
|
||||
<span className="font-display text-lead font-bold uppercase leading-none tracking-tight text-ink">
|
||||
{app('name')}
|
||||
</span>
|
||||
) : null}
|
||||
{number !== undefined ? (
|
||||
<span
|
||||
title={nav('state', { number: entryNumber(number) })}
|
||||
className="font-mono text-micro leading-none text-ink-3 tabular-nums"
|
||||
>
|
||||
{entryNumber(number)}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { TbCheck, TbCopy } from 'react-icons/tb'
|
||||
import { ghostButtonClass } from '~/components/admin/styles'
|
||||
|
||||
export type CopyButtonProps = {
|
||||
value: string
|
||||
label: string
|
||||
doneLabel: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CopyButton({ value, label, doneLabel, className }: CopyButtonProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!copied) {
|
||||
return
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => setCopied(false), 2000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [copied])
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
setCopied(true)
|
||||
} catch {
|
||||
setCopied(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" onClick={copy} className={`${ghostButtonClass} ${className ?? ''}`}>
|
||||
{copied ? (
|
||||
<TbCheck aria-hidden="true" className="size-4 shrink-0" />
|
||||
) : (
|
||||
<TbCopy aria-hidden="true" className="size-4 shrink-0" />
|
||||
)}
|
||||
{copied ? doneLabel : label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,10 @@ export const adminClientsPath = '/admin/clients' as Route
|
||||
export const adminPostTypesPath = '/admin/post-types' as Route
|
||||
export const adminNewPostTypePath = '/admin/post-types/new' as Route
|
||||
|
||||
export function adminApiFilePath(file: 'openapi' | 'style-guide'): Route {
|
||||
return `/admin/api/files/${file}` as Route
|
||||
}
|
||||
|
||||
export function adminPostTypePath(id: string): Route {
|
||||
return `/admin/post-types/${encodeURIComponent(id)}` as Route
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { openApiDocument } from './openapi'
|
||||
|
||||
export function openApiBody(origin: string): string {
|
||||
return JSON.stringify(openApiDocument(origin), null, '\t')
|
||||
}
|
||||
|
||||
export function openApiResponse(origin: string): Response {
|
||||
return new Response(openApiBody(origin), {
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'content-disposition': 'attachment; filename="logbuch-openapi.json"',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function readStyleGuide(): Promise<string | null> {
|
||||
try {
|
||||
return await readFile(join(process.cwd(), 'docs', 'style-guide.md'), 'utf8')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function styleGuideResponse(text: string, download = false): Response {
|
||||
const headers: Record<string, string> = { 'content-type': 'text/markdown; charset=utf-8' }
|
||||
|
||||
if (download) {
|
||||
headers['content-disposition'] = 'attachment; filename="logbuch-redaktionsleitfaden.md"'
|
||||
}
|
||||
|
||||
return new Response(text, { headers })
|
||||
}
|
||||
Reference in New Issue
Block a user