Add Logbuch: project update blog with admin, media and API
Public archive with sidebar navigation, project pages, month archive, search and entry pages with image blocks. Admin area for brands, projects, post types, users, api clients, media and the entry editor with drag and drop images, preview per audience, scheduling and publish checks. Read and write API with bearer tokens, audience scoping, idempotent creation, OpenAPI document and editorial guide. Magic link login with configurable allowed domains, whole app behind the session gate. 456 tests including design rule checks.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { findActiveClientByTokenHash, listClients } from '~/data/repositories/clients'
|
||||
import { performCreateClient, performRevokeClient, tokenPrefix } from '~/lib/admin-clients'
|
||||
import { hashToken } from '~/lib/api-auth'
|
||||
import {
|
||||
adminViewer,
|
||||
auditEntries,
|
||||
form,
|
||||
makeBrand,
|
||||
makeProject,
|
||||
moderatorViewer,
|
||||
resetAccounts,
|
||||
} from '../support/admin'
|
||||
|
||||
beforeEach(resetAccounts)
|
||||
|
||||
describe('performCreateClient', () => {
|
||||
it('legt einen Zugang an, zeigt das Token einmal und speichert nur den Hash', async () => {
|
||||
const brand = await makeBrand()
|
||||
const project = await makeProject(brand.id)
|
||||
|
||||
const state = await performCreateClient(
|
||||
adminViewer(),
|
||||
form({ name: 'Trakk Web', mode: 'read', scope: 'customer', projectId: project.id }),
|
||||
)
|
||||
|
||||
expect(state.status).toBe('ok')
|
||||
expect(state.token?.startsWith(tokenPrefix)).toBe(true)
|
||||
|
||||
const clients = await listClients()
|
||||
|
||||
expect(clients).toHaveLength(1)
|
||||
expect(clients[0]).toMatchObject({
|
||||
name: 'Trakk Web',
|
||||
mode: 'read',
|
||||
scope: 'customer',
|
||||
projectId: project.id,
|
||||
canPublish: false,
|
||||
revokedAt: null,
|
||||
})
|
||||
expect(clients[0]?.tokenHash).not.toBe(state.token)
|
||||
expect(clients[0]?.tokenHash).toBe(hashToken(state.token!))
|
||||
|
||||
const found = await findActiveClientByTokenHash(hashToken(state.token!))
|
||||
|
||||
expect(found?.id).toBe(clients[0]?.id)
|
||||
})
|
||||
|
||||
it('protokolliert das Anlegen ohne das Token', async () => {
|
||||
await performCreateClient(adminViewer(), form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }))
|
||||
|
||||
const entries = await auditEntries()
|
||||
|
||||
expect(entries).toHaveLength(1)
|
||||
expect(entries[0]).toMatchObject({ action: 'client.create', entity: 'api_client' })
|
||||
expect(JSON.stringify(entries[0]?.data)).not.toContain(tokenPrefix)
|
||||
})
|
||||
|
||||
it('lässt die Projektbindung leer, wenn kein Projekt gewählt ist', async () => {
|
||||
const state = await performCreateClient(
|
||||
adminViewer(),
|
||||
form({ name: 'Alle Projekte', mode: 'read', scope: 'internal', projectId: '' }),
|
||||
)
|
||||
|
||||
expect(state.status).toBe('ok')
|
||||
expect((await listClients())[0]?.projectId).toBeNull()
|
||||
})
|
||||
|
||||
it('lehnt ein unbekanntes Projekt ab', async () => {
|
||||
const state = await performCreateClient(
|
||||
adminViewer(),
|
||||
form({
|
||||
name: 'Fremd',
|
||||
mode: 'read',
|
||||
scope: 'customer',
|
||||
projectId: '11111111-1111-4111-8111-111111111111',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(state.fields?.projectId).toBe('projectUnknown')
|
||||
expect(await listClients()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('lehnt einen unbekannten Modus ab', async () => {
|
||||
const state = await performCreateClient(
|
||||
adminViewer(),
|
||||
form({ name: 'Fremd', mode: 'alles', scope: 'customer' }),
|
||||
)
|
||||
|
||||
expect(state.fields?.mode).toBe('modeInvalid')
|
||||
})
|
||||
|
||||
it('lässt einen Moderator keinen Zugang anlegen', async () => {
|
||||
const brand = await makeBrand()
|
||||
const project = await makeProject(brand.id)
|
||||
|
||||
const state = await performCreateClient(
|
||||
moderatorViewer([project.id]),
|
||||
form({ name: 'Eigener Zugang', mode: 'read', scope: 'customer', projectId: project.id }),
|
||||
)
|
||||
|
||||
expect(state.message).toBe('forbidden')
|
||||
expect(await listClients()).toHaveLength(0)
|
||||
expect(await auditEntries()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('performRevokeClient', () => {
|
||||
it('widerruft einen Zugang und protokolliert das', async () => {
|
||||
const created = await performCreateClient(
|
||||
adminViewer(),
|
||||
form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }),
|
||||
)
|
||||
|
||||
const state = await performRevokeClient(adminViewer(), form({ id: created.id! }))
|
||||
|
||||
expect(state.status).toBe('ok')
|
||||
|
||||
const clients = await listClients()
|
||||
|
||||
expect(clients[0]?.revokedAt).toBeInstanceOf(Date)
|
||||
expect((await auditEntries())[0]?.action).toBe('client.revoke')
|
||||
})
|
||||
|
||||
it('lässt ein widerrufenes Token nicht mehr durch', async () => {
|
||||
const created = await performCreateClient(
|
||||
adminViewer(),
|
||||
form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }),
|
||||
)
|
||||
|
||||
await performRevokeClient(adminViewer(), form({ id: created.id! }))
|
||||
|
||||
expect(await findActiveClientByTokenHash(hashToken(created.token!))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('meldet einen zweiten Widerruf als unbekannt', async () => {
|
||||
const created = await performCreateClient(
|
||||
adminViewer(),
|
||||
form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }),
|
||||
)
|
||||
|
||||
await performRevokeClient(adminViewer(), form({ id: created.id! }))
|
||||
const state = await performRevokeClient(adminViewer(), form({ id: created.id! }))
|
||||
|
||||
expect(state.message).toBe('notFound')
|
||||
})
|
||||
|
||||
it('lässt einen Moderator keinen Zugang widerrufen', async () => {
|
||||
const created = await performCreateClient(
|
||||
adminViewer(),
|
||||
form({ name: 'Trakk Web', mode: 'read', scope: 'customer' }),
|
||||
)
|
||||
|
||||
const state = await performRevokeClient(moderatorViewer(), form({ id: created.id! }))
|
||||
|
||||
expect(state.message).toBe('forbidden')
|
||||
expect((await listClients())[0]?.revokedAt).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user