Uloziste pro vsechna data, oprava .gitignore, dodelany navrh rozsireni
.gitignore mel vzorec `data/`, ktery se shodl i se `src/data/`. Sestnact zdrojovych souboru tim tise chybelo v gitu vcetne cele slozky `src/data/store/`. Opraveno na `/data/`, stejne v .dockerignore. Tickety vcetne logu, automatizace, incidenty a rozlozeni dashboardu se po kazde zmene ukladaji. Pomocnik `withMirror` je opak `withCache`: data se meni v pameti a zapisuji cela, misto aby se po zapisu znovu nacitala. Citace ID se pri startu dopocitaji z ulozenych zaznamu, takze novy ticket neprepise stary. Detail ticketu umi typ, tagy, vlastni pole typu a prehozeni na skupinu. Nastaveni ma prepnuti spravce na jiny ucet, vychozi jen pro cteni. Skupiny resitelu chodi spolu s lidmi jednim requestem. Dokumentace: rejstrik znovupouzitelnych funkci (15), navrh monetizace a ceny za krok (16), popis nastaveni a prav (17). Doplneny endpointy do openapi.ts, petice CRUD rout se generuje jednou funkci. Overeno v rezimu souboru: zmeny prezily tvrde ukonceni procesu a po restartu byly zpatky vcetne logu ticketu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e7cf499a0b
commit
afbe948da3
+480
@@ -1,5 +1,110 @@
|
||||
import { config } from './config.js';
|
||||
|
||||
/**
|
||||
* Sprava zaznamu ma u kazde entity stejnou petici endpointu, protoze ji na
|
||||
* serveru dela jedna fabrika (`routes/crud.ts`). Popisovat ji devetkrat rucne
|
||||
* by znamenalo devet mist, ktere se casem rozejdou.
|
||||
*/
|
||||
function crudPaths(entity: {
|
||||
/** Cast cesty, napr. `roles`. */
|
||||
path: string;
|
||||
/** Jak se o tom mluvi v popisu, napr. `roli`. */
|
||||
label: string;
|
||||
/** Pravo, ktere je na zapis potreba. */
|
||||
permission: string;
|
||||
}) {
|
||||
const id = { name: 'id', in: 'path', required: true, schema: { type: 'string' } };
|
||||
const body = {
|
||||
required: true,
|
||||
content: { 'application/json': { schema: { type: 'object' } } },
|
||||
};
|
||||
const record = {
|
||||
description: 'Zaznam',
|
||||
content: { 'application/json': { schema: { type: 'object' } } },
|
||||
};
|
||||
const denied = { '403': { description: `Chybi pravo ${entity.permission}` } };
|
||||
const base = `/api/dashboard/settings/${entity.path}`;
|
||||
|
||||
return {
|
||||
[base]: {
|
||||
get: {
|
||||
tags: ['Nastaveni'],
|
||||
summary: `Seznam - ${entity.label}`,
|
||||
description: 'Vraci jen zaznamy firem, do kterych volajici patri.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Seznam',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { items: { type: 'array', items: { type: 'object' } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...denied,
|
||||
},
|
||||
},
|
||||
post: {
|
||||
tags: ['Nastaveni'],
|
||||
summary: `Vytvorit - ${entity.label}`,
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: body,
|
||||
responses: { '201': record, '400': { description: 'Neplatny vstup' }, ...denied },
|
||||
},
|
||||
},
|
||||
[`${base}/{id}`]: {
|
||||
get: {
|
||||
tags: ['Nastaveni'],
|
||||
summary: `Detail - ${entity.label}`,
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [id],
|
||||
responses: { '200': record, '404': { description: 'Neexistuje' }, ...denied },
|
||||
},
|
||||
patch: {
|
||||
tags: ['Nastaveni'],
|
||||
summary: `Upravit - ${entity.label}`,
|
||||
description: 'Posilaji se jen menena pole. ID a cas vzniku se prepsat nedaji.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [id],
|
||||
requestBody: body,
|
||||
responses: {
|
||||
'200': record,
|
||||
'400': { description: 'Neplatny vstup' },
|
||||
'404': { description: 'Neexistuje' },
|
||||
...denied,
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
tags: ['Nastaveni'],
|
||||
summary: `Smazat - ${entity.label}`,
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [id],
|
||||
responses: {
|
||||
'204': { description: 'Smazano' },
|
||||
'404': { description: 'Neexistuje' },
|
||||
...denied,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Entity, ktere se spravuji v Nastaveni. Jeden radek na entitu. */
|
||||
const settingsEntities = [
|
||||
{ path: 'tenants', label: 'firmy', permission: 'tenant.manage' },
|
||||
{ path: 'users', label: 'uzivatele', permission: 'user.manage' },
|
||||
{ path: 'roles', label: 'role a prava', permission: 'role.manage' },
|
||||
{ path: 'people', label: 'resitele', permission: 'people.manage' },
|
||||
{ path: 'groups', label: 'skupiny resitelu', permission: 'group.manage' },
|
||||
{ path: 'ticket-types', label: 'typy ticketu', permission: 'ticketType.manage' },
|
||||
{ path: 'actions', label: 'akce na ticketu', permission: 'action.manage' },
|
||||
{ path: 'widgets', label: 'vlastni widgety', permission: 'widget.manage' },
|
||||
{ path: 'features', label: 'zalozky firmy', permission: 'tenant.manage' },
|
||||
];
|
||||
|
||||
/**
|
||||
* OpenAPI popis API.
|
||||
*
|
||||
@@ -29,6 +134,8 @@ export function buildOpenApiDocument() {
|
||||
{ name: 'Sluzby', description: 'Katalog toho, co umime napojit' },
|
||||
{ name: 'Konektory', description: 'Napojeni firmy na sluzbu vcetne pristupovych udaju' },
|
||||
{ name: 'Skripty', description: 'Vykonna cast sluzby: manifest, kod a zkusebni beh' },
|
||||
{ name: 'Nastaveni', description: 'Firmy, lide, role a prava, typy ticketu, akce, widgety' },
|
||||
{ name: 'Sprava platformy', description: 'Audit a prepnuti na jiny ucet' },
|
||||
{ name: 'Simulace', description: 'Vyvolani provoznich udalosti pro nahled' },
|
||||
{ name: 'Webhook', description: 'Verejny prijem dat do automatizace' },
|
||||
{ name: 'Kontakt', description: 'Poptavkovy formular z webu' },
|
||||
@@ -556,6 +663,11 @@ export function buildOpenApiDocument() {
|
||||
},
|
||||
},
|
||||
paths: {
|
||||
// Petice endpointu za kazdou entitu v Nastaveni, viz `crudPaths` vyse.
|
||||
...settingsEntities.reduce(
|
||||
(all, entity) => ({ ...all, ...crudPaths(entity) }),
|
||||
{} as Record<string, unknown>,
|
||||
),
|
||||
'/health': {
|
||||
get: {
|
||||
tags: ['Provoz'],
|
||||
@@ -954,6 +1066,374 @@ export function buildOpenApiDocument() {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/tickets/{id}/type': {
|
||||
post: {
|
||||
tags: ['Tickety'],
|
||||
summary: 'Nastavit typ ticketu',
|
||||
description:
|
||||
'Typ rozhoduje, ktera vlastni pole ticket ma a ktere akce se na nem ukazou. ' +
|
||||
'Pri zmene typu se hodnoty poli **nemazou**, jen prestanou byt videt.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['typeId'],
|
||||
properties: {
|
||||
typeId: { type: 'string', nullable: true, example: 'tt_order' },
|
||||
fields: {
|
||||
type: 'object',
|
||||
description: 'Hodnoty vlastnich poli. Klic je klic pole z typu ticketu.',
|
||||
additionalProperties: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ulozeno',
|
||||
content: {
|
||||
'application/json': { schema: { $ref: '#/components/schemas/Ticket' } },
|
||||
},
|
||||
},
|
||||
'403': { description: 'Chybi pravo ticket.type.change' },
|
||||
'404': { description: 'Ticket nebo typ neexistuje' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/tickets/{id}/tags': {
|
||||
post: {
|
||||
tags: ['Tickety'],
|
||||
summary: 'Nastavit tagy',
|
||||
description: 'Tagy se prepisuji cele. Prirustkova zmena by u vic lidi naraz kolidovala.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['tags'],
|
||||
properties: {
|
||||
tags: { type: 'array', maxItems: 20, items: { type: 'string', maxLength: 40 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ulozeno',
|
||||
content: {
|
||||
'application/json': { schema: { $ref: '#/components/schemas/Ticket' } },
|
||||
},
|
||||
},
|
||||
'403': { description: 'Chybi pravo ticket.tag' },
|
||||
'404': { description: 'Neexistuje' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/tickets/{id}/group': {
|
||||
post: {
|
||||
tags: ['Tickety'],
|
||||
summary: 'Prehodit na skupinu resitelu',
|
||||
description:
|
||||
'Prirazeni konkretnimu cloveku se **zrusi**. Kdyby zustalo, ticket by byl ' +
|
||||
've fronte skupiny i u cloveka a nikdo by nevedel, kdo to resi.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['groupId'],
|
||||
properties: { groupId: { type: 'string', nullable: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ulozeno',
|
||||
content: {
|
||||
'application/json': { schema: { $ref: '#/components/schemas/Ticket' } },
|
||||
},
|
||||
},
|
||||
'403': { description: 'Chybi pravo ticket.assign.group' },
|
||||
'404': { description: 'Neexistuje' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/tickets/{id}/actions': {
|
||||
get: {
|
||||
tags: ['Tickety'],
|
||||
summary: 'Akce dostupne k ticketu',
|
||||
description:
|
||||
'Vraci **jen akce, ktere v teto situaci opravdu jdou spustit**: sedi typ nebo ' +
|
||||
'tag, projdou podminky a volajici na ne ma pravo. Klient nefiltruje nic.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Akce',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
label: { type: 'string', example: 'Odeslat do iDokladu' },
|
||||
icon: { type: 'string' },
|
||||
style: { type: 'string', enum: ['primary', 'default', 'danger'] },
|
||||
confirm: { type: 'string', nullable: true },
|
||||
form: { type: 'array', items: { type: 'object' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'404': { description: 'Ticket neexistuje' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/tickets/{id}/actions/{actionId}': {
|
||||
post: {
|
||||
tags: ['Tickety'],
|
||||
summary: 'Spustit akci',
|
||||
description:
|
||||
'Vraci 200 **i kdyz akce selhala** - selhani akce neni chyba API. Cely prubeh ' +
|
||||
'vcetne toho, co sluzba vratila, se zapise do logu ticketu.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [
|
||||
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } },
|
||||
{ name: 'actionId', in: 'path', required: true, schema: { type: 'string' } },
|
||||
],
|
||||
requestBody: {
|
||||
required: false,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
form: {
|
||||
type: 'object',
|
||||
description: 'Hodnoty poli, ktera si akce vyzada.',
|
||||
additionalProperties: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Akce probehla nebo selhala, viz ok',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ok: { type: 'boolean' },
|
||||
summary: { type: 'string' },
|
||||
detail: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Cele chybove hlaseni. Nikdy se nezkracuje.',
|
||||
},
|
||||
durationMs: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'403': { description: 'Chybi pravo na tuto akci' },
|
||||
'404': { description: 'Ticket nebo akce neexistuje' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/widget-data': {
|
||||
post: {
|
||||
tags: ['Dashboard'],
|
||||
summary: 'Data vlastnich widgetu',
|
||||
description:
|
||||
'Jeden request na cely prehled. Deset dlazdic nesmi znamenat deset dotazu.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['widgetIds'],
|
||||
properties: { widgetIds: { type: 'array', items: { type: 'string' } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Data po widgetech',
|
||||
content: { 'application/json': { schema: { type: 'object' } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/widget-data/options': {
|
||||
get: {
|
||||
tags: ['Dashboard'],
|
||||
summary: 'Co jde ve vlastnim widgetu nastavit',
|
||||
description: 'Zdroje dat, mozna seskupeni a sirky. Aby to klient nemel v kodu.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Moznosti',
|
||||
content: { 'application/json': { schema: { type: 'object' } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/settings/catalog': {
|
||||
get: {
|
||||
tags: ['Nastaveni'],
|
||||
summary: 'Katalog prav a modulu',
|
||||
description:
|
||||
'Seznam vsech prav a zalozek. Formular role tak nema seznam prav v kodu klienta.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Katalog',
|
||||
content: { 'application/json': { schema: { type: 'object' } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/settings/users/{id}/password': {
|
||||
patch: {
|
||||
tags: ['Nastaveni'],
|
||||
summary: 'Zmenit heslo',
|
||||
description:
|
||||
'Svoje heslo si zmeni kazdy, cizi jen spravce platformy. Hash se nikdy nevraci.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['password'],
|
||||
properties: { password: { type: 'string', minLength: 8 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'204': { description: 'Zmeneno' },
|
||||
'400': { description: 'Kratke heslo' },
|
||||
'403': { description: 'Cizi heslo bez prava' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/admin/impersonate': {
|
||||
post: {
|
||||
tags: ['Sprava platformy'],
|
||||
summary: 'Prepnout se na jiny ucet',
|
||||
description:
|
||||
'Vraci novy token s narokem `act`. Bez `writes` projde **jen GET**, cokoliv ' +
|
||||
'jineho vrati 403. Prepnuti i jeho ukonceni je v auditu.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['userId'],
|
||||
properties: {
|
||||
userId: { type: 'string' },
|
||||
allowWrites: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'true = i zapis. Musi se zapnout vedome.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Token na cizi ucet',
|
||||
content: { 'application/json': { schema: { type: 'object' } } },
|
||||
},
|
||||
'403': { description: 'Neni spravce platformy' },
|
||||
'404': { description: 'Ucet neexistuje' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/admin/impersonate/stop': {
|
||||
post: {
|
||||
tags: ['Sprava platformy'],
|
||||
summary: 'Ukoncit prepnuti',
|
||||
description:
|
||||
'Jen zaznam do auditu. Svuj puvodni token si drzi klient, server o nem nevi.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: { '204': { description: 'Zapsano' } },
|
||||
},
|
||||
},
|
||||
'/api/admin/impersonate/candidates': {
|
||||
get: {
|
||||
tags: ['Sprava platformy'],
|
||||
summary: 'Koho lze prepnout',
|
||||
description: 'Spravci platformy se nenabizeji.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ucty',
|
||||
content: { 'application/json': { schema: { type: 'object' } } },
|
||||
},
|
||||
'403': { description: 'Neni spravce platformy' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/admin/audit': {
|
||||
get: {
|
||||
tags: ['Sprava platformy'],
|
||||
summary: 'Audit',
|
||||
description:
|
||||
'Kdo co udelal, vcetne odepreni a vcetne toho, kdo se za koho vydaval. ' +
|
||||
'Nejnovejsi nahore.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [
|
||||
{ name: 'action', in: 'query', schema: { type: 'string' } },
|
||||
{ name: 'result', in: 'query', schema: { type: 'string', enum: ['ok', 'denied'] } },
|
||||
{ name: 'limit', in: 'query', schema: { type: 'integer', maximum: 500, default: 200 } },
|
||||
],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Zaznamy',
|
||||
content: { 'application/json': { schema: { type: 'object' } } },
|
||||
},
|
||||
'403': { description: 'Neni spravce platformy' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/incidents': {
|
||||
get: {
|
||||
tags: ['Dashboard'],
|
||||
|
||||
Reference in New Issue
Block a user