diff --git a/README.md b/README.md index 348c2ade..372d61be 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,33 @@ REI3 ==== -REI3 is a business application platform that anyone can use. It includes access to a growing range of [business applications](https://rei3.de/applications/) that can be deployed and extended by organizations free of charge. New applications are created with the integrated [Builder](https://rei3.de/docu/) graphical utility and can be added to a global or self-hosted repositories. +REI3 is a business application platform that anyone can use. It provides access to a growing range of [business applications](https://rei3.de/applications_en/), which are offered for free. -Installation wizards for standalone as well as support for dedicated systems allow large and small organizations to deploy REI3 for their individual needs. +Individuals and organizations can extend or create applications with the integrated, graphical application [Builder](https://rei3.de/docu_en/). Applications can also be exported and, if desired, imported into repositories to be shared with others. + +How to install +============== +REI3 runs on Windows or Linux systems. On Windows systems it can be installed in minutes with a graphical installation wizard, while Linux systems are easily deployed with a pre-packaged binary. A portable version is also available for Windows clients for testing and developing applications. [Downloads](https://rei3.de/download_en/) are available on the official [website](https://rei3.de/home_en/). + +For information about how to install and configure REI3, please visit the [admin documentation](https://rei3.de/admindocu-en_us/). + +How to build applications +========================= +All versions of REI3 include the Builder utility, which can create new or change existing applications. After installing REI3, an administrator can enable the Builder inside the system configuration page. The maintenance mode must be enabled first, which will kick all non-admin users from the system while changes are being made. + +For information about how to use the Builder, please visit the [Builder documentation](https://rei3.de/builderdocu-en_us/). + +How to build your own version of REI3 +===================================== +Building your own version of REI3 is simple: +1. Install the latest version of [Golang](https://golang.org/dl/). +1. Download & extract the source code of the version you want to build (as in `2.4.3.2799`). +1. Go into the source code directory (where `r3.go` is located) and execute: `go build -ldflags "-X main.appVersion={YOUR_APP_VERSION}"` + * Make sure to replace `{YOUR_APP_VERSION}` with the version of the extracted source code. At least the major/minor version must match, otherwise you need to deal with upgrading the REI3 database as well (see `db/upgrade/upgrade.go`). + * By setting the environment parameter `GOOS`, you can build for either Windows (`GOOS=windows`) or Linux (`GOOS=linux`). +1. Use your new compiled version of REI3 to replace an installed version. + * Starting with REI3 2.5, static resource files (HTML, JS, CSS, language captions, etc.) are embedded into the binary during compilation. Replacing the binary is enough to fully overwrite REI3. + * With versions before 2.5, you need to also overwrite the folders `var` and `www` if you made any changes to the frontend or language captions. +1. You are now running your own version of REI3. Technologies ============ @@ -10,16 +35,10 @@ The REI3 server application is built on [Golang](https://golang.org/) with the f REI3 heavily relies on [PostgreSQL](https://www.postgresql.org/) for data management, storage and backend functions. -How to build -============ -To build REI3 a current version of [Golang](https://golang.org/dl/) must be installed. Inside the r3 source directory (where `r3.go` is located), `go build` will download Golang dependencies and build the current version of REI3. The Javascript parts of REI3 (located in `www`) do not need to be pre-compiled - REI3´s frontend runs natively in modern browsers. REI3 is compiled with [Rollup](https://rollupjs.org/guide/en/) for release builds. - -To run your own REI3 version, the regular system requirements must be met - in short: A reachable PostgreSQL database with full permissions and connection details stored in the configuration file `config.json`. REI3 can be run from a console, such as `r3 -run`. For more details please refer to the [admin docs](https://rei3.de/admindocu-en_us/). - How to contribute ================= Contributions are always welcome - feel free to fork and submit pull requests. -REI3 follows a four-digit versioning syntax, such as 2.4.2.2788 (MAJOR.MINOR.PATCH.BUILD). Major releases serve to introduce major changes to the application. Minor releases may bring new features, database changes and fixes. Patch releases should primarily focus on fixes, but may include small features as long as the database is not changed - they should only ever improve stability of a version, not introduce new issues. +REI3 follows a four-digit versioning syntax, such as 2.4.2.2788 (MAJOR.MINOR.PATCH.BUILD). Major releases serve to introduce major changes to the application. Minor releases may bring new features, database changes and fixes. Patch releases should primarily focus on fixes, but may include small features as long as the database is not changed. -The main branch will contain the currently released minor version of REI3; patches for this version can directly be submitted for the main branch. Each new minor release will use a separate branch, which will merge with main once the latest minor version is released. +The branch `main` will contain the currently released minor version of REI3; patches for this version can directly be submitted for the main branch. Each new minor release will use a separate branch, which will merge with `main` once the latest minor version is released. diff --git a/cache/cache_access.go b/cache/cache_access.go index 8d0b0ac2..09ce58f9 100644 --- a/cache/cache_access.go +++ b/cache/cache_access.go @@ -52,15 +52,20 @@ func RenewAccessById(loginId int64) error { return load(loginId, true) } -// kick one login -func KickLoginById(loginId int64) { +// update clients +func KickLoginById(loginId int64) { // kick single login ClientEvent_handlerChan <- types.ClientEvent{LoginId: loginId, Kick: true} } - -// kick all non-admins -func KickNonAdmins() { +func KickNonAdmins() { // kick all non-admins ClientEvent_handlerChan <- types.ClientEvent{LoginId: 0, KickNonAdmin: true} } +func ChangedBuilderMode(modeActive bool) { + if modeActive { + ClientEvent_handlerChan <- types.ClientEvent{LoginId: 0, BuilderOn: true} + } else { + ClientEvent_handlerChan <- types.ClientEvent{LoginId: 0, BuilderOff: true} + } +} // load access permissions for login ID into cache func load(loginId int64, renewal bool) error { diff --git a/cache/cache_caption.go b/cache/cache_caption.go new file mode 100644 index 00000000..cadb04c3 --- /dev/null +++ b/cache/cache_caption.go @@ -0,0 +1,36 @@ +package cache + +import ( + "encoding/json" + + _ "embed" +) + +var ( + //go:embed captions/de_de + caption_de_de json.RawMessage + + //go:embed captions/en_us + caption_en_us json.RawMessage + + //go:embed captions/it_it + caption_it_it json.RawMessage +) + +func GetCaptions(code string) json.RawMessage { + switch code { + case "de_de": + return caption_de_de + case "en_us": + return caption_en_us + case "it_it": + return caption_it_it + } + + // default to english, if language code was not valid + return caption_en_us +} + +func GetCaptionLanguageCodes() []string { + return []string{"en_us", "de_de", "it_it"} +} diff --git a/cache/cache_package.go b/cache/cache_package.go new file mode 100644 index 00000000..8d4e9b23 --- /dev/null +++ b/cache/cache_package.go @@ -0,0 +1,8 @@ +package cache + +import ( + _ "embed" +) + +//go:embed packages/core_company.rei3 +var Package_CoreCompany []byte diff --git a/cache/cache_schema.go b/cache/cache_schema.go index c002904b..52293c4e 100644 --- a/cache/cache_schema.go +++ b/cache/cache_schema.go @@ -41,10 +41,11 @@ var ( schema_mx sync.Mutex // references to specific entities - ModuleIdMap map[uuid.UUID]types.Module - RelationIdMap map[uuid.UUID]types.Relation - AttributeIdMap map[uuid.UUID]types.Attribute - RoleIdMap map[uuid.UUID]types.Role + ModuleIdMap map[uuid.UUID]types.Module + RelationIdMap map[uuid.UUID]types.Relation + AttributeIdMap map[uuid.UUID]types.Attribute + RoleIdMap map[uuid.UUID]types.Role + PgFunctionIdMap map[uuid.UUID]types.PgFunction // schema cache schemaCache schemaCacheType // full cache @@ -96,6 +97,7 @@ func UpdateSchema(moduleId pgtype.UUID, newVersion bool) error { RelationIdMap = make(map[uuid.UUID]types.Relation) AttributeIdMap = make(map[uuid.UUID]types.Attribute) RoleIdMap = make(map[uuid.UUID]types.Role) + PgFunctionIdMap = make(map[uuid.UUID]types.PgFunction) } else { log.Info("cache", "starting schema processing for one module") moduleIdsReload = append(moduleIdsReload, moduleId.Bytes) @@ -267,13 +269,16 @@ func reloadModule(id uuid.UUID) error { return err } - // get pg functions + // store & backfill PG functions log.Info("cache", "load functions") mod.PgFunctions, err = pgFunction.Get(mod.Id) if err != nil { return err } + for _, fnc := range mod.PgFunctions { + PgFunctionIdMap[fnc.Id] = fnc + } // update cache map with parsed module ModuleIdMap[mod.Id] = mod diff --git a/var/texts/de_de b/cache/captions/de_de similarity index 94% rename from var/texts/de_de rename to cache/captions/de_de index 53871445..0c57a964 100644 --- a/var/texts/de_de +++ b/cache/captions/de_de @@ -92,10 +92,12 @@ "nothingSelected":"Keine Auswahl getroffen", "nothingThere":"Keine Ergebnisse verfügbar", "notice":"Hinweis", + "order":"Reihenf.", "password":"Passwort", "results":"{CNT} Ergebnisse", "resultsNone":"- keine Ergebnisse -", "resultsOf":"/ {CNT}", + "role":"Rolle", "title":"Titel", "username":"Benutzername", "version":"Version" @@ -106,6 +108,11 @@ "apply":"Änderungen übernehmen", "removeLogo":"Logo entfernen" }, + "dialog":{ + "builderMode":"Der Builder dient zum Erstellen oder Verändern von Anwendungen.

Das Ändern von Anwendungen kann zum Datenverlust führen und sollte NIEMALS in produktiven Umgebungen getan werden.

Um den Builder sicher zu nutzen, bitte eine dedizierte Instanz von REI3 verwenden; fertige und getestete Anwendungen können dann in ein Produktivsystem transferiert werden.

Um mehr zu lernen, steht die Builder-Dokumentation zur Verfügung.", + "productionMode":"Der Wartungsmodus wird genutzt, um Anwendungen zu aktualisieren, installieren oder zu entfernen.

Beim Aktivieren des Wartungsmodus werden alle Benutzer ohne Adminrechte abgemeldet.", + "pleaseRead":"Bitte vor dem Speichern lesen" + }, "appName":"Instanzname", "appNameShort":"Seitentitel", "appVersion":"Plattform-Version", @@ -121,6 +128,7 @@ "bruteforceCountTracked":"Verfolgte Hosts", "bruteforceProtection":"Bruteforce-Schutz aktivieren", "bruteforceTitle":"Bruteforce-Schutz", + "builderMode":"Builder-Modus", "colorHint":"Leer lassen für Standardwerte", "companyColorHeader":"Titelfarbe (Hauptmenü)", "companyColorLogin":"Titelfarbe (Anmeldung)", @@ -156,7 +164,7 @@ "logLevelTransfer":"Transfer", "logsKeepDays":"Logs aufheben (in Tagen)", "productionMode":"Wartungsmodus", - "publicHostName":"Public hostname", + "publicHostName":"Öffentlicher Hostname", "pwForceDigit":"Erzwinge Zahl", "pwForceLower":"Erzwinge Kleinbuchstaben", "pwForceSpecial":"Erzwinge Sonderzeichen", @@ -307,7 +315,10 @@ "updateAll":"Alle aktualisieren ({COUNT})" }, "dialog":{ - "delete":"Bist du sicher, dass du diese Anwendung löschen möchtest? Dies wird auch alle inkludierten Daten vom System löschen.

Diese Aktion ist ohne aktuelles Backup nicht rückgängig zu machen.", + "delete":"Bist du sicher, dass du diese Anwendung löschen möchtest? Dies wird auch alle inkludierten Daten vom System löschen.

Diese Aktion ist ohne aktuelles Backup nicht rückgängig zu machen.{APPS}", + "deleteApps":"

Folgende Anwendungen sind von dieser abhängig und werden ebenfalls gelöscht:", + "deleteMulti":"Diese Aktion wird {COUNT} Anwendung(en) mit dazugehörigen Daten permanent löschen.

Möchtest du wirklich fortfahren?", + "deleteTitle":"Permanente Löschung der Anwendung \"{APP}\"", "owner":"Wenn diese Option aktiviert ist, können Änderungen an dieser Anwendung als neue Version exportiert werden. Falls du nicht der originale Autor bist, werden alle Änderungen VERLOREN GEHEN, wenn eine neue Version vom Autoren installiert wird. Dies kann auch zu DATENVERLUST führen.

Falls du vorhast, Anwendungen anderer Autoren zu verändern/erweitern, kannst du gefahrlos auf \"diesen aufbauen\" - bitte referenziere die Builder-Dokumentation für mehr Details.", "ownerTitle":"Warnung - bitte vorsichtig lesen!" }, @@ -315,7 +326,7 @@ "installFailed":"Aktualisierung der Anwendung ist fehlgeschlagen. Bei Aktualisierung einer einzelnen Anwendung können fehlende Abhängigkeiten zu Problemen führen - bitte versuchen, alle Anwendungen gemeinsam zu aktualisieren, um diese zu lösen.

Fehlermeldung: {ERROR}", "uploadFailed":"Anwendung konnte nicht von der hochgeladenen Datei installiert werden. Bitte das Loglevel für Transfere auf \"Alles\" erhöhen und erneut versuchen - Details werden dann im Systemlog aufgeführt." }, - "dependOnUs":"Folgende Anwendungen sind hiervon abhängig: {NAMES}", + "changeLog":"Änderungshistorie", "hidden":"Versteckt", "import":"Von Datei hinzufügen", "nothingInstalled":"Es sind keine Anwendungen installiert.", @@ -553,7 +564,7 @@ "icon":"Formular-Icon", "ics":"iCAL-Abonnements", "layout":"Listen-Layout", - "limit":"Ergebnislimit", + "limit":"Ergebnisanzahl", "new":"Neues Formular", "onMobile":"In mobiler Ansicht", "presetOpen":"Vord. Datensatz öffnen", @@ -623,7 +634,7 @@ "languageCode":"Sprach-Code", "languageCodeHint":"en_us, zh_cn, de_de, ...", "languageMain":"Primäre Sprache*", - "languageMainHint":"*dient als Ersatz, wenn die gewählte Benutzersprache nicht verfügbar ist", + "languageMainHint":"*wird verwendet wenn die gewählte Benutzersprache nicht verfügbar ist.", "languageTitle":"Anwendungstitel", "languages":"Sprachen", "new":"Neue Anwendung", @@ -633,6 +644,10 @@ "releaseBuildApp":"Plattform-Version", "releaseDate":"Veröff.-Datum", "startForm":"Startformular", + "startFormDefault":"Standard-Startformular*", + "startFormDefaultHint":"*wird verwendet wenn kein anderes Formular per Rolle zugewiesen werden kann.", + "startForms":"Startformulare", + "startFormsExplanation":"Das erste Startformular wird verwendet, das einer Rolle der aktuellen Anmeldung entspricht.", "title":"Anwendungen" }, "pgFunction":{ @@ -713,6 +728,8 @@ "choices":"Filtersätze ({COUNT})*", "choicesHint":"*Erste Satz ist erstmal aktiv - Auswahl möglich, wenn 2 oder mehr existieren.", "filters":"Filter ({COUNT})", + "fixedLimit":"Festes Ergebnislimit", + "fixedLimit0":"nicht aktiv", "joinAddHint":"Eine Relation mit dieser verbinden (join)", "joinApplyCreateHint":"(C) Datensatz erzeugen auf dieser Relation", "joinApplyDeleteHint":"(D) Datensatz löschen auf dieser Relation", @@ -752,6 +769,16 @@ "onInsert":"INSERT", "onUpdate":"UPDATE", "perRow":"EACH ROW", + "policies":"Richtlinien", + "policyActions":"Handlung", + "policyActionDelete":"Löschen", + "policyActionSelect":"Anzeigen", + "policyActionUpdate":"Verändern", + "policyExplanation":"Die erste Richtlinie, die einer Rolle der aktuellen Anmeldung entspricht, wird angewendet, solange die versuchte Handlung für diese Richtlinie aktiviert ist. Um bspw. einer Rolle vollen Zugriff zu erlauben, kann eine Richtlinie mit allen Handlungen aktiviert und keinen Filtern an oberster Stelle gesetzt werden.", + "policyFunctions":"Filterfunktionen", + "policyFunctionExcl":"Datensatz-IDs blockieren", + "policyFunctionIncl":"Datensatz-IDs erlauben", + "policyNotSet":"ungefiltert", "presets":"Vordefinierte Datensätze ({CNT})", "presetProtected":"Geschützt", "presetValues":"Werte", @@ -816,6 +843,8 @@ "icsHint":"Auf Kalender von Extern zugreifen", "icsPublish":"Für persönliche Nutzung veröffentlichen", "ganttToggleHint":"Zwischen Stunden- & Tagmodus wechseln", + "ganttShowLabels":"Gruppen", + "ganttShowLabelsHint":"Gruppen anzeigen/verstecken", "zoomResetHint":"Zoom-Level zurücksetzen" }, "icsDesc":"Diese URL verwenden, um Kalender zu abonnieren (iCalendar, ICS).", @@ -881,6 +910,7 @@ "javascript":"JavaScript-Ausdruck", "languageCode":"Login-Sprach-Code (en_us, ...)", "login":"Login-ID (Integer)", + "preset":"ID von vordefinierten Datensatz (Integer)", "record":"Datensatz-ID (Integer, Formular Relation 0)", "recordNew":"Datensatz ist neu (TRUE/FALSE, Formular)", "role":"Login hat Rolle (TRUE/FALSE)", @@ -929,7 +959,8 @@ "help":"Hilfeseiten", "helpContextTitle":"Kontext", "helpModuleTitle":"Anwendung", - "invalidInputs":"Eingaben prüfen!" + "invalidInputs":"Eingaben prüfen!", + "noAccess":"Kein Zugriff" }, "home":{ "button":{ diff --git a/var/texts/en_us b/cache/captions/en_us similarity index 94% rename from var/texts/en_us rename to cache/captions/en_us index 56fe983c..eb296d45 100644 --- a/var/texts/en_us +++ b/cache/captions/en_us @@ -92,10 +92,12 @@ "nothingSelected":"Nothing selected", "nothingThere":"No results available", "notice":"Notice", + "order":"Order", "password":"Password", "results":"{CNT} results", "resultsNone":"- no results -", "resultsOf":"/ {CNT}", + "role":"Role", "title":"Title", "username":"Username", "version":"Version" @@ -106,6 +108,11 @@ "apply":"Apply changes", "removeLogo":"Remove logo" }, + "dialog":{ + "builderMode":"The Builder is used to create or change applications.

Changing applications can result in data loss and should NEVER be done in production environments.

To safely use the Builder, please use a dedicated instance of REI3; ready and tested applications can then be transferred to a production system.

Please refer to the Builder documentation to learn more.", + "productionMode":"The maintenance mode is used to update, install or remove applications from the system.

Switching to maintenance mode will logout all users without admin permissions.", + "pleaseRead":"Please read before saving" + }, "appName":"Instance name", "appNameShort":"Page title", "appVersion":"Platform version", @@ -121,6 +128,7 @@ "bruteforceCountTracked":"Tracked hosts", "bruteforceProtection":"Enable bruteforce protection", "bruteforceTitle":"Bruteforce protection", + "builderMode":"Builder mode", "colorHint":"Keep empty for default", "companyColorHeader":"Title color (header)", "companyColorLogin":"Title color (login)", @@ -307,7 +315,10 @@ "updateAll":"Update all ({COUNT})" }, "dialog":{ - "delete":"Are you sure you want to delete this application? This will also delete all included data from your system.

This action is irreversible without current backups.", + "delete":"Are you sure you want to delete this application? This will also delete all included data from your system.

This action is irreversible without current backups.{APPS}", + "deleteApps":"

The following applications are dependend on this one and will also be deleted:", + "deleteMulti":"This action will permanently delete {COUNT} application(s) and corresponding data.

Do you really want to continue?", + "deleteTitle":"Permanent deletion of application '{APP}'", "owner":"With this option enabled, changes to this application can be exported as a new version. If you are not the original author, all changes will be LOST when a new version from the author is installed. This can also result in DATA LOSS.

If you intend to change/extent applications from other authors, you can safely do so by 'building on them' - please refer to the Builder documentation for more details.", "ownerTitle":"Warning - please read carefully!" }, @@ -315,7 +326,7 @@ "installFailed":"Update of application has failed. When updating a single application, missing dependencies can causes issues - please try updating all applications together to resolve these.

Error message: {ERROR}", "uploadFailed":"Failed to add application from the uploaded file. Please increase the log level for transfers to 'Everything' and try again - details will then be visible in the system logs." }, - "dependOnUs":"Following Applications depend on this: {NAMES}", + "changeLog":"Change log", "hidden":"Hidden", "import":"Add from file", "nothingInstalled":"No applications are installed.", @@ -553,7 +564,7 @@ "icon":"Form icon", "ics":"iCAL subscriptions", "layout":"List layout", - "limit":"Result limit", + "limit":"Result count", "new":"New form", "onMobile":"In mobile view", "presetOpen":"Open preset", @@ -623,7 +634,7 @@ "languageCode":"Language code", "languageCodeHint":"en_us, zh_cn, de_de, ...", "languageMain":"Primary language*", - "languageMainHint":"*is used as fallback, if chosen user language is not available", + "languageMainHint":"*is used if chosen user language is not available.", "languageTitle":"Application title", "languages":"Languages", "new":"New application", @@ -633,6 +644,10 @@ "releaseBuildApp":"Platform version", "releaseDate":"Release date", "startForm":"Start form", + "startFormDefault":"Default start form*", + "startFormDefaultHint":"*is used if no other form can be assigned via role.", + "startForms":"Start forms", + "startFormsExplanation":"The first start form is used that matches a role of the current login.", "title":"Applications" }, "pgFunction":{ @@ -713,6 +728,8 @@ "choices":"Filter sets ({COUNT})*", "choicesHint":"*First set is active by default - choice available if 2 or more exist.", "filters":"Filters ({COUNT})", + "fixedLimit":"Fixed result limit", + "fixedLimit0":"not active", "joinAddHint":"Join another relation to this one", "joinApplyCreateHint":"(C)reate record on this relation", "joinApplyDeleteHint":"(D)elete record on this relation", @@ -752,6 +769,16 @@ "onInsert":"INSERT", "onUpdate":"UPDATE", "perRow":"EACH ROW", + "policies":"Policies", + "policyActions":"Action", + "policyActionDelete":"Delete", + "policyActionSelect":"Display", + "policyActionUpdate":"Change", + "policyExplanation":"The first policy that matches a role of the current login is applied, if the attempted action is enabled for this policy. To give full access to a role for example, a policy with all actions enabled and no filters can be placed at the top.", + "policyFunctions":"Filter functions", + "policyFunctionExcl":"Block record IDs", + "policyFunctionIncl":"Allow record IDs", + "policyNotSet":"unfiltered", "presets":"Preset records ({CNT})", "presetProtected":"Protected", "presetValues":"Values", @@ -816,6 +843,8 @@ "icsHint":"Access calendar from external application", "icsPublish":"Publish for personal use", "ganttToggleHint":"Toggle between day & hour mode", + "ganttShowLabels":"Groups", + "ganttShowLabelsHint":"Show/hide groups", "zoomResetHint":"Reset zoom level" }, "icsDesc":"Use this URL to subscribe to this calendar (iCalendar, ICS).", @@ -881,6 +910,7 @@ "javascript":"JavaScript expression", "languageCode":"login language code (en_us, ...)", "login":"login ID (integer)", + "preset":"preset record ID (Integer)", "record":"record ID (integer, form relation 0)", "recordNew":"record is new (TRUE/FALSE, form)", "role":"login has role (TRUE/FALSE)", @@ -929,7 +959,8 @@ "help":"Help pages", "helpContextTitle":"Context", "helpModuleTitle":"Application", - "invalidInputs":"Check inputs!" + "invalidInputs":"Check inputs!", + "noAccess":"No access" }, "home":{ "button":{ diff --git a/cache/captions/it_it b/cache/captions/it_it new file mode 100644 index 00000000..b7ddb51f --- /dev/null +++ b/cache/captions/it_it @@ -0,0 +1,1101 @@ +{ + "generic":{ + "button":{ + "add":"Aggiungi", + "apply":"Applica", + "cancel":"Annulla", + "close":"Chiudi", + "create":"Crea", + "delete":"Cancella", + "deleteHint":"Cancella il record definitivamente", + "deleteSelected":"Cancella selezionati", + "edit":"Modifica", + "expert":"Avanzato", + "export":"Esporta", + "filter":"Filtra", + "filterHint":"Filtra i risultati", + "import":"Importa", + "new":"Nuovo", + "newHint":"Crea un nuovo record", + "open":"Apri", + "refresh":"Ricarica", + "refreshHint":"Ricarica il record", + "reset":"Resetta", + "save":"Salva", + "saveHint":"Salva i cambiamenti", + "saveNew":"Salva+Nuovo", + "saveNewHint":"Salva il record e ne crea uno nuovo", + "send":"Invia", + "show":"Mostra" + }, + "dialog":{ + "confirm":"Si prega di confermare" + }, + "error":{ + "contextDeadlineExceeded":"Non è stato possibile completare l'azione entro il periodo di tempo consentito. Riprova", + "fileTooLarge":"Impossibile caricare il file '{NAME}', la dimensione massima consentita è {SIZE}", + "foreignKeyConstraint":"Il record è ancora referenziato come '{ATR}' in '{MOD}'", + "foreignKeyUniqueConstraint":"'{NAME}' può essere referenziato solo una volta", + "generalError":"Si è verificato un errore imprevisto, contattare l'amministratore di sistema", + "generalErrorTitle":"Errore", + "presetProtected":"Questo record è protetto e non può essere cancellato", + "unauthorized":"Accesso non autorizzato", + "uniqueConstraint":"Il valore oer '{NAMES}' deve essere univoco" + }, + "option":{ + "aggArray":"vettore", + "aggAvg":"media", + "aggCount":"conta", + "aggList":"elenco separato da virgole", + "aggMax":"massimo", + "aggMin":"minimo", + "aggRecord":"[record singolo]", + "aggSum":"somma", + "no":"no", + "size0":"più piccolo", + "size1":"minore", + "size2":"predefinito", + "size3":"maggiore", + "size4":"più grande", + "sortAsc":"ascendente", + "sortDesc":"discendente", + "yes":"sì" + }, + "active":"Attivo", + "application":"Applicazione", + "applications":"Applicazioni", + "appName":"REI3", + "appWebsite":"https://rei3.de", + "busy":"Elaborazione - Clicca qui per abortire", + "date":"Data", + "description":"Descrizione", + "filters":"Filtri", + "home":"Home", + "icon":"Icona", + "id":"ID", + "inputDecimal":"Il valore deve essere decimale", + "inputFileNotUploaded":"File non ancora salvato", + "inputInvalid":"Valore non valido", + "inputLarge":"Valore troppo grande", + "inputLong":"Valore troppo lungo", + "inputRequired":"Obbligatorio", + "inputSelectMore":"...inserisci il testo per filtrare i risultati", + "inputShort":"Valore troppo corto", + "inputSmall":"Valore troppo piccolo", + "inputTooFewFiles":"Troppo pochi file", + "inputTooManyFiles":"Troppi file", + "licenseRequired":"Richiede licenza", + "limit":"Limite", + "menu":"Menu", + "missingCaption":"TITOLO MANCANTE", + "name":"Nome", + "nothingSelected":"Niente selezionato", + "nothingThere":"Nessun risultato disponibile", + "notice":"Avviso", + "order":"Ordine", + "password":"Password", + "results":"{CNT} risultati", + "resultsNone":"- nessun risultato -", + "resultsOf":"/ {CNT}", + "role":"Ruolo", + "title":"Titolo", + "username":"Username", + "version":"Versione" + }, + "admin":{ + "config":{ + "button":{ + "apply":"Applica modifiche", + "removeLogo":"Rimuovi logo" + }, + "dialog":{ + "builderMode":"Il Builder viene utilizzato per creare o modificare le applicazioni.

La modifica delle applicazioni può causare la perdita di dati e non dovrebbe MAI essere eseguita in ambienti di produzione.

Per utilizzare in sicurezza il Builder, utilizzare un'istanza dedicata di REI3; le applicazioni pronte e testate possono quindi essere trasferite a un sistema di produzione.

Fare riferimento a Documentazione del Builder per saperne di più.", + "productionMode":"La modalità di manutenzione viene utilizzata per aggiornare, installare o rimuovere applicazioni dal sistema.

Il passaggio alla modalità di manutenzione disconnetterà tutti gli utenti senza autorizzazioni di amministratore.", + "pleaseRead":"Si prega di leggere prima di salvare" + }, + "appName":"Nome Istanza", + "appNameShort":"Titolo pagina", + "appVersion":"Versione piattaforma", + "backupCount":"Mantieni le versioni", + "backupDaily":"Giornaliero", + "backupDir":"Cartella destinazione*", + "backupDirNote":"*Assicurati che questo percorso punti a un percorso di rete separato o che il suo contenuto venga copiato regolarmente su un secondo sistema. Ciò è necessario per il ripristino in caso di guasto completo del sistema.", + "backupEmbeddedNote":"I backup completi integrati sono disponibili solo se viene utilizzato il modello di distribuzione autonomo.

La documentazione del sistema contiene capitoli relativi alle misure di backup consigliate per istanze dedicate.", + "backupMonthly":"Mensile", + "backupWeekly":"Settimanale", + "bruteforceAttempts":"Blocca gli host dopo i tentativi", + "bruteforceCountBlocked":"Host Bloccati", + "bruteforceCountTracked":"Host Tracciati", + "bruteforceProtection":"Abilita protezione forza bruta", + "bruteforceTitle":"Protezione forza bruta", + "builderMode":"Modalità Builder", + "colorHint":"Mantieni vuoto per impostazione predefinita", + "companyColorHeader":"Colore titolo (intestazione)", + "companyColorLogin":"Colore titolo (accesso)", + "companyLogo":"Logo azienda", + "companyLogoDesc":"(file PNG, max. 64kb)", + "companyLogoUrl":"Collegamento logo azienda", + "companyLogoUrlDesc":"Qualsiasi URL valido", + "companyName":"Nome azienda", + "companyWelcome":"Testo di benvenuto (accesso)", + "dbTimeoutCsv":"Timeout database: elaborazione batch (CSV)", + "dbTimeoutDataRest":"Timeout database: richieste dati (REST)", + "dbTimeoutDataWs":"Timeout database: richieste dati (HTTP/WS)", + "dbTimeoutHint":"In secondi", + "dbTimeoutIcs":"Timeout database: download del calendario (ICS)", + "defaultLanguageCode":"Lingua predefinita", + "icsDaysPost":"Escludi eventi dopo la data corrente (in giorni)", + "icsDaysPre":"Escludi eventi prima la data corrente (in giorni)", + "icsDownload":"Abilita iscrizioni al calendario", + "licenseState":"Stato licenza", + "licenseStateNok":"Nessuna licenza attiva", + "licenseStateOk":"Valido per {COUNT} giorni in più", + "logLevel1":"Solo errori", + "logLevel2":"Errori e avvertimenti", + "logLevel3":"Tutto", + "logLevelApplication":"Applicazioni", + "logLevelBackup":"Backup", + "logLevelCache":"Cache", + "logLevelCsv":"Importa/esporta CSV", + "logLevelLdap":"LDAP", + "logLevelMail":"Posta", + "logLevelScheduler":"Pianificatore", + "logLevelServer":"Server", + "logLevelTransfer":"Trasferimento", + "logsKeepDays":"Mantieni log (in giorni)", + "productionMode":"Modalità manutenzione", + "publicHostName":"Nome host pubblico", + "pwForceDigit":"Richiesti numeri", + "pwForceLower":"Richieste lettere minuscole", + "pwForceSpecial":"Richiesti caratteri speciali", + "pwForceUpper":"Richieste lettere maiuscole", + "pwLengthMin":"Lunghezza minima", + "pwTitle":"Impostazioni password", + "repoFeedback":"Consenti feedback utente anonimo", + "repoKeyManagement":"Gestione delle chiavi affidabili", + "repoPublicKeys":"Chiavi pubbliche", + "repoPublicKeyAdd":"Aggiungi chiave pubblica", + "repoPublicKeyInputNameHint":"Nome chiave", + "repoPublicKeyInputValueHint":"Esempio:\n-----BEGIN RSA PUBLIC KEY-----\nKEY\n-----END RSA PUBLIC KEY-----", + "repoSkipVerify":"Consenti certificati non attendibili", + "repoUrl":"URL dell'archivio", + "title":"Configurazione di sistema", + "titleBackup":"Backup completi integrati", + "titleCustom":"Personalizzazione", + "titleGeneral":"Generale", + "titleIcs":"Iscrizioni al calendario", + "titleLogging":"Livelli log", + "titleLogins":"Accessi", + "titleMail":"Invia posta", + "titlePerformance":"Performance", + "titleRepo":"Archivio applicazione", + "tokenExpiryHours":"Massima durata della sessione in ore", + "updateCheck":"Stato versione", + "updateCheckCurrent":"Attuale", + "updateCheckNewer":"Cutting edge", + "updateCheckOlder":"Aggiornamento disponibile", + "updateCheckUnknown":"Sconosciuto" + }, + "ldaps":{ + "button":{ + "expert":"Impostazioni avanzate", + "import":"Importa gli accessi ora", + "new":"Aggiungi connessione", + "test":"Test connessione" + }, + "dialog":{ + "delete":"Sei sicuro di voler eliminare questa connessione LDAP?", + "importDone":"L'importazione è stata completata", + "testDone":"Il test di connessione è andato a buon fine" + }, + "assignRoles":"Imposta i ruoli in base all'appartenenza al gruppo
(disabilita l'assegnazione dei ruoli manuale)", + "bindUserDn":"Associa utente DN", + "bindUserDnHint":"Esempio: CN=sola lettura,OU=Utente,DC=miaazienda,DC=locale", + "bindUserPw":"Associa password utente", + "description":"Questa connessione LDAP importa i nomi di accesso e abilita l'autenticazione con le credenziali LDAP.
Le appartenenze ai gruppi LDAP possono essere utilizzate per assegnare automaticamente i ruoli.", + "groupDn":"Gruppo DN", + "groupDnHint":"Esempio: CN=Admin_User,OU=Group,DC=miaazienda,DC=local", + "host":"Host", + "hostHint":"Nome host LDAP", + "keyAttribute":"Attributo chiave univoco", + "keyAttributeHint":"Esempio: objectGUID", + "loginAttribute":"Attributo accesso (login)", + "loginAttributeHint":"Esempio: sAMAccountName", + "memberAttribute":"Attributo membro", + "memberAttributeHint":"Esmpio: memberOf", + "msAdExt":"Estensioni Microsoft AD", + "msAdExtHint":"Appartenenze a gruppi nidificati, controllo dell'account utente", + "nameHint":"Nome univoco", + "port":"Porta", + "portHint":"Predefinito 389 se no TLS, 636 per TLS", + "role":"Ruolo", + "searchClass":"Classe oggetto", + "searchClassHint":"Esempio: user", + "searchDn":"Cerca DN", + "searchDnHint":"Esempio: OU=User,DC=miaazienda,DC=local", + "title":"Crea/modifica connessione", + "titleRoles":"Ruoli per appartenenza al gruppo", + "tls":"Usa TLS", + "tlsVerify":"Verifica TLS" + }, + "license":{ + "active":"Licenza installata", + "clientId":"ID cliente", + "licenseEmpty":"Nessuna", + "licenseExpired":"Licenza scaduta", + "licenseId":"ID licenza", + "licenseValid":"La licenza è valida per {COUNT} giorni in più", + "registeredFor":"Registrato per", + "upload":"Carica file licenza", + "validUntil":"Valido fino a" + }, + "login":{ + "dialog":{ + "delete":"Sei sicuro di voler eliminare questo login?

Questa azione è irreversibile." + }, + "error":{ + "uniqueConstraint":"Lo username deve essere univoco" + }, + "admin":"Admin", + "adminHint":"Amministratore della piattaforma", + "authentication":"Autenticazione", + "language":"Lingua", + "ldapAssignActive":"I ruoli vengono assegnati tramite l'appartenenza ai gruppi LDAP", + "newLogin":"Nuovo login", + "noAuth":"Pubblico", + "noAuthHint":"Può accedere senza autenticazione", + "passwordHint":"Imposta nuova password", + "passwordHintNoAuth":"Nessuna autenticazione", + "recordSelectHint":"...", + "roles":"Ruoli" + }, + "logs":{ + "byString":"Cerca testo", + "context":"Contesto", + "date":"Timestamp", + "level":"Livello", + "level1":"Errore", + "level2":"Avvertimento", + "level3":"Informazione", + "message":"Messaggio", + "module":"Applicazione" + }, + "mails":{ + "account":"Account email", + "accounts":"Account email", + "accountHost":"Hostname", + "accountMode":"Modo", + "accountNew":"Nuovo account", + "accountPass":"Password", + "accountPort":"Porta", + "accountSendAs":"Indirizzo invio", + "accountStartTls":"STARTTLS", + "accountTest":"Test email", + "accountUser":"Username", + "attempts":"Tentativi invio", + "bccList":"BCC", + "body":"Corpo", + "ccList":"CC", + "dir":"Direction", + "dirIn":"IN", + "dirOut":"OUT", + "files":"Allegati", + "noMails":"Lo spooler di posta è vuoto.", + "toList":"A", + "subject":"Oggetto", + "testAccount":"Seleziona account", + "testOk":"L'e-mail di prova è stata aggiunta con successo allo spooler.", + "testRecipient":"Indirizzo del destinatario", + "testSubject":"Oggetto" + }, + "modules":{ + "button":{ + "repository":"Aggiungi dall'archivio", + "update":"Aggiorna a v{VERSION}", + "updateAll":"Aggiorna tutti ({COUNT})" + }, + "dialog":{ + "delete":"Sei sicuro di voler eliminare questa applicazione? Questo eliminerà anche tutti i dati inclusi dal tuo sistema.

Questa azione è irreversibile senza backup correnti.{APPS}", + "deleteApps":"

Le seguenti applicazioni dipendono da questa e verranno anch'esse eliminate:", + "deleteMulti":"Questa azione eliminerà in modo definitivo {COUNT} applicazione(i) e i dati corrispondenti.

Vuoi davvero continuare?", + "deleteTitle":"Cancellazione definitivo dell'applicazione '{APP}'", + "owner":"Con questa opzione abilitata, le modifiche a questa applicazione possono essere esportate come una nuova versione. Se non sei l'autore originale, tutte le modifiche andranno PERSE quando verrà installata una nuova versione dell'autore. Ciò può anche comportare la PERDITA DI DATI.

Se intendi modificare/estendere le applicazioni di altri autori, puoi farlo in sicurezza \"costruendo su di esse\" - fai riferimento alla documentazione del Builder per maggiori dettagli.", + "ownerTitle":"Avvertimento - leggi attentamente!" + }, + "error":{ + "installFailed":"L'aggiornamento dell'applicazione non è riuscito. Quando si aggiorna una singola applicazione, le dipendenze mancanti possono causare problemi: prova ad aggiornare tutte le applicazioni insieme per risolverli.

Messaggio di errore: {ERROR}", + "uploadFailed":"Impossibile aggiungere l'applicazione dal file caricato. Aumenta il livello di registro per i trasferimenti a \"Tutto\" e riprova: i dettagli saranno visibili nei registri di sistema." + }, + "changeLog":"Log modifiche", + "hidden":"Nascosto", + "import":"Aggiungi dal file", + "nothingInstalled":"Nessuna applicazione è installata.", + "owner":"Esporta i cambiamenti", + "position":"Ordine nel menu", + "productionMode":"Le applicazioni possono essere modificate solo quando è attiva la modalità di manutenzione.", + "releaseDate":"Data rilascio", + "repoNotIncluded":"non disponibile", + "repoOutdatedApp":"aggiornamento della piattaforma richiesto", + "repoUpToDate":"ultima versione", + "update":"Aggiorna", + "updateDone":"L'aggiornamento è stato applicato con successo" + }, + "repo":{ + "button":{ + "install":"Installa", + "installed":"Installato", + "showInstalled":"Mostra installati" + }, + "author":"per {NAME}", + "byString":"Cerca testo", + "fetchDone":"L'applicazione è stata installata con successo.

Per ottenere l'accesso, è necessario assegnare i ruoli.", + "maintenanceBlock":"Installazione solo in modalità manutenzione", + "notCompatible":"Richiesto aggiornamento della piattaforma", + "supportPage":"Sito web" + }, + "roles":{ + "button":{ + "all":"Mostra tutto", + "descriptions":"Descrizioni" + }, + "addLogin":"Aggiungi login", + "descriptionEmpty":"Nessuna descrizione disponibile", + "nothingInstalled":"Nessuna applicazione installata." + }, + "scheduler":{ + "button":{ + "runNow":"Esegui ora" + }, + "names":{ + "cleanupBruteforce":"Pulisci casche forza bruta", + "cleanupDataLogs":"Pulisci i log delle modifiche scadute", + "cleanupFiles":"Elimina i caricamenti di file scaduti", + "cleanupLogs":"Pulisci i log di sistema scaduti", + "cleanupTempDir":"Pulisci cartella temporanea", + "embeddedBackup":"Gestisci backup integrati", + "importLdapLogins":"Importa login e ruoli tramite LDAP", + "mailAttach":"Trasferimento allegati e-mail", + "mailRetrieve":"Recupero e-mail", + "mailSend":"Invio email", + "repoCheck":"Esegui l'aggiornamento dell'archivio", + "updateCheck":"Verifica aggiornamenti della piattaforma" + }, + "dateAttempt":"Ultimo avvio", + "dateSuccess":"Ultimo completamento riuscito", + "functions":"Attività dell'applicazione", + "interval":"Intervallo esecuzione", + "intervalSeconds":"intervallo esecuzione (in secondi)", + "intervalTypeDays":"giorno(i)", + "intervalTypeHours":"ora(e)", + "intervalTypeMinutes":"minuto(i)", + "intervalTypeMonths":"mese(i)", + "intervalTypeSeconds":"secondo(i)", + "intervalTypeWeeks":"settimana(e)", + "intervalTypeYears":"anno(i)", + "scheduleLine":"Ogni {VALUE} {TYPE}", + "scheduleLineDayMonths":"al {DAY}.", + "scheduleLineDayWeeks":"al {DAY}. weekday", + "scheduleLineDayYears":"al {DAY}. of the year", + "scheduleLineTime":"alle {HH}:{MM}:{SS}", + "systemTasks":"Attività di sistema" + }, + "navigationConfig":"Sistema", + "navigationLdaps":"LDAP-Logins", + "navigationLicense":"Licenza", + "navigationLogins":"Logins", + "navigationLogs":"Logs", + "navigationMailAccounts":"Account Email", + "navigationMails":"Spooler Email", + "navigationModules":"Applicazioni", + "navigationRepo":"Archivio", + "navigationRoles":"Ruolo", + "navigationScheduler":"Pianificatore", + "title":"Amministrazione", + "titleDocs":"Documentazione amministrazione" + }, + "builder":{ + "attribute":{ + "dialog":{ + "delete":"Sei sicuro di voler eliminare questo attributo? Questo eliminerà anche tutti i dati inclusi dal tuo sistema.

Questa azione è irreversibile senza backup correnti." + }, + "new":"Nuovo attributo" + }, + "form":{ + "chart":{ + "axisType":"Tipo assi", + "axisTypeCategory":"Categoria", + "axisTypeLog":"Log", + "axisTypeTime":"Tempo", + "axisTypeValue":"Valore", + "help":"I grafici vengono creati con Apache ECharts. La sorgente dati viene riempita dal campo della query; i valori sono referenziati dall'indice di colonna (0 è la prima colonna). Possono essere utilizzati altri tipi di serie (oltre a barre, linee, ...) (vedi ECharts opzioni).", + "serieColumnTooltip": "Colonna per tooltip", + "serieColumnX":"Colonna per asse X", + "serieColumnY":"Colonna per asse Y", + "serieTypeBar":"Barre", + "serieTypeLine":"Linee", + "serieTypePie":"Torta", + "serieTypeScatter":"Scatter", + "series":"Serie" + }, + "dialog":{ + "delete":"Sei sicuro di voler eliminare questo modulo?" + }, + "option":{ + "displayColor":"colere", + "displayDefault":"predefinito", + "displayDate":"data", + "displayDatetime":"data ora", + "displayEmail":"email", + "displayGallery":"galleria", + "displayHidden":"nascosto", + "displayLogin":"login", + "displayPhone":"telefono", + "displayTime":"ora", + "displayTextarea":"area testo", + "displayRichtext":"testo formattato", + "displaySlider":"valore a cursore", + "displayUrl":"URL", + "ganttStepsDays":"giorni", + "ganttStepsHours":"ore" + }, + "states":{ + "button":{ + "addCondition":"Aggiungi condizione", + "addEffect":"Aggiungi effetto", + "showAll":"Mostra tutto" + }, + "option":{ + "filterFieldIdHint":"Per campo" + }, + "conditions":"Condizioni", + "descriptionHint":"Descrizione", + "effects":"Effetti", + "fieldChanged":"Modificato", + "modeField":"Valore campo", + "modeField2Field":"valore campo", + "modeField2Fixed":"valore fisso", + "modeField2Preset":"preimpostato", + "modeRecord":"Stato record", + "modeRole":"Ruolo", + "newRecord":"Nuovo", + "title":"Stati modulo" + }, + "aggregator":"Aggregato", + "attributeRecord":"Applica il record del modulo a", + "autoRenew":"Auto refresh", + "autoRenew0":"non attivo", + "autoRenewHint":"in secondi", + "autoSelect":"Selezione automatica dei record", + "autoSelectHint":"0=off, 2=primi due, -3=ultimi tre", + "captions":"Titoli", + "category":"Selettore categoria", + "columnsTarget":"Posiziona le colonne qui", + "columnsTemplates":"Colonne disponibili", + "columnBatch":"Raggruppamento di colonne", + "columnBatchNot":"non raggruppata", + "columnBatchHint":"La colonna fa parte del gruppo {CNT}", + "columnBatchHintNot":"La colonna non è raggruppata con altre", + "columnHeaderData":"Recupero dei dati", + "columnLength":"Lunghezza (carat.)", + "columnLength0":"senza limite", + "columnSize":"Dimensione (pixel)", + "columnSize0":"automatico", + "columnTitle":"Titolo", + "columnWrap":"A capo automatico", + "containerContentLayout":"Disposizione dei contenuti", + "content":"Query dati del modulo", + "contentField":"Query dati del campo", + "copy":"Duplica modulo", + "copyForm":"Seleziona modulo", + "copyNewName":"Nuovo nome del modulo", + "csvExport":"Esporta CSV", + "csvImport":"Importa CSV", + "date0":"Data da", + "date1":"Data a", + "dateColor":"Colore", + "dateRange0":"Eventi giorni prima", + "dateRange1":"Eventi giorni dopo", + "dateRangeHint":"rispetto alla data attuale", + "display":"Visualizza", + "distincted":"Distinto", + "fields":"Campi", + "fieldAttributeIdAltDateTo":"Data a (periodo)", + "fieldAttributeIdAltRichtextFiles":"Accesso file Richtext", + "fieldDefault":"Valore predefinito", + "fieldDefaultHint":"Controlla i documenti per i segnaposto", + "fieldDefaultPresetIds":"Preimpostazioni predefinite", + "fieldDirection":"Direzione", + "fieldHelp":"Testo aiuto", + "fieldIcon":"Icona campo", + "fieldMax":"Massimo valore/lunghezza", + "fieldMin":"Minimo valore/lunghezza", + "fieldMoveSource":"Sposta questo campo", + "fieldMoveTarget":"Sposta il campo selezionato dopo questo campo", + "fieldMoveInside":"Sposta il campo selezionato all'interno di questo campo", + "fieldOptions":"Opzioni campo", + "fieldRegexCheck":"Convalida con RegEx", + "fieldSize":"Dimensione base (pixel)", + "fieldSize0":"automatico", + "fieldTitle":"Titolo", + "fileExt":"Estensione file", + "fileName":"nome file", + "fileSize":"Dimensione file", + "filterExpert":"Filtro esperto", + "filterQuick":"Filtro rapido", + "flexAlignContent":"Allinea contenuto", + "flexAlignItems":"Alinea elementi", + "flexJustifyContent":"Giustifica contenuto", + "flexSizeGrow":"Fattore di crescita", + "flexSizeMax":"Fattore di crescita (percentuale)", + "flexSizeMin":"Ridurre a (percentuale)", + "flexSizeShrink":"Fattore riduzione", + "flexWrap":"Wrap contenuto", + "formOpen":"Apri record (relazione 0)", + "formOpenEmpty":"Opri modulo", + "formTitle":"Titolo modulo", + "gantt":"Gantt", + "ganttNotes":"Column batch 1 groups results.", + "ganttSteps":"Intervallo Gantt", + "ganttStepsToggle":"Attiva/disattiva intervallo di Gantt", + "groupBy":"Raggruppa per", + "headerSize":"Dimensione", + "helpText":"testo aiuto", + "hidden":"Il campo è nascosto per impostazione predefinita", + "icon":"Icona modulo", + "ics":"Abbonamenti iCAL", + "layout":"Layout elenco", + "limit":"Limite risultato", + "new":"Nuovo modulo", + "onMobile":"Nella vista mobile", + "presetOpen":"Preimpostazione apertura", + "readonly":"Solo lettura", + "required":"Obbligatorio", + "showHelp":"Aiuto", + "showOutsideIn":"Riferimento relazione", + "showStates":"Stati", + "sql":"Anteprima SQL", + "state":"Stato", + "stateDefault":"predefinito", + "stateHidden":"nascosto", + "stateOptional":"opzionale", + "stateReadonly":"sola lettura", + "stateRequired":"obbligatorio", + "subQuery":"Sub query", + "subQueryAttribute":"Attributo", + "title":"Moduli" + }, + "help":{ + "title":"Aiuto Applicazione" + }, + "icon":{ + "add":"Aggiungi/modifica file icona", + "addHelp":"Sono supportati solo file PNG con max. 64 kb. Le icone dovrebbero essere sempre quadrate.", + "title":"Icone" + }, + "loginForm":{ + "attributeLogin":"Attributo login", + "attributeLookup":"Attributo di ricerca", + "formOpen":"Modulo da aprire", + "newLoginForm":"Nuovo modulo login", + "title":"Moduli Login" + }, + "menu":{ + "button":{ + "add":"Aggiungi voce" + }, + "copy":"Copia tutte le voci di menu da", + "formId":"- Seleziona modulo -", + "showChildrenHint":"Mostra le voci secondarie per impostazione predefinita", + "title":"Menu" + }, + "module":{ + "button":{ + "check":"Controlla l'esportazione", + "export":"Esporta", + "exportKeySet":"Memorizza la chiave in memoria", + "graph":"Grafico delle dipendenze", + "keyCreate":"Genera", + "versionCreate":"Crea nuova versione" + }, + "color":"Color", + "dependsOn":"Depende da", + "export":"Trasferisci - Esporta l'applicazione in un file", + "exportKeyEmpty":"La chiave di firma è necessaria per l'esportazione", + "exportKeySet":"Chiave salvata in memoria", + "exportOwner":"Esporta modifiche", + "exportPrivateKeyHint":"Esempio:\n-----BEGIN RSA PRIVATE KEY-----\nKEY\n-----END RSA PRIVATE KEY-----", + "exportState":"Stato esportazione", + "exportStateNok":"Modifiche alla versione attuale", + "exportStateOk":"Pronto", + "graphDependsOn":"Dipende da: {COUNT}", + "keyCreate":"Trasferimento - Genera nuova coppia di chiavi di firma", + "keyCreateInfo":"Assicurati di tenere sempre al sicuro la chiave privata. La chiave pubblica deve essere importata nelle istanze REI3 di destinazione per accettare le tue applicazioni.", + "keyCreateLength":"Seleziona la lunghezza della chiave", + "languageCode":"Codice lingua", + "languageCodeHint":"en_us, zh_cn, de_de, ...", + "languageMain":"Lingua principale*", + "languageMainHint":"*viene utilizzato se la lingua utente scelta non è disponibile.", + "languageTitle":"Titolo applicazione", + "languages":"Langue", + "new":"Nuova applicazione", + "parent":"Assegnato a", + "position":"Posizione predefinita", + "releaseBuild":"Versione appl.", + "releaseBuildApp":"Versione Piattaforma", + "releaseDate":"Data rilascio", + "startForm":"Modulo iniziale", + "startFormDefault":"Modulo iniziale predefinito*", + "startFormDefaultHint":"*è utilizzato se nessun altro modulo può essere assegnato in base al ruolo.", + "startForms":"Moduli iniziali", + "startFormsExplanation":"Viene utilizzato il primo modulo iniziale che corrisponde a un ruolo del login corrente.", + "title":"Applicazioni" + }, + "pgFunction":{ + "button":{ + "addNew":"NUOVO prefisso", + "addOld":"VECCHIO prefisso", + "addSchedule":"Aggiungi nuova pianificazione", + "details":"Dettagli", + "template":"Template" + }, + "dialog":{ + "delete":"Sei sicuro di voler eliminare questa funzione?" + }, + "help":{ + "abort_show_message":"instance.abort_show_message(message TEXT) => VOID

Interrompe l'operazione e invia un messaggio all'utente che lo esegue, che viene visualizzato nell'interfaccia utente.", + "get_login_id":"instance.get_login_id() => INTEGER

Restituisce l'ID del login che sta eseguendo l'operazione.", + "get_login_language_code":"instance.get_login_language_code() => TEXT

Restituisce il codice della lingua di 5 lettere ('en_us', ...) del login, utilizzato per accedere al backend dei dati.", + "get_name":"instance.get_name() => TEXT

Restituisce il nome dell'istanza, definito nella finestra di personalizzazione del pannello di amministrazione.", + "get_public_hostname":"instance.get_public_hostname() => TEXT

Restituisce il nome dell'host pubblico dell'istanza, definito nella configurazione del sistema.", + "get_role_ids":"instance.get_role_ids(
login_id INTEGER,
inherited BOOLEAN DEFAULT FALSE
) => UUID[]

Ottieni un vettore con gli ID dei ruoli assegnati al login indicato.

Se 'inherited' è impostato su TRUE, i ruoli principali sono inclusi. Le appartenenze nidificate sono state completamente risolte.", + "has_role":"instance.has_role(
login_id INTEGER,
role_id UUID,
inherited BOOLEAN DEFAULT FALSE
) => BOOLEAN

Restituisce se al login indicato è assegnato l'ID di ruolo specificato.

Se 'inherited' è impostato su TRUE, i ruoli padre sono inclusi. Le appartenenze nidificate sono state completamente risolte.

Esempio: SELECT instance.has_role(1,'00000000-0000-0000-0000-00000000001',FALSE)", + "has_role_any":"instance.has_role_any(
login_id INTEGER,
role_ids UUID[],
inherited BOOLEAN DEFAULT FALSE
) => BOOLEAN

Restituisce se al login indicato è assegnato uno degli ID di ruolo specificati.

Se 'inherited' è impostato su TRUE, i ruoli padre sono inclusi. Le appartenenze nidificate sono state completamente risolte.

Esempio: SELECT instance.has_role_any(1,ARRAY['00000000-0000-0000-0000-00000000001','00000000-0000 0000-0000-00000000002']: :UUID[],FALSE)", + "log_error":"instance.log_error(message TEXT, app_name TEXT DEFAULT NULL) => VOID

Registra il messaggio di errore. Se il nome dell'applicazione può essere risolto, il log è associato ad esso.", + "log_info":"instance.log_error(message TEXT, app_name TEXT DEFAULT NULL) => VOID

Registra il messaggio informativo. Se il nome dell'applicazione può essere risolto, il log è associato ad esso.", + "log_warning":"instance.log_error(message TEXT, app_name TEXT DEFAULT NULL) => VOID

Messaggio di avviso dei registri. Se il nome dell'applicazione può essere risolto, il log è associato ad esso.", + "mail_delete":"instance.mail_delete(mail_id INTEGER) => INTEGER

Elimina l'e-mail specificata, inclusi gli allegati.", + "mail_delete_after_attach":"instance.mail_delete_after_attach(
mail_id INTEGER,
attach_record_id INTEGER,
attach_attribute_id UUID
) => INTEGER

Contrassegna gli allegati di posta elettronica da aggiungere a un attributo file del record specificato; l'e-mail e i relativi allegati vengono eliminati successivamente.", + "mail_get_next":"instance.mail_get_next(account_name TEXT DEFAULT NULL) => instance.mail

Restituisce l'e-mail successiva dallo spooler di posta; restituisce NULL se non è disponibile alcuna email. Quando viene specificato un nome account, restituisce solo i messaggi ricevuti con l'account specificato.

Il tipo restituito 'instance.mail' è composto da:
id INTEGER,
from_list TEXT,
to_list TEXT,
cc_list TEXT,
subject TEXT,
body TEXT
Dopo aver elaborato un'email, questa dovrebbe essere eliminata; direttamente (mail_delete) o dopo aver memorizzato i suoi allegati (mail_delete_after_attach).", + "mail_send":"instance.mail_send(
subject TEXT,
body TEXT,
to_list TEXT DEFAULT '',
cc_list TEXT DEFAULT '',
bcc_list TEXT DEFAULT '',
account_name TEXT DEFAULT NULL,
attach_record_id INTEGER DEFAULT NULL,
attach_attribute_id UUID DEFAULT NULL
) => INTEGER

Genera un messaggio di posta elettronica in uscita per lo spooler di posta. Parametri opzionali:" + }, + "option":{ + "intervalDays":"Giorni", + "intervalHours":"Ore", + "intervalMinutes":"Minuti", + "intervalMonths":"Mesi", + "intervalSeconds":"Secondi", + "intervalWeeks":"Settimane", + "intervalYears":"Anni" + }, + "code":"Corpo funzione", + "codeArgs":"Argomenti", + "codeArgsHint":"opzionale, 'var1 integer'", + "codeReturns":"Returns", + "codeReturnsHint":"espressione, 'trigger', 'integer',...", + "entityInput":"Seleziona il segnaposto e fai clic sul corpo della funzione per utilizzare la sintassi upgrade-safe.", + "intervalAtDayForMonths":"al giorno del mese", + "intervalAtDayForWeeks":"al giorno della settimana (0=do, 1=lu, ...)", + "intervalAtDayForYears":"al giorno dell'anno", + "intervalAtTime":"all'ora", + "intervalEvery":"Esegui: Sempre", + "new":"Nuova funzione", + "placeholders":"Segnaposti", + "placeholdersInstance":"Instanza", + "placeholdersModules":"Applicazioni", + "preview":"Anteprima", + "runOnce":"esegui una volta", + "schedules":"Ricorrente", + "schedulesItem":"{CNT} pianificazioni", + "title":"Funzioni", + "titleOne":"titolo funzione" + }, + "pgTrigger":{ + "dialog":{ + "delete":"Sei sicuro di voler eliminare questo trigger?" + } + }, + "preset":{ + "dialog":{ + "delete":"Sei sicuro di voler eliminare questo record predefinito?

ATTENZIONE!

A seconda della definizione della chiave esterna (ON UPDATE/DELETE), l'eliminazione di i record preimpostati possono causare la perdita dei dati del cliente (CASCADED) o problemi di aggiornamento dell'applicazione (NO ACTION)." + }, + "new":"Nuovo predefinito", + "protected":"Protetto", + "value":"Valori attributi", + "valueCount":"{CNT} valore(i)" + }, + "query":{ + "choice":"Imposta", + "choices":"Filtri impostati ({COUNT})*", + "choicesHint":"*Il primo set è attivo per impostazione predefinita - scelta disponibile se ne esistono 2 o più.", + "filters":"Filtri ({COUNT})", + "fixedLimit":"Limite risultati impostato", + "fixedLimit0":"non attivo", + "joinAddHint":"Unisci un'altra relazione a questa", + "joinApplyCreateHint":"(C)rete: creaa un record su questa relazione", + "joinApplyDeleteHint":"(D)elete: cancella un record su questa relazione", + "joinApplyUpdateHint":"(U)pdate: Aggiorna un record su questa relazione", + "joinConnectorHint":"(L)eft/(I)nner/(R)ight/(F)ull join", + "lookups":"Importa lookups per unique index ({COUNT})", + "lookupWarning":"Se gli attributi di relazione (n:1, 1:1) vengono utilizzati negli indici univoci,le relazioni riferite devono essere incluse nell'importazione.", + "orders":"Ordinamento ({COUNT})", + "relations":"relazioni ({COUNT})", + "select":"Seleziona relazioni per join" + }, + "relation":{ + "dialog":{ + "deleteRelation":"Sei sicuro di voler eliminare questa relazione? Questo eliminerà anche tutti i dati inclusi dal tuo sistema.

Questa azione è irreversibile senza backup correnti." + }, + "attributes":"Attributi ({CNT})", + "codeCondition":"Condizione", + "content":"Contenuto", + "def":"PREDDEFINITO", + "execute":"Eseguire", + "external":"Relazioni di riferimento ({CNT})", + "fires":"Accensioni", + "graph":"Grafico delle relazioni", + "graphBase":"relazione base", + "indexes":"Indici ({CNT})", + "indexAttributes":"Attributi", + "indexAutoFki":"Sistema", + "indexCreate":"Aggiungi attributo al nuovo indice", + "indexUnique":"Univoco", + "isConstraint":"CONSTRAINT", + "isDeferrable":"DEFERRABLE", + "isDeferred":"INIT. DEFERRED", + "length":"Lunghezza", + "newRelation":"Nuova relazionen", + "nullable":"Annullabile", + "onDelete":"DELETE", + "onInsert":"INSERT", + "onUpdate":"UPDATE", + "perRow":"EACH ROW", + "policies":"Politica", + "policyActions":"Azione", + "policyActionDelete":"Dancella", + "policyActionSelect":"Visualizza", + "policyActionUpdate":"Modifica", + "policyExplanation":"Viene applicato il primo criterio che corrisponde a un ruolo del login corrente, se l'azione tentata è abilitata per questo criterio. Per concedere l'accesso completo a un ruolo, ad esempio, è possibile posizionare in cima un criterio con tutte le azioni abilitate e nessun filtro.", + "policyFunctions":"Filtro funzioni", + "policyFunctionExcl":"Blocca il record ID", + "policyFunctionIncl":"Abilita il record ID", + "policyNotSet":"non filtrato", + "presets":"Record preimpostati ({CNT})", + "presetProtected":"Protetto", + "presetValues":"Valori", + "presetValuesPreview":"Anteprima valori", + "preview":"Visualizzazione dati", + "previewLimit":"Limite", + "previewPage":"Pagina", + "relationship":"Relazioni", + "retention":"Log cambiamenti", + "retentionCount":"Mantieni X modifiche", + "retentionDays":"Mantieni X giorni", + "title":"Relazioni", + "titleOne":"Dettagli relazione '{NAME}'", + "triggers":"Triggers ({CNT})" + }, + "role":{ + "dialog":{ + "delete":"Sei sicuro di voler eliminare questo ruolo?" + }, + "option":{ + "accessDelete":"RWD", + "accessInherit":"[inherit]", + "accessNone":"-", + "accessRead":"R", + "accessWrite":"RW" + }, + "access":"Accesso", + "assignable":"Assignabile", + "attribute":"Attributi", + "children":"Membro di", + "data":"Dati", + "legend":"Legenda: No accesso (-), lettura (R), scrittura (W), cancellazione (D)", + "menu":"Menu", + "menus":"Menu", + "newRole":"Nuovo ruolo", + "relation":"Relazione", + "title":"Ruoli", + "titleOne":"Ruolo '{NAME}'" + }, + "backHint":"Torna alla panoramica dell'applicazione", + "language":"Traduzione", + "languageNextHint":"Passa alla lingua successiva (tasto di scelta rapida: CTRL + q)", + "navigationFilterHint":"Filtra per testo", + "navigationForms":"Moduli", + "navigationFormsSub":"Disegna modulo", + "navigationHelp":"Aiuto", + "navigationIcons":"Icone", + "navigationLoginForms":"Modulo login", + "navigationMenu":"Menu", + "navigationPgFunctions":"Funzioni", + "navigationPgFunctionsSub":"dettagli funzione", + "navigationRelations":"Relazioni", + "navigationRelationsSub":"Dettagli Relazione", + "navigationRoles":"Ruoli", + "navigationRolesSub":"Ruolp accesso", + "pageTitle":"Builder", + "pageTitleDocs":"Documentazione Builder" + }, + "calendar":{ + "button":{ + "ics":"Accesso", + "icsHint":"Accesso calendario da applicazione esterna", + "icsPublish":"Pubblica per uso personale", + "ganttToggleHint":"Passa dalla modalità giorno a quella ora", + "ganttShowLabels":"Gruppi", + "ganttShowLabelsHint":"Mostra/nascondi groppi", + "zoomResetHint":"Ripristina livello di zoom" + }, + "icsDesc":"Usa questo URL per iscriverti a questo calendario (iCalendar, ICS).", + "month0":"Gennaio", + "month1":"Febbraio", + "month2":"Marzo", + "month3":"Aprile", + "month4":"Maggio", + "month5":"Giugno", + "month6":"Luglio", + "month7":"Agosto", + "month8":"Settembre", + "month9":"Ottobre", + "month10":"Novembre", + "month11":"Dicembre", + "now":"Ora", + "nowHint":"Mostra ora/data corrente", + "today":"Oggi", + "todayHint":"Mostra/seleziona oggi", + "weekDay0":"Domenica", + "weekDay1":"Lunedì", + "weekDay2":"Martedì", + "weekDay3":"Mercoledì", + "weekDay4":"Giovedì", + "weekDay5":"venerdì", + "weekDay6":"Sabato", + "weekDayShort0":"Dom", + "weekDayShort1":"Lun", + "weekDayShort2":"Mar", + "weekDayShort3":"Mer", + "weekDayShort4":"Gio", + "weekDayShort5":"Ven", + "weekDayShort6":"Sab" + }, + "feedback":{ + "button":{ + "whatIsSent":"Cosa viene inviato?" + }, + "option":{ + "codeBug":"Bug / Issue", + "codeGeneric":"General", + "codePraise":"Praise", + "codeSuggestion":"Suggestion" + }, + "moduleRelated":"Questo feedback è per l'applicazione aperta? ({NOME})", + "sendNok":"Non è stato possibile inviare il feedback. Per favore riprova più tardi.", + "sendOk":"Il feedback è stato inviato con successo. Grazie!", + "submit":"Fare clic sull'icona appropriata per inviare un feedback:", + "textHint":"Inserisci qui il feedback (facoltativo)", + "title":"Invia feedback anonimo", + "whatIsSent":"Oltre ai campi visibili (testo del feedback, smiley scelto, ecc.) vengono inviati i seguenti dati:
", + "whatIsSentPost":"Questi dati aiutano a elaborare il feedback. Non vengono inviate informazioni personali. Prendiamo molto sul serio la privacy." + }, + "filter":{ + "option":{ + "connector":{ + "AND":"AND", + "OR":"OR" + }, + "content":{ + "attribute":"valore attributo", + "field":"valore campo", + "javascript":"Espressione JavaScript", + "languageCode":"codice lingua login (en_us, ...)", + "login":"ID login (integer)", + "preset":"ID preimpostato (integer)", + "record":"ID record (integer, relazione modulo 0)", + "recordNew":"il record è nuovo (TRUE/FALSE, modulo)", + "role":"il login ha ruolo (TRUE/FALSE)", + "subQuery":"sub query", + "true":"valore vero (TRUE)", + "value":"valore fisso" + }, + "operator":{ + "eq":"uguale", + "eqAny":"include", + "le":"maggiore/uguale", + "lt":"maggiore", + "ilike":"contiene", + "like":"contiene (corrispondenza maiuscole)", + "ne":"diverso", + "neAll":"escluso", + "not_ilike":"non contiene", + "not_like":"non contiene (corrispondenza maiuscole)", + "not_null":"non vuoto", + "null":"vuoto", + "se":"minore/uguale", + "st":"minore" + } + }, + "add":"Aggiungi", + "javascriptHint":"Es.: return new Date().getFullYear();", + "nestingMain":"Query principale", + "nestingSub":"Sub query", + "queryAggregatorNull":"no aggregazione", + "queryShow":"Mostra sub query", + "valueHint":"Valore testo o numero" + }, + "form":{ + "button":{ + "help":"Aiuto", + "helpHint":"Mostra pagine aiuto", + "log":"Log", + "logHint":"Apri log modifiche", + "retractLogs":"Ritira logs" + }, + "dialog":{ + "delete":"Sei sicuro di voler eliminare questo record in modo permanente?" + }, + "dataLog":"Registri delle modifiche", + "deletedUser":"Utenti cancellati", + "help":"Pagine aiuto", + "helpContextTitle":"Contesto", + "helpModuleTitle":"Applicazione", + "invalidInputs":"Controlla input!", + "noAccess":"Nessun accesso" + }, + "home":{ + "button":{ + "goToApps":"Apri file import", + "goToLogins":"Assegna ruoli", + "goToRepo":"Apri archivio", + "installBundle":"Installa Core Company", + "openHelp":"Pagina aiuto" + }, + "wizard":{ + "installBundle":"Core Company", + "installBundleDesc":"Installa un set preconfezionato di applicazioni principali, utile per qualsiasi organizzazione. L'insieme \"Core Company\" include:Dopo l'installazione, dai un'occhiata alle pagine di aiuto delle singole applicazioni per saperne di più sulla configurazione iniziale di ciascuna.", + "installFile":"Da file", + "installFileDesc":"Installa applicazioni da file REI3 locali. Per sistemi offline o applicazioni fai da te.

I file per le applicazioni REI3 offerte pubblicamente possono essere scaricati dal sito ufficiale repository.", + "installRepo":"Da archivio", + "installRepoDesc":"Accesso a Internet richiesto

Installa applicazioni dal repository REI3 ufficiale. Il repository contiene le versioni correnti delle applicazioni REI3 offerte pubblicamente.

Tutte le applicazioni sono riviste e firmate digitalmente.", + "intro":"Benvenuto in REI3

Al momento non ci sono applicazioni installate. Per iniziare a utilizzare REI3, scegli una delle opzioni di installazione di seguito.", + "title":"Setup" + }, + "firstSteps":"Primi passi", + "firstStepsIntro":"La maggior parte delle applicazioni richiede una configurazione iniziale e/o dati permanenti per essere utili. Si prega di fare riferimento alle singole pagine di aiuto per iniziare:", + "newVersion":"Notifica aggiornamento", + "newVersionText":"È disponibile una nuova versione di REI3.

Scarica REI3 v{VERSION}", + "noAccess":"Non è stato ancora concesso l'accesso.
Contatta il tuo amministratore di sistema.", + "noAccessAdmin":"Per ottenere l'accesso, assegna i ruoli al tuo login.", + "noAccessTitle":"Nessun accesso alle applicazioni", + "title":"Home page" + }, + "input":{ + "date":{ + "dateFrom":"Da", + "dateTo":"A", + "fullDayHint":"Selezione per tutto il giorno", + "inputDay":"dd", + "inputHour":"--", + "inputMinute":"--", + "inputMonth":"mm", + "inputSecond":"--", + "inputYear":"yyyy" + } + }, + "list":{ + "button":{ + "all":"Tutto", + "allHint":"Seleziona tutti i risultati", + "autoRenew":"{VALUE}s", + "autoRenewHint":"Aggiorna i dati ogni {VALUE} secondi", + "csv":"CSV", + "csvHint":"Importa/esporta record tramite file di valori separati da virgole" + }, + "dialog":{ + "delete":"Sei sicuro di voler eliminare in modo permanente i record selezionati?" + }, + "error":{ + "csvDateParse":"Valore temporale '{VALUE}' non valido, formato previsto: {EXPECT}", + "csvFieldCount":"Numero di campi errato", + "csvLineError":"Errore alla linea {COUNT}: ", + "csvNotNull":"La colonna '{NAME}' non ha valori o non può essere risolto", + "csvNumberParse":"Numero non valido '{VALUE}'", + "csvTypeSyntax":"Valore non valido '{VALUE}'" + }, + "message":{ + "csvExport":"L'esportazione CSV utilizza filtri e ordini dall'elenco corrente.", + "csvImport":"L'importazione CSV richiede {COUNT} campi dall'elenco mostrato di seguito, nello stesso ordine per ogni riga.", + "csvImportSuccess":"{COUNT} linee sono state importate" + }, + "option":{ + "csvExport":"esporta", + "csvImport":"importa" + }, + "autoRenew":"Rinnovo automatico", + "autoRenewInput":"Aggiorna i dati ogni X secondi", + "autoRenewInputHint":"min. 10s", + "csvAction":"Attenzione", + "csvBool":"Stringhe booleane", + "csvCommaChar":"Delimitatore campo", + "csvDate":"Formato data", + "csvDatetime":"Formato data-ora", + "csvFile":"File CSV", + "csvHasHeader":"Linea intestazione", + "csvTime":"Formato tempo", + "csvTimeHint":"15:04:05", + "csvTimezone":"Timezone", + "csvTotalLimit":"Conteggio righe (0 = tutte)", + "inputHintCreate":"Crea nuovo record", + "inputHintOpen":"Apri record", + "inputHintRemove":"Rimuovi record", + "inputHintSelect":"Aggiungi record esistente", + "inputPlaceholderAdd":"...aggiungi di più", + "orderBy":"Ordina per", + "quick":"Filtra risultati in tutte le colonne" + }, + "settings":{ + "button":{ + "logout":"Logout" + }, + "option":{ + "cornerKeep":"- predefinito -", + "cornerRounded":"arrotondato", + "cornerSquared":"squadrato" + }, + "security":{ + "messagePwCurrentWrong":"La password attuale non è valida", + "messagePwDiff":"Le password non corrispondono", + "messagePwRequiresDigit":"La password deve contenere una cifra", + "messagePwRequiresLower":"La password deve contente una lettera minuscola", + "messagePwRequiresSpecial":"La password deve contenere un carattere speciale (!, ?, #, ecc.)", + "messagePwRequiresUpper":"La password deve contente una lettera maiuscola", + "messagePwShort":"Password troppo corta", + "pwNew0":"Nuova password", + "pwNew1":"Nuova password (ripetere)", + "pwOld":"Password attuale", + "titlePwChange":"Cambia password" + }, + "bordersAll":"Aggiungi bordi", + "bordersCorners":"Angoli del bordo", + "compact":"Layout compatto", + "dateFormat":"Formato Data", + "dateFormat0":"Y-m-d (2012-12-30)", + "dateFormat1":"Y/m/d (2012/12/30)", + "dateFormat2":"d.m.Y (30.12.2012)", + "dateFormat3":"d/m/Y (30/12/2012)", + "dateFormat4":"m/d/Y (12/30/2012)", + "dark":"Modalità scura", + "fontSize":"Font size", + "headerCaptions":"Mostra titolo menu principale", + "hintFirstSteps":"Mostra 'primi-passi'", + "languageCode":"Lingua", + "pageLimit":"Max. larghezza della pagina", + "pageTitle":"Impostazioni", + "spacing":"Spaziatura", + "sundayFirstDow":"Domenica è il primo giorno della settimana", + "titleAdmin":"Admin", + "titleDisplay":"Visualizza", + "titleSecurity":"Securezza", + "titleTheme":"Tema" + } +} diff --git a/var/packages/core_company.rei3 b/cache/packages/core_company.rei3 similarity index 100% rename from var/packages/core_company.rei3 rename to cache/packages/core_company.rei3 diff --git a/compatible/compatible.go b/compatible/compatible.go index 55824e41..2566c1ed 100644 --- a/compatible/compatible.go +++ b/compatible/compatible.go @@ -1,2 +1,34 @@ /* central package for fixing issues with modules from older versions */ package compatible + +import ( + "github.com/jackc/pgtype" +) + +// general fix: pgx types use UNDEFINED as default state, we need NULL to work with them +func FixPgxNull(input interface{}) interface{} { + + switch v := input.(type) { + case pgtype.Bool: + if v.Status == pgtype.Undefined { + v.Status = pgtype.Null + } + return v + case pgtype.Int4: + if v.Status == pgtype.Undefined { + v.Status = pgtype.Null + } + return v + case pgtype.Varchar: + if v.Status == pgtype.Undefined { + v.Status = pgtype.Null + } + return v + case pgtype.UUID: + if v.Status == pgtype.Undefined { + v.Status = pgtype.Null + } + return v + } + return input +} diff --git a/config/config.go b/config/config.go index f9a09556..d83e6c38 100644 --- a/config/config.go +++ b/config/config.go @@ -29,7 +29,7 @@ var ( appVersionBuild string // build counter of this application (1023) // configuration file location - filePath string = "config.json" + filePath string // location of configuration file in JSON format filePathTempl string = "config_template.json" // configuration values from file, must not be changed during runtime @@ -38,10 +38,6 @@ var ( // operation data TokenSecret *jwt.HMACSHA License types.License = types.License{} - - // system language codes - languageCodeDefault = "en_us" - languageCodes = []string{"de_de", "en_us"} ) // returns @@ -65,12 +61,6 @@ func GetConfigFilepath() string { func GetLicenseActive() bool { return License.ValidUntil > tools.GetTimeUnix() } -func GetLanguageCodeValid(requestedCode string) string { - if tools.StringInSlice(requestedCode, languageCodes) { - return requestedCode - } - return languageCodeDefault -} // setters func SetAppVersion(version string) { diff --git a/config/config_caption.go b/config/config_caption.go deleted file mode 100644 index 701b689a..00000000 --- a/config/config_caption.go +++ /dev/null @@ -1,41 +0,0 @@ -package config - -import ( - "encoding/json" - "errors" - "path/filepath" - "r3/tools" -) - -var ( - captionByCode = make(map[string]json.RawMessage) // app captions, key = language code (en_us, ...) -) - -// get all captions by language code -func GetAppCaptions(code string) (json.RawMessage, error) { - access_mx.Lock() - defer access_mx.Unlock() - - if _, exists := captionByCode[code]; !exists { - return json.RawMessage{}, errors.New("language code does not exist") - } - return captionByCode[code], nil -} - -// load application captions into memory for regular retrieval -func InitAppCaptions() (err error) { - access_mx.Lock() - defer access_mx.Unlock() - - // load application captions from text files - for _, code := range languageCodes { - - captionByCode[code], err = tools.GetFileContents( - filepath.Join(File.Paths.Captions, code), true) - - if err != nil { - return err - } - } - return nil -} diff --git a/config/config_store.go b/config/config_store.go index 95ac3922..8c619b6a 100644 --- a/config/config_store.go +++ b/config/config_store.go @@ -23,14 +23,14 @@ var ( NamesUint64 = []string{"backupDaily", "backupMonthly", "backupWeekly", "backupCountDaily", "backupCountMonthly", "backupCountWeekly", - "bruteforceAttempts", "bruteforceProtection", "dbTimeoutCsv", - "dbTimeoutDataRest", "dbTimeoutDataWs", "dbTimeoutIcs", "icsDaysPost", - "icsDaysPre", "icsDownload", "logApplication", "logBackup", "logCache", - "logCsv", "logLdap", "logMail", "logServer", "logScheduler", - "logTransfer", "logsKeepDays", "productionMode", "pwForceDigit", - "pwForceLower", "pwForceSpecial", "pwForceUpper", "pwLengthMin", - "schemaTimestamp", "repoChecked", "repoFeedback", "repoSkipVerify", - "tokenExpiryHours"} + "bruteforceAttempts", "bruteforceProtection", "builderMode", + "dbTimeoutCsv", "dbTimeoutDataRest", "dbTimeoutDataWs", "dbTimeoutIcs", + "icsDaysPost", "icsDaysPre", "icsDownload", "logApplication", + "logBackup", "logCache", "logCsv", "logLdap", "logMail", "logServer", + "logScheduler", "logTransfer", "logsKeepDays", "productionMode", + "pwForceDigit", "pwForceLower", "pwForceSpecial", "pwForceUpper", + "pwLengthMin", "schemaTimestamp", "repoChecked", "repoFeedback", + "repoSkipVerify", "tokenExpiryHours"} ) // store setters diff --git a/config_dedicated.json b/config_dedicated.json index acd1d484..f11bf5c9 100644 --- a/config_dedicated.json +++ b/config_dedicated.json @@ -1,5 +1,4 @@ { - "builder":false, "db": { "embedded": false, "host": "localhost", @@ -9,15 +8,12 @@ "pass": "app" }, "paths": { - "captions": "var/texts/", "certificates": "data/certificates/", "embeddedDbBin": "pgsql/bin/", "embeddedDbData": "data/database/", "files": "data/files/", - "packages": "var/packages/", "temp": "data/temp/", - "transfer":"data/transfer", - "web": "www/" + "transfer": "data/transfer" }, "web": { "cert": "cert.crt", diff --git a/config_template.json b/config_template.json index d30ce29b..5e79a369 100644 --- a/config_template.json +++ b/config_template.json @@ -1,5 +1,4 @@ { - "builder":false, "db": { "embedded": true, "host": "localhost", @@ -9,15 +8,12 @@ "pass": "app" }, "paths": { - "captions": "var/texts/", "certificates": "data/certificates/", "embeddedDbBin": "pgsql/bin/", "embeddedDbData": "data/database/", "files": "data/files/", - "packages": "var/packages/", "temp": "data/temp/", - "transfer":"data/transfer", - "web": "www/" + "transfer": "data/transfer" }, "web": { "cert": "cert.crt", diff --git a/data/data.go b/data/data.go index 25b5e026..bda930dc 100644 --- a/data/data.go +++ b/data/data.go @@ -1,7 +1,12 @@ package data import ( + "fmt" "r3/cache" + "r3/schema/lookups" + "r3/tools" + "r3/types" + "strings" "github.com/gofrs/uuid" ) @@ -46,3 +51,90 @@ func authorizedRelation(loginId int64, relationId uuid.UUID, requestedAccess int } return false } + +// get applicable policy filter (e. g. WHERE clause) for data call +func getPolicyFilter(loginId int64, action string, tableAlias string, + policies []types.RelationPolicy) (string, error) { + + if len(policies) == 0 { + return "", nil + } + + access, err := cache.GetAccessById(loginId) + if err != nil { + return "", err + } + + clauses := []string{} + + // go through policies in order + for _, p := range policies { + + // ignore if login does not have role + if !tools.UuidInSlice(p.RoleId, access.RoleIds) { + continue + } + + // ignore if policy does not apply to requested action + switch action { + case "delete": + if !p.ActionDelete { + continue + } + case "select": + if !p.ActionSelect { + continue + } + case "update": + if !p.ActionUpdate { + continue + } + default: + return "", fmt.Errorf("unknown action '%s'", action) + } + + if p.PgFunctionIdExcl.Valid { + + fncName, err := getFunctionName(p.PgFunctionIdExcl.UUID) + if err != nil { + return "", err + } + + clauses = append(clauses, fmt.Sprintf(`"%s"."%s" <> ALL(%s())`, + tableAlias, lookups.PkName, fncName)) + } + + if p.PgFunctionIdIncl.Valid { + + fncName, err := getFunctionName(p.PgFunctionIdIncl.UUID) + if err != nil { + return "", err + } + + clauses = append(clauses, fmt.Sprintf(`"%s"."%s" = ANY(%s())`, + tableAlias, lookups.PkName, fncName)) + } + + // first matching policy is applied + break + } + + // no policy found or policy does not filter at all + if len(clauses) == 0 { + return "", nil + } + + return fmt.Sprintf("\nAND %s", strings.Join(clauses, "\nAND ")), nil +} + +func getFunctionName(pgFunctionId uuid.UUID) (string, error) { + fnc, exists := cache.PgFunctionIdMap[pgFunctionId] + if !exists { + return "", fmt.Errorf("unknown PG function '%s'", pgFunctionId) + } + mod, exists := cache.ModuleIdMap[fnc.ModuleId] + if !exists { + return "", fmt.Errorf("unknown module '%s'", fnc.ModuleId) + } + return fmt.Sprintf(`"%s"."%s"`, mod.Name, fnc.Name), nil +} diff --git a/data/data_del.go b/data/data_del.go index c7243f95..55095273 100644 --- a/data/data_del.go +++ b/data/data_del.go @@ -6,6 +6,7 @@ import ( "fmt" "r3/cache" "r3/handler" + "r3/schema/lookups" "github.com/gofrs/uuid" "github.com/jackc/pgx/v4" @@ -19,10 +20,9 @@ func Del_tx(ctx context.Context, tx pgx.Tx, relationId uuid.UUID, return errors.New(handler.ErrUnauthorized) } - // check source relation and module rel, exists := cache.RelationIdMap[relationId] if !exists { - return errors.New("relation does not exist") + return fmt.Errorf("unknown relation '%s'", relationId) } // check for protected preset record @@ -34,13 +34,23 @@ func Del_tx(ctx context.Context, tx pgx.Tx, relationId uuid.UUID, mod, exists := cache.ModuleIdMap[rel.ModuleId] if !exists { - return errors.New("module does not exist") + return fmt.Errorf("unknown module '%s'", rel.ModuleId) + } + + // get policy filter if applicable + tableAlias := "t" + policyFilter, err := getPolicyFilter(loginId, "delete", tableAlias, rel.Policies) + if err != nil { + return err } if _, err := tx.Exec(ctx, fmt.Sprintf(` - DELETE FROM "%s"."%s" - WHERE id = $1 - `, mod.Name, rel.Name), recordId); err != nil { + DELETE FROM "%s"."%s" AS "%s" + WHERE "%s"."%s" = $1 + %s + `, mod.Name, rel.Name, tableAlias, tableAlias, + lookups.PkName, policyFilter), recordId); err != nil { + return err } return nil diff --git a/data/data_get.go b/data/data_get.go index 58828b4a..b6c0facd 100644 --- a/data/data_get.go +++ b/data/data_get.go @@ -22,7 +22,7 @@ import ( var regexRelId = regexp.MustCompile(`^\_r(\d+)id`) // finds: _r3id // get data -// updates SQL query pointer value (for error logging), data rows and total count +// updates SQL query pointer value (for error logging), returns data rows + total count func Get_tx(ctx context.Context, tx pgx.Tx, data types.DataGet, loginId int64, query *string) ([]types.DataGetResult, int, error) { @@ -91,6 +91,9 @@ func Get_tx(ctx context.Context, tx pgx.Tx, data types.DataGet, loginId int64, Values: values, }) } + if err := rows.Err(); err != nil { + return results, 0, err + } rows.Close() // work out result count @@ -105,10 +108,9 @@ func Get_tx(ctx context.Context, tx pgx.Tx, data types.DataGet, loginId int64, return results, count, nil } -// build SELECT calls from data GET request -// queries can also be sub queries, a nesting level is included for separation (0 = main query) -// can optionally add expressions to collect all tupel IDs -// returns queries (data + count) +// build SQL call from data GET request +// also used for sub queries, a nesting level is included for separation (0 = main query) +// returns data + count SQL query strings func prepareQuery(data types.DataGet, queryArgs *[]interface{}, queryCountArgs *[]interface{}, loginId int64, nestingLevel int) (string, string, error) { @@ -120,50 +122,81 @@ func prepareQuery(data types.DataGet, queryArgs *[]interface{}, queryCountArgs * } var ( - inSelect []string // select expressions inJoin []string // relation joins + inSelect []string // select expressions + inWhere []string // filters mapIndex_relId = make(map[int]uuid.UUID) // map of all relations by index ) // check source relation and module rel, exists := cache.RelationIdMap[data.RelationId] if !exists { - return "", "", errors.New("relation does not exist") + return "", "", fmt.Errorf("unknown relation '%s'", data.RelationId) } mod, exists := cache.ModuleIdMap[rel.ModuleId] if !exists { - return "", "", errors.New("module does not exist") + return "", "", fmt.Errorf("unknown module '%s'", rel.ModuleId) } - // JOIN relations connected via relationship attributes + // define relation code for source relation + // source relation might have index != 0 (for GET from joined relation) + relCode := getRelationCode(data.IndexSource, nestingLevel) + + // add relations as joins via relationship attributes mapIndex_relId[data.IndexSource] = data.RelationId for _, join := range data.Joins { if join.IndexFrom == -1 { // source relation need not be joined continue } - if err := joinRelation(mapIndex_relId, join, &inJoin, nestingLevel); err != nil { + if err := addJoin(mapIndex_relId, join, &inJoin, nestingLevel); err != nil { return "", "", err } } - // define relation code for source relation - // source relation might have index != 0 (for GET from joined relation) - relCode := getRelationCode(data.IndexSource, nestingLevel) - - // build WHERE lines - // before SELECT expressions because these are excluded from count query + // add filters + // before expressions because these are excluded from 'total count' query // SQL arguments are numbered ($1, $2, ...) with no way to skip any (? placeholder is not allowed); - // excluded sub queries arguments from SELECT expressions causes missing argument numbers - queryWhere := "" - if err := buildWhere(data.Filters, queryArgs, queryCountArgs, loginId, - nestingLevel, &queryWhere); err != nil { + // excluded sub queries arguments from expressions causes missing argument numbers + for i, filter := range data.Filters { - return "", "", err + // overwrite first filter connector and add brackets in first and last filter line + // done so that query filters do not interfere with other filters + if i == 0 { + filter.Connector = "AND" + filter.Side0.Brackets++ + } + if i == len(data.Filters)-1 { + filter.Side1.Brackets++ + } + + if err := addWhere(filter, queryArgs, queryCountArgs, + loginId, &inWhere, nestingLevel); err != nil { + + return "", "", err + } + } + + for index, relationId := range mapIndex_relId { + + // add policy filter if applicable + rel, exists := cache.RelationIdMap[relationId] + if !exists { + return "", "", fmt.Errorf("unknown relation '%s'", relationId) + } + + policyFilter, err := getPolicyFilter(loginId, "select", + getRelationCode(index, nestingLevel), rel.Policies) + + if err != nil { + return "", "", err + } + inWhere = append(inWhere, policyFilter) } + queryWhere := strings.Replace(strings.Join(inWhere, ""), "AND", "\nWHERE", 1) - // process SELECT expressions + // add expressions mapIndex_agg := make(map[int]bool) // map of indexes with aggregation mapIndex_aggRecords := make(map[int]bool) // map of indexes with record aggregation for pos, expr := range data.Expressions { @@ -191,7 +224,7 @@ func prepareQuery(data types.DataGet, queryArgs *[]interface{}, queryCountArgs * } // attribute expression - if err := selectAttribute(pos, expr, mapIndex_relId, &inSelect, + if err := addSelect(pos, expr, mapIndex_relId, &inSelect, nestingLevel); err != nil { return "", "", err @@ -205,7 +238,7 @@ func prepareQuery(data types.DataGet, queryArgs *[]interface{}, queryCountArgs * } } - // SELECT relation tupel IDs after attributes on main query + // add expressions for relation tupel IDs after attributes (on main query) if nestingLevel == 0 { for index, relId := range mapIndex_relId { @@ -252,13 +285,22 @@ func prepareQuery(data types.DataGet, queryArgs *[]interface{}, queryCountArgs * queryGroup = fmt.Sprintf("\nGROUP BY %s", strings.Join(groupByItems, ", ")) } - // build ORDER BY, LIMIT, OFFSET lines - queryOrder, queryLimit, queryOffset := "", "", "" - if err := buildOrderLimitOffset(data, nestingLevel, &queryOrder, &queryLimit, &queryOffset); err != nil { + // build ORDER BY + queryOrder, err := addOrderBy(data, nestingLevel) + if err != nil { return "", "", err } - // build data query + // build LIMIT/OFFSET + queryLimit, queryOffset := "", "" + if data.Limit != 0 { + queryLimit = fmt.Sprintf("\nLIMIT %d", data.Limit) + } + if data.Offset != 0 { + queryOffset = fmt.Sprintf("\nOFFSET %d", data.Offset) + } + + // build final data retrieval SQL query query := fmt.Sprintf( `SELECT %s`+"\n"+ `FROM "%s"."%s" AS "%s" %s%s%s%s%s%s`, @@ -271,7 +313,7 @@ func prepareQuery(data types.DataGet, queryArgs *[]interface{}, queryCountArgs * queryLimit, // LIMIT queryOffset) // OFFSET - // build totals query (not relevant for sub queries) + // build final total count SQL query (not relevant for sub queries) queryCount := "" if nestingLevel == 0 { @@ -298,7 +340,7 @@ func prepareQuery(data types.DataGet, queryArgs *[]interface{}, queryCountArgs * // if attribute is from another relation than the given index (relationship), // attribute value = tupel IDs in relation with given index via given attribute // 'outside in' is important in cases of self reference, where direction cannot be ascertained by attribute -func selectAttribute(exprPos int, expr types.DataGetExpression, +func addSelect(exprPos int, expr types.DataGetExpression, mapIndex_relId map[int]uuid.UUID, inSelect *[]string, nestingLevel int) error { relCode := getRelationCode(expr.Index, nestingLevel) @@ -393,7 +435,7 @@ func selectAttribute(exprPos int, expr types.DataGetExpression, return nil } -func joinRelation(mapIndex_relId map[int]uuid.UUID, rel types.DataGetJoin, +func addJoin(mapIndex_relId map[int]uuid.UUID, rel types.DataGetJoin, inJoin *[]string, nestingLevel int) error { // check join attribute @@ -456,149 +498,128 @@ func joinRelation(mapIndex_relId map[int]uuid.UUID, rel types.DataGetJoin, } // fills WHERE string from data GET request filters and returns count of arguments -func buildWhere(filters []types.DataGetFilter, queryArgs *[]interface{}, - queryCountArgs *[]interface{}, loginId int64, nestingLevel int, where *string) error { - - bracketBalance := 0 - inWhere := make([]string, 0) - - for i, filter := range filters { - - // overwrite first filter connector and add brackets in first and last filter line - // done so that query filters do not interfere with other filters - if i == 0 { - filter.Connector = "AND" - filter.Side0.Brackets++ - } - if i == len(filters)-1 { - filter.Side1.Brackets++ - } +func addWhere(filter types.DataGetFilter, queryArgs *[]interface{}, + queryCountArgs *[]interface{}, loginId int64, inWhere *[]string, nestingLevel int) error { - if !tools.StringInSlice(filter.Connector, types.QueryFilterConnectors) { - return errors.New("bad filter connector") - } - if !tools.StringInSlice(filter.Operator, types.QueryFilterOperators) { - return errors.New("bad filter operator") - } - - bracketBalance -= filter.Side0.Brackets - bracketBalance += filter.Side1.Brackets - isNullOp := isNullOperator(filter.Operator) + if !tools.StringInSlice(filter.Connector, types.QueryFilterConnectors) { + return errors.New("bad filter connector") + } + if !tools.StringInSlice(filter.Operator, types.QueryFilterOperators) { + return errors.New("bad filter operator") + } - // define comparisons - var getComp = func(s types.DataGetFilterSide, comp *string) error { - var err error - var isQuery = s.Query.RelationId != uuid.Nil + isNullOp := isNullOperator(filter.Operator) - // sub query filter - if isQuery { - subQuery, _, err := prepareQuery(s.Query, queryArgs, - queryCountArgs, loginId, nestingLevel+1) + // define comparisons + var getComp = func(s types.DataGetFilterSide, comp *string) error { + var err error + var isQuery = s.Query.RelationId != uuid.Nil - if err != nil { - return err - } + // sub query filter + if isQuery { + subQuery, _, err := prepareQuery(s.Query, queryArgs, + queryCountArgs, loginId, nestingLevel+1) - *comp = fmt.Sprintf("(\n%s\n)", subQuery) - return nil + if err != nil { + return err } - // attribute filter - if s.AttributeId.Status == pgtype.Present { - *comp, err = getAttributeCode(s.AttributeId.Bytes, - getRelationCode(s.AttributeIndex, s.AttributeNested)) - - if err != nil { - return err - } + *comp = fmt.Sprintf("(\n%s\n)", subQuery) + return nil + } - // special case: (I)LIKE comparison needs attribute cast as TEXT - // this is relevant for integers/floats/etc. - if isLikeOperator(filter.Operator) { - *comp = fmt.Sprintf("%s::TEXT", *comp) - } - return nil - } + // attribute filter + if s.AttributeId.Status == pgtype.Present { + *comp, err = getAttributeCode(s.AttributeId.Bytes, + getRelationCode(s.AttributeIndex, s.AttributeNested)) - // user value filter - // can be anything, text, numbers, floats, boolean, NULL values - // create placeholders and add to query arguments - - if isNullOp { - // do not add user value as argument if NULL operator is used - // to use NULL operator the data type must be known ahead of time (prepared statement) - // "pg: could not determine data type" - // because user can add anything we would check the type ourselves - // or just check for NIL because that´s all we care about in this case - if s.Value == nil { - *comp = "NULL" - return nil - } else { - *comp = "NOT NULL" - return nil - } + if err != nil { + return err } + // special case: (I)LIKE comparison needs attribute cast as TEXT + // this is relevant for integers/floats/etc. if isLikeOperator(filter.Operator) { - // special syntax for ILIKE comparison (add wildcard characters) - s.Value = fmt.Sprintf("%%%s%%", s.Value) + *comp = fmt.Sprintf("%s::TEXT", *comp) } + return nil + } - // PGX fix: cannot use proper true/false values in SQL parameters - // no good solution found so far, error: 'cannot convert (true|false) to Text' - if fmt.Sprintf("%T", s.Value) == "bool" { - if s.Value.(bool) == true { - s.Value = "true" - } else { - s.Value = "false" - } + // user value filter + // can be anything, text, numbers, floats, boolean, NULL values + // create placeholders and add to query arguments + + if isNullOp { + // do not add user value as argument if NULL operator is used + // to use NULL operator the data type must be known ahead of time (prepared statement) + // "pg: could not determine data type" + // because user can add anything we would check the type ourselves + // or just check for NIL because that´s all we care about in this case + if s.Value == nil { + *comp = "NULL" + return nil + } else { + *comp = "NOT NULL" + return nil } + } - *queryArgs = append(*queryArgs, s.Value) - if queryCountArgs != nil { - *queryCountArgs = append(*queryCountArgs, s.Value) - } + if isLikeOperator(filter.Operator) { + // special syntax for ILIKE comparison (add wildcard characters) + s.Value = fmt.Sprintf("%%%s%%", s.Value) + } - if isArrayOperator(filter.Operator) { - *comp = fmt.Sprintf("($%d)", len(*queryArgs)) + // PGX fix: cannot use proper true/false values in SQL parameters + // no good solution found so far, error: 'cannot convert (true|false) to Text' + if fmt.Sprintf("%T", s.Value) == "bool" { + if s.Value.(bool) == true { + s.Value = "true" } else { - *comp = fmt.Sprintf("$%d", len(*queryArgs)) + s.Value = "false" } - return nil } - // build left/right comparison sides (ignore right side, if NULL operator) - comp0, comp1 := "", "" - if err := getComp(filter.Side0, &comp0); err != nil { - return err - } - if !isNullOp { - if err := getComp(filter.Side1, &comp1); err != nil { - return err - } + *queryArgs = append(*queryArgs, s.Value) + if queryCountArgs != nil { + *queryCountArgs = append(*queryCountArgs, s.Value) } - // generate WHERE line from parsed filter definition - line := fmt.Sprintf("\n%s %s%s %s %s%s", filter.Connector, - getBrackets(filter.Side0.Brackets, false), - comp0, filter.Operator, comp1, - getBrackets(filter.Side1.Brackets, true)) + if isArrayOperator(filter.Operator) { + *comp = fmt.Sprintf("($%d)", len(*queryArgs)) + } else { + *comp = fmt.Sprintf("$%d", len(*queryArgs)) + } + return nil + } - inWhere = append(inWhere, line) + // build left/right comparison sides (ignore right side, if NULL operator) + comp0, comp1 := "", "" + if err := getComp(filter.Side0, &comp0); err != nil { + return err } - if bracketBalance != 0 { - return errors.New("bracket count is unequal") + if !isNullOp { + if err := getComp(filter.Side1, &comp1); err != nil { + return err + } } - // join lines and replace first AND with WHERE - *where = strings.Replace(strings.Join(inWhere, ""), "AND", "WHERE", 1) + // generate WHERE line from parsed filter definition + line := fmt.Sprintf("\n%s %s%s %s %s%s", filter.Connector, + getBrackets(filter.Side0.Brackets, false), + comp0, filter.Operator, comp1, + getBrackets(filter.Side1.Brackets, true)) + + *inWhere = append(*inWhere, line) + return nil } -func buildOrderLimitOffset(data types.DataGet, nestingLevel int, - order *string, limit *string, offset *string) error { +func addOrderBy(data types.DataGet, nestingLevel int) (string, error) { + + if len(data.Orders) == 0 { + return "", nil + } - // build order by line orderItems := make([]string, len(data.Orders)) var codeSelect string var err error @@ -627,7 +648,7 @@ func buildOrderLimitOffset(data types.DataGet, nestingLevel int, getRelationCode(int(ord.Index.Int), nestingLevel)) if err != nil { - return err + return "", err } } @@ -635,7 +656,7 @@ func buildOrderLimitOffset(data types.DataGet, nestingLevel int, // order by chosen expression (by position in array) codeSelect = getExpressionCodeSelect(int(ord.ExpressionPos.Int)) } else { - return errors.New("unknown data GET order parameter") + return "", errors.New("unknown data GET order parameter") } if ord.Ascending == true { @@ -644,17 +665,7 @@ func buildOrderLimitOffset(data types.DataGet, nestingLevel int, orderItems[i] = fmt.Sprintf("%s DESC NULLS LAST", codeSelect) } } - - if len(orderItems) != 0 { - *order = fmt.Sprintf("\nORDER BY %s", strings.Join(orderItems, ", ")) - } - if data.Limit != 0 { - *limit = fmt.Sprintf("\nLIMIT %d", data.Limit) - } - if data.Offset != 0 { - *offset = fmt.Sprintf("\nOFFSET %d", data.Offset) - } - return nil + return fmt.Sprintf("\nORDER BY %s", strings.Join(orderItems, ", ")), nil } // helpers diff --git a/data/data_set.go b/data/data_set.go index 3bd3b25a..4ba65191 100644 --- a/data/data_set.go +++ b/data/data_set.go @@ -103,7 +103,7 @@ func Set_tx(ctx context.Context, tx pgx.Tx, dataSetsByIndex map[int]types.DataSe // set data for index if err := setForIndex_tx(ctx, tx, index, dataSetsByIndex, - indexRecordIds, indexRecordsCreated); err != nil { + indexRecordIds, indexRecordsCreated, loginId); err != nil { return indexRecordIds, err } @@ -124,7 +124,7 @@ func Set_tx(ctx context.Context, tx pgx.Tx, dataSetsByIndex map[int]types.DataSe // recursive call, if relationship tupel must be created first func setForIndex_tx(ctx context.Context, tx pgx.Tx, index int, dataSetsByIndex map[int]types.DataSet, indexRecordIds map[int]int64, - indexRecordsCreated map[int]bool) error { + indexRecordsCreated map[int]bool, loginId int64) error { if _, exists := indexRecordsCreated[index]; exists { return nil @@ -249,13 +249,24 @@ func setForIndex_tx(ctx context.Context, tx pgx.Tx, index int, } if !isNewRecord && len(values) != 0 { + // update existing record + + // get policy filter if applicable + tableAlias := "t" + policyFilter, err := getPolicyFilter(loginId, "update", tableAlias, rel.Policies) + if err != nil { + return err + } + values = append(values, dataSet.RecordId) if _, err := tx.Exec(ctx, fmt.Sprintf(` - UPDATE "%s"."%s" SET %s - WHERE id = %s - `, mod.Name, rel.Name, strings.Join(params, `, `), - fmt.Sprintf("$%d", len(values))), values...); err != nil { + UPDATE "%s"."%s" AS "%s" SET %s + WHERE "%s"."%s" = %s + %s + `, mod.Name, rel.Name, tableAlias, strings.Join(params, `, `), tableAlias, + lookups.PkName, fmt.Sprintf("$%d", len(values)), policyFilter), + values...); err != nil { return err } @@ -283,7 +294,7 @@ func setForIndex_tx(ctx context.Context, tx pgx.Tx, index int, // we must create other relation first, as we need to refer to it if err := setForIndex_tx(ctx, tx, shipIndex, dataSetsByIndex, - indexRecordIds, indexRecordsCreated); err != nil { + indexRecordIds, indexRecordsCreated, loginId); err != nil { return err } @@ -323,14 +334,15 @@ func setForIndex_tx(ctx context.Context, tx pgx.Tx, index int, if len(values) == 0 { insertQuery = fmt.Sprintf(` INSERT INTO "%s"."%s" DEFAULT VALUES - RETURNING id - `, mod.Name, rel.Name) + RETURNING "%s" + `, mod.Name, rel.Name, lookups.PkName) } else { insertQuery = fmt.Sprintf(` INSERT INTO "%s"."%s" (%s) VALUES (%s) - RETURNING id - `, mod.Name, rel.Name, strings.Join(names, `, `), strings.Join(params, `, `)) + RETURNING "%s" + `, mod.Name, rel.Name, strings.Join(names, `, `), + strings.Join(params, `, `), lookups.PkName) } if err := tx.QueryRow(ctx, insertQuery, values...).Scan(&newRecordId); err != nil { @@ -387,9 +399,10 @@ func setForIndex_tx(ctx context.Context, tx pgx.Tx, index int, if _, err := tx.Exec(ctx, fmt.Sprintf(` UPDATE "%s"."%s" SET "%s" = NULL WHERE "%s" = $1 - AND id <> ALL($2) + AND "%s" <> ALL($2) `, shipMod.Name, shipRel.Name, shipAtr.Name, - shipAtr.Name), indexRecordIds[index], shipValues.values); err != nil { + shipAtr.Name, lookups.PkName), indexRecordIds[index], + shipValues.values); err != nil { return err } @@ -397,9 +410,9 @@ func setForIndex_tx(ctx context.Context, tx pgx.Tx, index int, // add new references to this tupel if _, err := tx.Exec(ctx, fmt.Sprintf(` UPDATE "%s"."%s" SET "%s" = $1 - WHERE id = ANY($2) - `, shipMod.Name, shipRel.Name, shipAtr.Name), indexRecordIds[index], - shipValues.values); err != nil { + WHERE "%s" = ANY($2) + `, shipMod.Name, shipRel.Name, shipAtr.Name, lookups.PkName), + indexRecordIds[index], shipValues.values); err != nil { return err } diff --git a/db/upgrade/upgrade.go b/db/upgrade/upgrade.go index 4f71ed20..0a51309f 100644 --- a/db/upgrade/upgrade.go +++ b/db/upgrade/upgrade.go @@ -94,6 +94,151 @@ func oneIteration(tx pgx.Tx, dbVersionCut string) error { // mapped by current database version string, returns new database version string var upgradeFunctions = map[string]func(tx pgx.Tx) (string, error){ + "2.4": func(tx pgx.Tx) (string, error) { + _, err := tx.Exec(db.Ctx, ` + -- repo change logs + ALTER TABLE instance.repo_module ADD COLUMN change_log TEXT; + + -- relation policies + CREATE TABLE app.relation_policy ( + relation_id uuid NOT NULL, + "position" smallint NOT NULL, + role_id uuid NOT NULL, + pg_function_id_excl uuid, + pg_function_id_incl uuid, + action_delete boolean NOT NULL, + action_select boolean NOT NULL, + action_update boolean NOT NULL, + CONSTRAINT policy_pkey PRIMARY KEY (relation_id,"position"), + CONSTRAINT policy_pg_function_id_excl_fkey FOREIGN KEY (pg_function_id_excl) + REFERENCES app.pg_function (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + DEFERRABLE INITIALLY DEFERRED + NOT VALID, + CONSTRAINT policy_pg_function_id_incl_fkey FOREIGN KEY (pg_function_id_incl) + REFERENCES app.pg_function (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + DEFERRABLE INITIALLY DEFERRED + NOT VALID, + CONSTRAINT policy_relation_id_fkey FOREIGN KEY (relation_id) + REFERENCES app.relation (id) MATCH SIMPLE + ON UPDATE CASCADE + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED + NOT VALID, + CONSTRAINT policy_role_id_fkey FOREIGN KEY (role_id) + REFERENCES app.role (id) MATCH SIMPLE + ON UPDATE CASCADE + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED + NOT VALID + ); + CREATE INDEX fki_relation_policy_pg_function_id_excl_fkey + ON app.relation_policy USING btree (pg_function_id_excl ASC NULLS LAST); + CREATE INDEX fki_relation_policy_pg_function_id_incl_fkey + ON app.relation_policy USING btree (pg_function_id_incl ASC NULLS LAST); + CREATE INDEX fki_relation_policy_relation_id_fkey + ON app.relation_policy USING btree (relation_id ASC NULLS LAST); + CREATE INDEX fki_relation_policy_role_id_fkey + ON app.relation_policy USING btree (role_id ASC NULLS LAST); + + -- missing record attribute on calendar fields + ALTER TABLE app.field_calendar ADD COLUMN attribute_id_record UUID; + ALTER TABLE app.field_calendar ADD CONSTRAINT field_calendar_attribute_id_record_fkey + FOREIGN KEY (attribute_id_record) + REFERENCES app.attribute (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + + -- start forms + CREATE TABLE IF NOT EXISTS app.module_start_form( + module_id uuid NOT NULL, + "position" integer NOT NULL, + role_id uuid NOT NULL, + form_id uuid NOT NULL, + CONSTRAINT module_start_form_pkey PRIMARY KEY (module_id, "position"), + CONSTRAINT module_start_form_form_id_fkey FOREIGN KEY (form_id) + REFERENCES app.form (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT module_start_form_module_id_fkey FOREIGN KEY (module_id) + REFERENCES app.module (id) MATCH SIMPLE + ON UPDATE CASCADE + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT module_start_form_role_id_fkey FOREIGN KEY (role_id) + REFERENCES app.role (id) MATCH SIMPLE + ON UPDATE CASCADE + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED + ); + CREATE INDEX fki_module_start_form_module_id_fkey + ON app.module_start_form USING btree (module_id ASC NULLS LAST); + CREATE INDEX fki_module_start_form_role_id_fkey + ON app.module_start_form USING btree (role_id ASC NULLS LAST); + CREATE INDEX fki_module_start_form_form_id_fkey + ON app.module_start_form USING btree (form_id ASC NULLS LAST); + + -- new config + INSERT INTO instance.config (name,value) + VALUES ('builderMode','0'); + + -- new preset filter criteria + ALTER TYPE app.query_filter_side_content ADD VALUE 'preset'; + ALTER TABLE app.query_filter_side ADD COLUMN preset_id UUID; + ALTER TABLE app.query_filter_side ADD CONSTRAINT query_filter_side_preset_id_fkey + FOREIGN KEY (preset_id) + REFERENCES app.preset (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + + -- new query fixed limit + ALTER TABLE app.query ADD COLUMN fixed_limit INTEGER NOT NULL DEFAULT 0; + ALTER TABLE app.query ALTER COLUMN fixed_limit DROP DEFAULT; + + -- update log function + CREATE OR REPLACE FUNCTION instance.log( + level integer, + message text, + app_name text DEFAULT NULL::text) + RETURNS void + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE + AS $BODY$ + DECLARE + module_id UUID; + level_show INT; + BEGIN + -- check log level + SELECT value::INT INTO level_show + FROM instance.config + WHERE name = 'logApplication'; + + IF level_show < level THEN + RETURN; + END IF; + + -- resolve module ID if possible + -- if not possible: log with module_id = NULL (better than not to log) + IF app_name IS NOT NULL THEN + SELECT id INTO module_id + FROM app.module + WHERE name = app_name; + END IF; + + INSERT INTO instance.log (level,context,module_id,message,date_milli) + VALUES (level,'module',module_id,message,(EXTRACT(EPOCH FROM CLOCK_TIMESTAMP()) * 1000)::BIGINT); + END; + $BODY$; + `) + return "2.5", err + }, "2.3": func(tx pgx.Tx) (string, error) { _, err := tx.Exec(db.Ctx, ` CREATE TABLE IF NOT EXISTS app.field_chart ( @@ -278,14 +423,6 @@ var upgradeFunctions = map[string]func(tx pgx.Tx) (string, error){ }, "2.1": func(tx pgx.Tx) (string, error) { - // update configuration file - config.File.Paths.Captions = "var/texts/" - config.File.Paths.Packages = "var/packages/" - - if err := config.WriteFile(); err != nil { - return "", err - } - // replace PG function schedule positions with new IDs type schedule struct { pgFunctionId uuid.UUID diff --git a/go.mod b/go.mod index 119f1e79..1e68cd37 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,12 @@ go 1.17 require ( github.com/arran4/golang-ical v0.0.0-20210825232153-efac1f4cb8ac - github.com/emersion/go-imap v1.1.0 + github.com/emersion/go-imap v1.2.0 github.com/emersion/go-message v0.15.0 github.com/gbrlsnchs/jwt/v3 v3.0.1 github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect github.com/go-ldap/ldap/v3 v3.4.1 - github.com/gofrs/uuid v4.0.0+incompatible + github.com/gofrs/uuid v4.1.0+incompatible github.com/gorilla/websocket v1.4.2 github.com/h2non/filetype v1.1.1 github.com/jackc/pgtype v1.8.1 @@ -17,14 +17,14 @@ require ( github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/kardianos/service v1.2.0 github.com/magefile/mage v1.11.0 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect - golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68 // indirect golang.org/x/text v0.3.7 // indirect ) require ( github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect - github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect + github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect github.com/google/go-cmp v0.5.4 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect @@ -33,7 +33,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.1.1 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/puddle v1.1.3 // indirect + github.com/jackc/puddle v1.1.4 // indirect github.com/pkg/errors v0.9.1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) diff --git a/go.sum b/go.sum index 0c01158b..ad70734d 100644 --- a/go.sum +++ b/go.sum @@ -13,13 +13,13 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emersion/go-imap v1.1.0 h1:hAW8Dbi/AwiVO5Wi40FTVuCzVrTmwtEK6De9GSoOy+Y= -github.com/emersion/go-imap v1.1.0/go.mod h1:0hCeak4mA2z9hICM20jeqN6fyV0Oad0lZTyeeAyUS6o= -github.com/emersion/go-message v0.14.1/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU= +github.com/emersion/go-imap v1.2.0 h1:lyUQ3+EVM21/qbWE/4Ya5UG9r5+usDxlg4yfp3TgHFA= +github.com/emersion/go-imap v1.2.0/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= -github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs= +github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/gbrlsnchs/jwt/v3 v3.0.1 h1:lbUmgAKpxnClrKloyIwpxm4OuWeDl5wLk52G91ODPw4= @@ -32,8 +32,9 @@ github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.1.0+incompatible h1:sIa2eCvUTwgjbqXrPLfNwUf9S3i3mpH1O1atV+iL/Wk= +github.com/gofrs/uuid v4.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -88,8 +89,9 @@ github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570= github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.4 h1:5Ey/o5IfV7dYX6Znivq+N9MdK1S18OJI5OJq6EAAADw= +github.com/jackc/puddle v1.1.4/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= @@ -110,7 +112,6 @@ github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls= github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -164,8 +165,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -189,15 +190,14 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3 h1:3Ad41xy2WCESpufXwgs7NpDSu+vjxqLt2UFqUV+20bI= -golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68 h1:Ywe/f3fNleF8I6F6qv3MeFoSZ6CTf2zBMMa/7qVML8M= +golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/handler/handler.go b/handler/handler.go index cf795b09..dbcba33d 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -12,12 +12,13 @@ import ( ) var ( - ErrAuthFailed = "authentication failed" - ErrBackend = "backend error" - ErrBruteforceBlock = "blocked assumed bruteforce attempt" - ErrGeneral = "general error" - ErrPresetProtected = "preset record is protected against deletion" - ErrUnauthorized = "unauthorized" + ErrAuthFailed = "authentication failed" + ErrBackend = "backend error" + ErrBruteforceBlock = "blocked assumed bruteforce attempt" + ErrGeneral = "general error" + ErrPresetProtected = "preset record is protected against deletion" + ErrWsClientChanFull = "client channel is full, dropping response" + ErrUnauthorized = "unauthorized" ) func GetStringFromPart(part *multipart.Part) string { diff --git a/handler/websocket/websocket.go b/handler/websocket/websocket.go index 6d1effd6..5beee62f 100644 --- a/handler/websocket/websocket.go +++ b/handler/websocket/websocket.go @@ -17,58 +17,55 @@ import ( "github.com/gorilla/websocket" ) -// a specific, active websocket client +// a websocket client type clientType struct { address string // IP address, no port - admin bool // is admin? - change_mx sync.Mutex // mutex for safely changing client + admin bool // belongs to admin login? ctx context.Context // global context for client requests ctxCancel context.CancelFunc // to abort requests in case of disconnect loginId int64 // client login ID, 0 = not logged in yet noAuth bool // logged in without authentication (username only) - send chan []byte // websocket send channel - sendOpen bool // websocket send channel open? - ws *websocket.Conn // websocket connection + write_mx sync.Mutex + ws *websocket.Conn // websocket connection } // a hub for all active websocket clients -// clients can only be added/removed via the single, central hub type hubType struct { - broadcast chan []byte - clients map[*clientType]bool - add chan *clientType - remove chan *clientType + clients map[*clientType]bool + + // action channels + clientAdd chan *clientType // add client to hub + clientDel chan *clientType // delete client from hub } var ( - clientUpgrader = websocket.Upgrader{} + clientUpgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024} + handlerContext = "websocket" - hub_mx sync.Mutex // mutex for safely changing websocket hub hub = hubType{ clients: make(map[*clientType]bool), - add: make(chan *clientType), - remove: make(chan *clientType), - broadcast: make(chan []byte), + clientAdd: make(chan *clientType), + clientDel: make(chan *clientType), } ) func StartBackgroundTasks() { go hub.start() - go handleClientEvents() } -// handles websocket client func Handler(w http.ResponseWriter, r *http.Request) { // bruteforce check must occur before websocket connection is established - // otherwise the HTTP writer is hijacked + // otherwise the HTTP writer is not usable (hijacked for websocket) if blocked := bruteforce.Check(r); blocked { handler.AbortRequestNoLog(w, handler.ErrBruteforceBlock) return } - conn, err := clientUpgrader.Upgrade(w, r, nil) + ws, err := clientUpgrader.Upgrade(w, r, nil) if err != nil { handler.AbortRequest(w, handlerContext, err, handler.ErrGeneral) return @@ -78,6 +75,7 @@ func Handler(w http.ResponseWriter, r *http.Request) { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { handler.AbortRequest(w, handlerContext, err, handler.ErrGeneral) + ws.Close() return } @@ -89,51 +87,85 @@ func Handler(w http.ResponseWriter, r *http.Request) { client := &clientType{ address: host, admin: false, - change_mx: sync.Mutex{}, ctx: ctx, ctxCancel: ctxCancel, loginId: 0, noAuth: false, - send: make(chan []byte), - sendOpen: true, - ws: conn, + write_mx: sync.Mutex{}, + ws: ws, } - hub.add <- client + hub.clientAdd <- client - go client.write() go client.read() } func (hub *hubType) start() { + + var removeClient = func(client *clientType) { + if _, exists := hub.clients[client]; exists { + log.Info("server", fmt.Sprintf("disconnecting client at %s", client.address)) + client.ws.WriteMessage(websocket.CloseMessage, []byte{}) // optional + client.ws.Close() + client.ctxCancel() + delete(hub.clients, client) + } + } + for { + // hub is only handled here, no locking is required select { - case client := <-hub.add: - hub_mx.Lock() + case client := <-hub.clientAdd: hub.clients[client] = true - hub_mx.Unlock() - - case client := <-hub.remove: - hub_mx.Lock() - if _, exists := hub.clients[client]; exists { - client.change_mx.Lock() - log.Info("server", fmt.Sprintf("disconnecting client at %s", client.address)) - delete(hub.clients, client) - close(client.send) - client.ctxCancel() - client.sendOpen = false - client.ws.Close() - client.change_mx.Unlock() + + case client := <-hub.clientDel: + removeClient(client) + + case event := <-cache.ClientEvent_handlerChan: + + jsonMsg := []byte{} // message back to client + kickEvent := event.Kick || event.KickNonAdmin + + if !kickEvent { + // if clients are not kicked, prepare response + var err error + + if event.BuilderOff || event.BuilderOn { + jsonMsg, err = prepareUnrequested("builder_mode_changed", event.BuilderOn) + } + if event.Renew { + jsonMsg, err = prepareUnrequested("reauthorized", nil) + } + if event.SchemaLoading { + jsonMsg, err = prepareUnrequested("schema_loading", nil) + } + if event.SchemaTimestamp != 0 { + jsonMsg, err = prepareUnrequested("schema_loaded", event.SchemaTimestamp) + } + if err != nil { + log.Error("server", "could not prepare unrequested transaction", err) + continue + } } - hub_mx.Unlock() - - case message := <-hub.broadcast: - for client := range hub.clients { - select { - case client.send <- message: - default: // client send channel is full - log.Warning("server", "websocket", fmt.Errorf("client channel is full")) - hub.remove <- client + + for client, _ := range hub.clients { + + // login ID 0 affects all + if event.LoginId != 0 && event.LoginId != client.loginId { + continue + } + + // non-kick event, send message + if !kickEvent { + go client.write(jsonMsg) + } + + // kick client, if requested + if event.Kick || (event.KickNonAdmin && !client.admin) { + log.Info("server", fmt.Sprintf("kicking client (login ID %d)", + client.loginId)) + + removeClient(client) } } } @@ -144,66 +176,25 @@ func (client *clientType) read() { for { _, message, err := client.ws.ReadMessage() if err != nil { - hub.remove <- client + hub.clientDel <- client return } - // do not wait for result to allow parallel requests - // useful for new requests after aborting long running requests + // do not wait for response to allow for parallel requests go func() { - result := client.handleTransaction(message) - - client.change_mx.Lock() - if client.sendOpen { - client.send <- result - } - client.change_mx.Unlock() + client.write(client.handleTransaction(message)) }() } } -func (client *clientType) write() { - for { - select { - case message, open := <-client.send: - if !open { - client.ws.WriteMessage(websocket.CloseMessage, []byte{}) - return - } - if err := client.ws.WriteMessage(websocket.TextMessage, message); err != nil { - hub.remove <- client - return - } - } - } -} - -func (client *clientType) sendUnrequested(ressource string, payload interface{}) { +func (client *clientType) write(message []byte) { + client.write_mx.Lock() + defer client.write_mx.Unlock() - var resTrans types.UnreqResponseTransaction - resTrans.TransactionNr = 0 // transaction was not requested - - payloadJson, err := json.Marshal(payload) - if err != nil { + if err := client.ws.WriteMessage(websocket.TextMessage, message); err != nil { + hub.clientDel <- client return } - - resTrans.Responses = make([]types.UnreqResponse, 1) - resTrans.Responses[0].Payload = payloadJson - resTrans.Responses[0].Ressource = ressource - resTrans.Responses[0].Result = "OK" - - transJson, err := json.Marshal(resTrans) - if err != nil { - return - } - - client.change_mx.Lock() - defer client.change_mx.Unlock() - - if client.sendOpen { - client.send <- []byte(transJson) - } } func (client *clientType) handleTransaction(reqTransJson json.RawMessage) json.RawMessage { @@ -239,7 +230,7 @@ func (client *clientType) handleTransaction(reqTransJson json.RawMessage) json.R resTrans.Responses = make([]types.Response, 0) if blocked := bruteforce.CheckByHost(client.address); blocked { - hub.remove <- client + hub.clientDel <- client return []byte("{}") } @@ -287,46 +278,24 @@ func (client *clientType) handleTransaction(reqTransJson json.RawMessage) json.R return resTransJson } -// client events from outside the websocket handler -func handleClientEvents() { - for { - event := <-cache.ClientEvent_handlerChan +func prepareUnrequested(ressource string, payload interface{}) ([]byte, error) { - for client, _ := range hub.clients { - - // login ID 0 affects all - if event.LoginId != 0 && event.LoginId != client.loginId { - continue - } - - // ask client to renew authorization cache - if event.Renew { - client.sendUnrequested("reauthorized", nil) - } - - // kick client - if event.Kick { - log.Info("server", fmt.Sprintf("kicking client (login ID %d)", - client.loginId)) - - hub.remove <- client - } + var resTrans types.UnreqResponseTransaction + resTrans.TransactionNr = 0 // transaction was not requested - // kick non-admin - if event.KickNonAdmin && !client.admin { - log.Info("server", fmt.Sprintf("kicking non-admin client (login ID %d)", - client.loginId)) + payloadJson, err := json.Marshal(payload) + if err != nil { + return []byte{}, err + } - hub.remove <- client - } + resTrans.Responses = make([]types.UnreqResponse, 1) + resTrans.Responses[0].Payload = payloadJson + resTrans.Responses[0].Ressource = ressource + resTrans.Responses[0].Result = "OK" - // inform clients about schema events - if event.SchemaLoading { - client.sendUnrequested("schema_loading", nil) - } - if event.SchemaTimestamp != 0 { - client.sendUnrequested("schema_loaded", event.SchemaTimestamp) - } - } + transJson, err := json.Marshal(resTrans) + if err != nil { + return []byte{}, err } + return transJson, nil } diff --git a/r3.go b/r3.go index 964be22d..216f6e33 100644 --- a/r3.go +++ b/r3.go @@ -3,8 +3,10 @@ package main import ( "bufio" "context" + "embed" "flag" "fmt" + "io/fs" "net" "net/http" "os" @@ -48,6 +50,10 @@ var ( appName string = "REI3" appNameShort string = "R3" appVersion string = "0.1.2.3" + + // embed static web files + //go:embed www/* + fsStatic embed.FS ) type cliInput struct { @@ -63,6 +69,7 @@ type cliInput struct { serviceInstall bool serviceUninstall bool setData string + wwwPath string } type program struct { cli cliInput @@ -80,18 +87,19 @@ func main() { // process configuration overwrites from command line var cli cliInput - flag.StringVar(&cli.serviceName, "servicename", appName, "Specify name of service to manage (to (un)install, start or stop service)") - flag.BoolVar(&cli.serviceInstall, "install", false, fmt.Sprintf("Install %s service", appName)) - flag.BoolVar(&cli.serviceUninstall, "uninstall", false, fmt.Sprintf("Uninstall %s service", appName)) - flag.BoolVar(&cli.serviceStart, "start", false, fmt.Sprintf("Start %s service", appName)) - flag.BoolVar(&cli.serviceStop, "stop", false, fmt.Sprintf("Stop %s service", appName)) - flag.BoolVar(&cli.open, "open", false, fmt.Sprintf("Open URL of %s in default browser (combined with -run)", appName)) + flag.StringVar(&cli.adminCreate, "newadmin", "", "Create new admin user (username:password), password must not contain spaces or colons") + flag.StringVar(&cli.configFile, "config", "config.json", "Location of configuration file (combined with -run)") flag.BoolVar(&cli.dynamicPort, "dynamicport", false, "Start with a port provided by the operating system (combined with -run)") flag.BoolVar(&cli.http, "http", false, "Start with HTTP (not encrypted, for testing/development only, combined with -run)") + flag.BoolVar(&cli.open, "open", false, fmt.Sprintf("Open URL of %s in default browser (combined with -run)", appName)) flag.BoolVar(&cli.run, "run", false, fmt.Sprintf("Run %s from within this console (see 'config.json' for configuration)", appName)) - flag.StringVar(&cli.adminCreate, "newadmin", "", "Create new admin user (username:password), password must not contain spaces or colons") + flag.BoolVar(&cli.serviceInstall, "install", false, fmt.Sprintf("Install %s service", appName)) + flag.StringVar(&cli.serviceName, "servicename", appName, "Specify name of service to manage (to (un)install, start or stop service)") + flag.BoolVar(&cli.serviceStart, "start", false, fmt.Sprintf("Start %s service", appName)) + flag.BoolVar(&cli.serviceStop, "stop", false, fmt.Sprintf("Stop %s service", appName)) + flag.BoolVar(&cli.serviceUninstall, "uninstall", false, fmt.Sprintf("Uninstall %s service", appName)) flag.StringVar(&cli.setData, "setdata", "", "Write to config file: Data directory (platform files and database if stand-alone)") - flag.StringVar(&cli.configFile, "config", "", "Start with alternative config file location (combined with -run)") + flag.StringVar(&cli.wwwPath, "wwwpath", "", "(Development) Use web files from given path instead of embedded ones") flag.Parse() // define service and service logger @@ -159,12 +167,9 @@ func main() { return } - // change configuration file location - if cli.configFile != "" { - config.SetConfigFilePath(cli.configFile) - } - // load configuration from file + config.SetConfigFilePath(cli.configFile) + if err := config.LoadFile(); err != nil { prg.logger.Errorf("failed to read configuration file, %v", err) return @@ -362,12 +367,6 @@ func (prg *program) execute(svc service.Service) { return } - // load captions into memory for regular delivery - if err := config.InitAppCaptions(); err != nil { - prg.logger.Errorf("failed to read captions into memory, %v", err) - return - } - log.Info("server", fmt.Sprintf("is ready to start application (%s)", appVersion)) // apply configuration parameters @@ -384,7 +383,18 @@ func (prg *program) execute(svc service.Service) { go websocket.StartBackgroundTasks() mux := http.NewServeMux() - mux.Handle("/", http.FileServer(http.Dir(config.File.Paths.Web))) + + if prg.cli.wwwPath == "" { + fsStaticWww, err := fs.Sub(fs.FS(fsStatic), "www") + if err != nil { + prg.logger.Errorf("failed to access embedded web file directory, %v", err) + return + } + mux.Handle("/", http.FileServer(http.FS(fsStaticWww))) + } else { + mux.Handle("/", http.FileServer(http.Dir(prg.cli.wwwPath))) + } + mux.HandleFunc("/cache/download/", cache_download.Handler) mux.HandleFunc("/csv/download/", csv_download.Handler) mux.HandleFunc("/csv/upload", csv_upload.Handler) diff --git a/repo/repo_get.go b/repo/repo_get.go index 1938d546..fac1da11 100644 --- a/repo/repo_get.go +++ b/repo/repo_get.go @@ -17,9 +17,9 @@ func GetModule(byString string, languageCode string, limit int, var qb tools.QueryBuilder qb.UseDollarSigns() - qb.AddList("SELECT", []string{"rm.module_id_wofk", "rm.name", "rm.author", - "rm.in_store", "rm.release_build", "rm.release_build_app", - "rm.release_date", "rm.file"}) + qb.AddList("SELECT", []string{"rm.module_id_wofk", "rm.name", + "rm.change_log", "rm.author", "rm.in_store", "rm.release_build", + "rm.release_build_app", "rm.release_date", "rm.file"}) qb.Set("FROM", "instance.repo_module AS rm") @@ -95,8 +95,8 @@ func GetModule(byString string, languageCode string, limit int, for rows.Next() { var rm types.RepoModule - if err := rows.Scan(&rm.ModuleId, &rm.Name, &rm.Author, &rm.InStore, - &rm.ReleaseBuild, &rm.ReleaseBuildApp, &rm.ReleaseDate, + if err := rows.Scan(&rm.ModuleId, &rm.Name, &rm.ChangeLog, &rm.Author, + &rm.InStore, &rm.ReleaseBuild, &rm.ReleaseBuildApp, &rm.ReleaseDate, &rm.FileId); err != nil { return repoModules, 0, err diff --git a/repo/repo_update.go b/repo/repo_update.go index 3199cbff..591b9137 100644 --- a/repo/repo_update.go +++ b/repo/repo_update.go @@ -15,7 +15,7 @@ import ( author 49c10371-c3ee-4d42-8961-d6d8ccda7bc7 author.name 295f5bd9-772a-41f0-aa81-530a0678e441 -language 820de67e-ee99-44f9-a37a-4a7d3ac7301c +language 820de67e-ee99-44f9-a37a-4a7d3ac7301c language.code 19bd7a3b-9b3d-45da-9c07-4d8f62874b35 module 08dfb28b-dbb4-4b70-8231-142235516385 @@ -23,12 +23,13 @@ module.name fbab278a-4898-4f46-a1d7-35d1a80ee3dc module.uuid 98bc635b-097e-4cf0-92c9-2bb97a7c2a5e module.in_store 0ba7005c-834b-4d2b-a967-d748f91c2bed module.author a72f2de6-e1ee-4432-804b-b57f44013f4c +module.log_summary f36130a9-bfed-42dc-920f-036ffd0d35b0 module_release a300afae-a8c5-4cfc-9375-d85f45c6347c module_release.file b28e8f5c-ebeb-4565-941b-4d942eedc588 module_release.module 922dc949-873f-4a21-9699-8740c0491b3a -module_release.release_build d0766fcc-7a68-490c-9c81-f542ad37109b -module_release.release_build_app ce998cfd-a66f-423c-b82b-d2b48a21c288 +module_release.release_build d0766fcc-7a68-490c-9c81-f542ad37109b +module_release.release_build_app ce998cfd-a66f-423c-b82b-d2b48a21c288 module_release.release_date 9f9b6cda-069d-405b-bbb8-c0d12bbce910 module_transl_meta 12ae386b-d1d2-48b2-a60b-2d5a11c42826 @@ -36,7 +37,7 @@ module_transl_meta.description 3cd8b8b1-3d3f-41b0-ba6c-d7ef567a686f module_transl_meta.language 8aa84747-8224-4f8d-baf1-2d87df374fe6 module_transl_meta.module 1091d013-988c-442b-beff-c853e8df20a8 module_transl_meta.support_page 4793cd87-0bc9-4797-9538-ca733007a1d1 -module_transl_meta.title 6f66272a-7713-45a8-9565-b0157939399b +module_transl_meta.title 6f66272a-7713-45a8-9565-b0157939399b */ // update internal module repository from external data API @@ -61,13 +62,13 @@ func Update() error { // get modules, their latest releases and translated module meta data if err := getModules(token, dataAccessUrl, skipVerify, repoModuleMap); err != nil { - return err + return fmt.Errorf("failed to get modules, %w", err) } if err := getModuleReleases(token, dataAccessUrl, skipVerify, repoModuleMap, lastRun); err != nil { - return err + return fmt.Errorf("failed to get module releases, %w", err) } if err := getModuleMetas(token, dataAccessUrl, skipVerify, repoModuleMap); err != nil { - return err + return fmt.Errorf("failed to get meta info for modules, %w", err) } // apply changes to local module store @@ -78,10 +79,10 @@ func Update() error { defer tx.Rollback(db.Ctx) if err := removeModules_tx(tx, repoModuleMap); err != nil { - return err + return fmt.Errorf("failed to remove modules, %w", err) } if err := addModules_tx(tx, repoModuleMap); err != nil { - return err + return fmt.Errorf("failed to add modules, %w", err) } if err := config.SetUint64_tx(tx, "repoChecked", thisRun); err != nil { return err @@ -109,12 +110,13 @@ func addModules_tx(tx pgx.Tx, repoModuleMap map[uuid.UUID]types.RepoModule) erro if !exists { if _, err := tx.Exec(db.Ctx, ` INSERT INTO instance.repo_module ( - module_id_wofk, name, author, in_store, release_build, - release_build_app, release_date, file + module_id_wofk, name, change_log, author, in_store, + release_build, release_build_app, release_date, file ) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8) - `, sm.ModuleId, sm.Name, sm.Author, sm.InStore, sm.ReleaseBuild, - sm.ReleaseBuildApp, sm.ReleaseDate, sm.FileId); err != nil { + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) + `, sm.ModuleId, sm.Name, sm.ChangeLog, sm.Author, sm.InStore, + sm.ReleaseBuild, sm.ReleaseBuildApp, sm.ReleaseDate, + sm.FileId); err != nil { return err } @@ -123,21 +125,23 @@ func addModules_tx(tx pgx.Tx, repoModuleMap map[uuid.UUID]types.RepoModule) erro if sm.ReleaseBuild == 0 { if _, err := tx.Exec(db.Ctx, ` UPDATE instance.repo_module - SET name = $1, author = $2, in_store = $3 - WHERE module_id_wofk = $4 - `, sm.Name, sm.Author, sm.InStore, sm.ModuleId); err != nil { + SET name = $1, change_log = $2, author = $3, in_store = $4 + WHERE module_id_wofk = $5 + `, sm.Name, sm.ChangeLog, sm.Author, sm.InStore, + sm.ModuleId); err != nil { + return err } } else { if _, err := tx.Exec(db.Ctx, ` UPDATE instance.repo_module - SET name = $1, author = $2, in_store = $3, - release_build = $4, release_build_app = $5, - release_date = $6, file = $7 - WHERE module_id_wofk = $8 - `, sm.Name, sm.Author, sm.InStore, sm.ReleaseBuild, - sm.ReleaseBuildApp, sm.ReleaseDate, sm.FileId, - sm.ModuleId); err != nil { + SET name = $1, change_log = $2, author = $3, in_store = $4, + release_build = $5, release_build_app = $6, + release_date = $7, file = $8 + WHERE module_id_wofk = $9 + `, sm.Name, sm.ChangeLog, sm.Author, sm.InStore, + sm.ReleaseBuild, sm.ReleaseBuildApp, sm.ReleaseDate, + sm.FileId, sm.ModuleId); err != nil { return err } diff --git a/repo/repo_update_modules.go b/repo/repo_update_modules.go index a73e8465..f607d2b3 100644 --- a/repo/repo_update_modules.go +++ b/repo/repo_update_modules.go @@ -41,6 +41,12 @@ func getModules(token string, url string, skipVerify bool, Aggregator: pgtype.Varchar{Status: pgtype.Null}, Index: 0, }, + types.DataGetExpression{ // module change log + AttributeId: tools.UuidStringToNullUuid("f36130a9-bfed-42dc-920f-036ffd0d35b0"), + AttributeIdNm: pgtype.UUID{Status: pgtype.Null}, + Aggregator: pgtype.Varchar{Status: pgtype.Null}, + Index: 0, + }, types.DataGetExpression{ // author name AttributeId: tools.UuidStringToNullUuid("295f5bd9-772a-41f0-aa81-530a0678e441"), AttributeIdNm: pgtype.UUID{Status: pgtype.Null}, @@ -68,7 +74,7 @@ func getModules(token string, url string, skipVerify bool, } for _, row := range res.Rows { - if len(row.Values) != 4 { + if len(row.Values) != 5 { return errors.New("invalid value count for store module") } @@ -83,6 +89,16 @@ func getModules(token string, url string, skipVerify bool, case 2: repo.InStore = value.(bool) case 3: + repo.ChangeLog = pgtype.Varchar{ + Status: pgtype.Null, + } + if value != nil { + repo.ChangeLog = pgtype.Varchar{ + Status: pgtype.Present, + String: value.(string), + } + } + case 4: repo.Author = value.(string) } } diff --git a/repo/repo_update_releases.go b/repo/repo_update_releases.go index f80347cc..b5327871 100644 --- a/repo/repo_update_releases.go +++ b/repo/repo_update_releases.go @@ -124,7 +124,7 @@ func getModuleReleases(token string, url string, skipVerify bool, // add only first release per module (are sorted descending by build) if tools.UuidInSlice(moduleId, moduleIdsAdded) { - continue + break } if _, exists := repoModuleMap[moduleId]; !exists { @@ -167,7 +167,11 @@ func getModuleReleases(token string, url string, skipVerify bool, moduleIdsAdded = append(moduleIdsAdded, repoModule.ModuleId) } } - repoModuleMap[repoModule.ModuleId] = repoModule + + // only the latest release is used, module ID is not set for subsequent ones + if repoModule.ModuleId != uuid.Nil { + repoModuleMap[repoModule.ModuleId] = repoModule + } } return nil } diff --git a/request/request.go b/request/request.go index 491ce523..05af932e 100644 --- a/request/request.go +++ b/request/request.go @@ -305,6 +305,9 @@ func Exec_tx(ctx context.Context, tx pgx.Tx, loginId int64, isAdmin bool, isNoAu return LoginGetMembers(reqJson) case "getRecords": return LoginGetRecords(reqJson) + case "informBuilderState": + cache.ChangedBuilderMode(config.GetUint64("builderMode") == 1) + return nil, nil case "kick": return LoginKick(reqJson) case "kickNonAdmins": diff --git a/request/request_lookups.go b/request/request_lookups.go index 280791cb..84564922 100644 --- a/request/request_lookups.go +++ b/request/request_lookups.go @@ -32,15 +32,7 @@ func LookupGet(reqJson json.RawMessage, loginId int64) (interface{}, error) { `, loginId).Scan(&languageCode); err != nil { return nil, err } - - // overwrite non-valid system language code - languageCode = config.GetLanguageCodeValid(languageCode) - - res, err := config.GetAppCaptions(languageCode) - if err != nil { - return nil, err - } - return res, nil + return cache.GetCaptions(languageCode), nil case "customizing": var res struct { diff --git a/request/request_module.go b/request/request_module.go index f73b7430..679af539 100644 --- a/request/request_module.go +++ b/request/request_module.go @@ -73,8 +73,8 @@ func ModuleSet_tx(tx pgx.Tx, reqJson json.RawMessage) (interface{}, error) { } if err := module.Set_tx(tx, req.Id, req.ParentId, req.FormId, req.IconId, req.Name, req.Color1, req.Position, req.LanguageMain, req.ReleaseBuild, - req.ReleaseBuildApp, req.ReleaseDate, req.DependsOn, req.Languages, - req.Captions); err != nil { + req.ReleaseBuildApp, req.ReleaseDate, req.DependsOn, req.StartForms, + req.Languages, req.Captions); err != nil { return nil, err } diff --git a/request/request_package.go b/request/request_package.go index 490941a3..e4c764ae 100644 --- a/request/request_package.go +++ b/request/request_package.go @@ -1,12 +1,24 @@ package request import ( - "path/filepath" + "io/ioutil" + "r3/cache" "r3/config" + "r3/tools" "r3/transfer" ) func PackageInstall() (interface{}, error) { - return nil, transfer.ImportFromFiles( - []string{filepath.Join(config.File.Paths.Packages, "core_company.rei3")}) + + // store package file from embedded binary data to temp folder + filePath, err := tools.GetUniqueFilePath(config.File.Paths.Temp, 8999999, 9999999) + if err != nil { + return nil, err + } + + if err := ioutil.WriteFile(filePath, cache.Package_CoreCompany, 0644); err != nil { + return nil, err + } + + return nil, transfer.ImportFromFiles([]string{filePath}) } diff --git a/request/request_public.go b/request/request_public.go index fb70095c..b206fce7 100644 --- a/request/request_public.go +++ b/request/request_public.go @@ -7,31 +7,33 @@ import ( func PublicGet() (interface{}, error) { var res struct { - Activated bool `json:"activated"` - AppName string `json:"appName"` - AppNameShort string `json:"appNameShort"` - AppVersion string `json:"appVersion"` - Builder bool `json:"builder"` - CompanyColorHeader string `json:"companyColorHeader"` - CompanyColorLogin string `json:"companyColorLogin"` - CompanyLogo string `json:"companyLogo"` - CompanyLogoUrl string `json:"companyLogoUrl"` - CompanyName string `json:"companyName"` - CompanyWelcome string `json:"companyWelcome"` - ProductionMode uint64 `json:"productionMode"` - SchemaTimestamp int64 `json:"schemaTimestamp"` + Activated bool `json:"activated"` + AppName string `json:"appName"` + AppNameShort string `json:"appNameShort"` + AppVersion string `json:"appVersion"` + Builder bool `json:"builder"` + CompanyColorHeader string `json:"companyColorHeader"` + CompanyColorLogin string `json:"companyColorLogin"` + CompanyLogo string `json:"companyLogo"` + CompanyLogoUrl string `json:"companyLogoUrl"` + CompanyName string `json:"companyName"` + CompanyWelcome string `json:"companyWelcome"` + LanguageCodes []string `json:"languageCodes"` + ProductionMode uint64 `json:"productionMode"` + SchemaTimestamp int64 `json:"schemaTimestamp"` } res.Activated = config.GetLicenseActive() res.AppName = config.GetString("appName") res.AppNameShort = config.GetString("appNameShort") res.AppVersion, _, _, _ = config.GetAppVersions() - res.Builder = config.File.Builder + res.Builder = config.GetUint64("builderMode") == 1 res.CompanyColorHeader = config.GetString("companyColorHeader") res.CompanyColorLogin = config.GetString("companyColorLogin") res.CompanyLogo = config.GetString("companyLogo") res.CompanyLogoUrl = config.GetString("companyLogoUrl") res.CompanyName = config.GetString("companyName") res.CompanyWelcome = config.GetString("companyWelcome") + res.LanguageCodes = cache.GetCaptionLanguageCodes() res.ProductionMode = config.GetUint64("productionMode") res.SchemaTimestamp = cache.GetSchemaTimestamp() return res, nil diff --git a/request/request_relation.go b/request/request_relation.go index f8b1acce..55069c9f 100644 --- a/request/request_relation.go +++ b/request/request_relation.go @@ -49,7 +49,7 @@ func RelationSet_tx(tx pgx.Tx, reqJson json.RawMessage) (interface{}, error) { return nil, err } return nil, relation.Set_tx(tx, req.ModuleId, req.Id, req.Name, - req.RetentionCount, req.RetentionDays) + req.RetentionCount, req.RetentionDays, req.Policies) } func RelationPreview(reqJson json.RawMessage) (interface{}, error) { diff --git a/schema/field/field.go b/schema/field/field.go index 3bbf0196..2f25f777 100644 --- a/schema/field/field.go +++ b/schema/field/field.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/json" "errors" + "r3/compatible" "r3/db" "r3/schema" "r3/schema/caption" @@ -37,9 +38,9 @@ func Get(formId uuid.UUID) ([]interface{}, error) { -- calendar field fn.form_id_open, fn.attribute_id_date0, fn.attribute_id_date1, - fn.attribute_id_color, fn.index_date0, fn.index_date1, fn.index_color, - fn.ics, fn.gantt, fn.gantt_steps, fn.gantt_steps_toggle, fn.date_range0, - fn.date_range1, + fn.attribute_id_color, fn.attribute_id_record, fn.index_date0, + fn.index_date1, fn.index_color, fn.ics, fn.gantt, fn.gantt_steps, + fn.gantt_steps_toggle, fn.date_range0, fn.date_range1, -- chart field fa.chart_option, @@ -111,9 +112,9 @@ func Get(formId uuid.UUID) ([]interface{}, error) { var autoRenew, dateRange0, dateRange1, indexColor, min, max pgtype.Int4 var attributeId, attributeIdAlt, attributeIdNm, attributeIdDate0, attributeIdDate1, attributeIdColor, attributeIdRecordBtn, - attributeIdRecordData, attributeIdRecordList, fieldParentId, - formIdOpenBtn, formIdOpenCal, formIdOpenData, formIdOpenList, - iconId pgtype.UUID + attributeIdRecordCalendar, attributeIdRecordData, + attributeIdRecordList, fieldParentId, formIdOpenBtn, formIdOpenCal, + formIdOpenData, formIdOpenList, iconId pgtype.UUID var category, csvExport, csvImport, filterQuick, filterQuickList, gantt, ganttStepsToggle, ics, outsideIn, wrap pgtype.Bool var defPresetIds []uuid.UUID @@ -121,15 +122,16 @@ func Get(formId uuid.UUID) ([]interface{}, error) { if err := rows.Scan(&fieldId, &fieldParentId, &iconId, &content, &state, &onMobile, &atrContent, &attributeIdRecordBtn, &formIdOpenBtn, &formIdOpenCal, &attributeIdDate0, &attributeIdDate1, - &attributeIdColor, &indexDate0, &indexDate1, &indexColor, - &ics, &gantt, &ganttSteps, &ganttStepsToggle, &dateRange0, - &dateRange1, &chartOption, &direction, &justifyContent, &alignItems, - &alignContent, &wrap, &grow, &shrink, &basis, &perMin, &perMax, - &size, &attributeId, &attributeIdAlt, &index, &display, &min, &max, - &def, ®exCheck, &formIdOpenData, &attributeIdRecordData, - &attributeIdNm, &category, &filterQuick, &outsideIn, &autoSelect, - &defPresetIds, &attributeIdRecordList, &formIdOpenList, &autoRenew, - &csvExport, &csvImport, &layout, &filterQuickList, &resultLimit); err != nil { + &attributeIdColor, &attributeIdRecordCalendar, &indexDate0, + &indexDate1, &indexColor, &ics, &gantt, &ganttSteps, + &ganttStepsToggle, &dateRange0, &dateRange1, &chartOption, + &direction, &justifyContent, &alignItems, &alignContent, &wrap, + &grow, &shrink, &basis, &perMin, &perMax, &size, &attributeId, + &attributeIdAlt, &index, &display, &min, &max, &def, ®exCheck, + &formIdOpenData, &attributeIdRecordData, &attributeIdNm, &category, + &filterQuick, &outsideIn, &autoSelect, &defPresetIds, + &attributeIdRecordList, &formIdOpenList, &autoRenew, &csvExport, + &csvImport, &layout, &filterQuickList, &resultLimit); err != nil { rows.Close() return fields, err @@ -154,26 +156,27 @@ func Get(formId uuid.UUID) ([]interface{}, error) { posButtonLookup = append(posButtonLookup, pos) case "calendar": fields = append(fields, types.FieldCalendar{ - Id: fieldId, - IconId: iconId, - Content: content, - State: state, - OnMobile: onMobile, - FormIdOpen: formIdOpenCal, - AttributeIdDate0: attributeIdDate0.Bytes, - AttributeIdDate1: attributeIdDate1.Bytes, - AttributeIdColor: attributeIdColor, - IndexDate0: int(indexDate0.Int), - IndexDate1: int(indexDate1.Int), - IndexColor: indexColor, - Ics: ics.Bool, - Gantt: gantt.Bool, - GanttSteps: ganttSteps, - GanttStepsToggle: ganttStepsToggle.Bool, - DateRange0: int64(dateRange0.Int), - DateRange1: int64(dateRange1.Int), - Columns: []types.Column{}, - Query: types.Query{}, + Id: fieldId, + IconId: iconId, + Content: content, + State: state, + OnMobile: onMobile, + FormIdOpen: formIdOpenCal, + AttributeIdDate0: attributeIdDate0.Bytes, + AttributeIdDate1: attributeIdDate1.Bytes, + AttributeIdColor: attributeIdColor, + AttributeIdRecord: attributeIdRecordCalendar, + IndexDate0: int(indexDate0.Int), + IndexDate1: int(indexDate1.Int), + IndexColor: indexColor, + Ics: ics.Bool, + Gantt: gantt.Bool, + GanttSteps: ganttSteps, + GanttStepsToggle: ganttStepsToggle.Bool, + DateRange0: int64(dateRange0.Int), + DateRange1: int64(dateRange1.Int), + Columns: []types.Column{}, + Query: types.Query{}, }) posCalendarLookup = append(posCalendarLookup, pos) case "chart": @@ -218,10 +221,10 @@ func Get(formId uuid.UUID) ([]interface{}, error) { State: state, OnMobile: onMobile, FormIdOpen: formIdOpenData, - AttributeIdRecord: attributeIdRecordData, AttributeId: attributeId.Bytes, AttributeIdAlt: attributeIdAlt, AttributeIdNm: attributeIdNm, + AttributeIdRecord: attributeIdRecordData, Index: int(index.Int), Display: display.String, AutoSelect: int(autoSelect.Int), @@ -522,9 +525,9 @@ func Set_tx(tx pgx.Tx, formId uuid.UUID, parentId pgtype.UUID, } if err := setCalendar_tx(tx, fieldId, f.FormIdOpen, f.AttributeIdDate0, f.AttributeIdDate1, f.AttributeIdColor, - f.IndexDate0, f.IndexDate1, f.IndexColor, f.Gantt, f.GanttSteps, - f.GanttStepsToggle, f.Ics, f.DateRange0, f.DateRange1, - f.Columns); err != nil { + f.AttributeIdRecord, f.IndexDate0, f.IndexDate1, f.IndexColor, + f.Gantt, f.GanttSteps, f.GanttStepsToggle, f.Ics, f.DateRange0, + f.DateRange1, f.Columns); err != nil { return err } @@ -682,8 +685,8 @@ func setButton_tx(tx pgx.Tx, fieldId uuid.UUID, attributeIdRecord pgtype.UUID, } func setCalendar_tx(tx pgx.Tx, fieldId uuid.UUID, formIdOpen pgtype.UUID, attributeIdDate0 uuid.UUID, attributeIdDate1 uuid.UUID, - attributeIdColor pgtype.UUID, indexDate0 int, indexDate1 int, - indexColor pgtype.Int4, gantt bool, ganttSteps pgtype.Varchar, + attributeIdColor pgtype.UUID, attributeIdRecord pgtype.UUID, indexDate0 int, + indexDate1 int, indexColor pgtype.Int4, gantt bool, ganttSteps pgtype.Varchar, ganttStepsToggle bool, ics bool, dateRange0 int64, dateRange1 int64, columns []types.Column) error { @@ -692,18 +695,23 @@ func setCalendar_tx(tx pgx.Tx, fieldId uuid.UUID, formIdOpen pgtype.UUID, return err } + // fix imports < 2.5: New optional record attribute + attributeIdRecord = compatible.FixPgxNull(attributeIdRecord).(pgtype.UUID) + if known { if _, err := tx.Exec(db.Ctx, ` UPDATE app.field_calendar SET form_id_open = $1, attribute_id_date0 = $2, - attribute_id_date1 = $3, attribute_id_color = $4, - index_date0 = $5, index_date1 = $6, index_color = $7, - gantt = $8, gantt_steps = $9, gantt_steps_toggle = $10, - ics = $11, date_range0 = $12, date_range1 = $13 - WHERE field_id = $14 + attribute_id_date1 = $3, attribute_id_color = $4, + attribute_id_record = $5, index_date0 = $6, index_date1 = $7, + index_color = $8, gantt = $9, gantt_steps = $10, + gantt_steps_toggle = $11, ics = $12, date_range0 = $13, + date_range1 = $14 + WHERE field_id = $15 `, formIdOpen, attributeIdDate0, attributeIdDate1, attributeIdColor, - indexDate0, indexDate1, indexColor, gantt, ganttSteps, - ganttStepsToggle, ics, dateRange0, dateRange1, fieldId); err != nil { + attributeIdRecord, indexDate0, indexDate1, indexColor, gantt, + ganttSteps, ganttStepsToggle, ics, dateRange0, dateRange1, + fieldId); err != nil { return err } @@ -711,13 +719,14 @@ func setCalendar_tx(tx pgx.Tx, fieldId uuid.UUID, formIdOpen pgtype.UUID, if _, err := tx.Exec(db.Ctx, ` INSERT INTO app.field_calendar ( field_id, form_id_open, attribute_id_date0, attribute_id_date1, - attribute_id_color, index_date0, index_date1, index_color, - gantt, gantt_steps, gantt_steps_toggle, ics, date_range0, - date_range1 - ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) + attribute_id_color, attribute_id_record, index_date0, + index_date1, index_color, gantt, gantt_steps, + gantt_steps_toggle, ics, date_range0, date_range1 + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) `, fieldId, formIdOpen, attributeIdDate0, attributeIdDate1, - attributeIdColor, indexDate0, indexDate1, indexColor, - gantt, ganttSteps, ganttStepsToggle, ics, dateRange0, dateRange1); err != nil { + attributeIdColor, attributeIdRecord, indexDate0, indexDate1, + indexColor, gantt, ganttSteps, ganttStepsToggle, ics, dateRange0, + dateRange1); err != nil { return err } diff --git a/schema/module/module.go b/schema/module/module.go index caf1fe90..276adfea 100644 --- a/schema/module/module.go +++ b/schema/module/module.go @@ -95,8 +95,14 @@ func Get(ids []uuid.UUID) ([]types.Module, error) { } rows.Close() - // get captions + // get start forms & captions for i, mod := range modules { + + mod.StartForms, err = getStartForms(mod.Id) + if err != nil { + return modules, err + } + mod.Captions, err = caption.Get("module", mod.Id, []string{"moduleTitle", "moduleHelp"}) if err != nil { return modules, err @@ -109,8 +115,8 @@ func Get(ids []uuid.UUID) ([]types.Module, error) { func Set_tx(tx pgx.Tx, id uuid.UUID, parentId pgtype.UUID, formId pgtype.UUID, iconId pgtype.UUID, name string, color1 string, position int, languageMain string, releaseBuild int, releaseBuildApp int, - releaseDate int64, dependsOn []uuid.UUID, languages []string, - captions types.CaptionMap) error { + releaseDate int64, dependsOn []uuid.UUID, startForms []types.ModuleStartForm, + languages []string, captions types.CaptionMap) error { if err := db.CheckIdentifier(name); err != nil { return err @@ -232,14 +238,6 @@ func Set_tx(tx pgx.Tx, id uuid.UUID, parentId pgtype.UUID, return errors.New("module dependency to itself is not allowed") } - isCircular, err := hasCircularDependency_tx(tx, id, moduleIdOn) - if err != nil { - return err - } - if isCircular { - return errors.New("circular module dependency is not allowed") - } - if _, err := tx.Exec(db.Ctx, ` INSERT INTO app.module_depends (module_id, module_id_on) VALUES ($1,$2) @@ -248,6 +246,23 @@ func Set_tx(tx pgx.Tx, id uuid.UUID, parentId pgtype.UUID, } } + // set start forms + if _, err := tx.Exec(db.Ctx, ` + DELETE FROM app.module_start_form + WHERE module_id = $1 + `, id); err != nil { + return err + } + + for i, sf := range startForms { + if _, err := tx.Exec(db.Ctx, ` + INSERT INTO app.module_start_form (module_id, position, role_id, form_id) + VALUES ($1,$2,$3,$4) + `, id, i, sf.RoleId, sf.FormId); err != nil { + return err + } + } + // set languages if _, err := tx.Exec(db.Ctx, ` DELETE FROM app.module_language @@ -276,44 +291,29 @@ func Set_tx(tx pgx.Tx, id uuid.UUID, parentId pgtype.UUID, return nil } -func hasCircularDependency_tx(tx pgx.Tx, moduleIdSource uuid.UUID, - moduleIdCandidate uuid.UUID) (bool, error) { - - moduleIdsCheckNext := make([]uuid.UUID, 0) +func getStartForms(id uuid.UUID) ([]types.ModuleStartForm, error) { - rows, err := tx.Query(db.Ctx, ` - SELECT module_id_on - FROM app.module_depends + startForms := make([]types.ModuleStartForm, 0) + rows, err := db.Pool.Query(db.Ctx, ` + SELECT role_id, form_id + FROM app.module_start_form WHERE module_id = $1 - `, moduleIdCandidate) + ORDER BY position ASC + `, id) if err != nil { - return false, err + return startForms, err } + defer rows.Close() for rows.Next() { - var id uuid.UUID - if err := rows.Scan(&id); err != nil { - rows.Close() - return false, err - } - - // any dependency from any module to source module is circular dependency - if moduleIdSource == id { - rows.Close() - return true, nil + var sf types.ModuleStartForm + if err := rows.Scan(&sf.RoleId, &sf.FormId); err != nil { + return startForms, err } - moduleIdsCheckNext = append(moduleIdsCheckNext, id) - } - rows.Close() + startForms = append(startForms, sf) - // check modules that candidate is dependent on for dependency to source module - for _, id := range moduleIdsCheckNext { - isCircular, err := hasCircularDependency_tx(tx, moduleIdSource, id) - if isCircular || err != nil { - return isCircular, err - } } - return false, nil + return startForms, nil } func getDependsOn_tx(tx pgx.Tx, id uuid.UUID) ([]uuid.UUID, error) { diff --git a/schema/preset/preset.go b/schema/preset/preset.go index 10c3e024..1ec0c35c 100644 --- a/schema/preset/preset.go +++ b/schema/preset/preset.go @@ -287,7 +287,8 @@ func setRecord_tx(tx pgx.Tx, relationId uuid.UUID, presetId uuid.UUID, recordId // if refered record does not exist, do not set record // otherwise potential NOT NULL constraint would be breached if !exists { - return fmt.Errorf("referenced preset '%s' does not exist", value.PresetIdRefer.Bytes) + return fmt.Errorf("referenced preset '%s' does not exist", + uuid.FromBytesOrNil(value.PresetIdRefer.Bytes[:])) } sqlNames = append(sqlNames, fmt.Sprintf(`"%s"`, atrName)) sqlValues = append(sqlValues, recordId) diff --git a/schema/query/query.go b/schema/query/query.go index a951038d..0305e879 100644 --- a/schema/query/query.go +++ b/schema/query/query.go @@ -3,6 +3,7 @@ package query import ( "errors" "fmt" + "r3/compatible" "r3/db" "r3/schema" "r3/schema/caption" @@ -39,11 +40,11 @@ func Get(entity string, id uuid.UUID, filterPosition int, filterSide int) (types } if err := db.Pool.QueryRow(db.Ctx, fmt.Sprintf(` - SELECT id, relation_id + SELECT id, relation_id, fixed_limit FROM app.query WHERE %s_id = $1 %s - `, entity, filterClause), id).Scan(&q.Id, &q.RelationId); err != nil { + `, entity, filterClause), id).Scan(&q.Id, &q.RelationId, &q.FixedLimit); err != nil { return q, err } @@ -208,17 +209,19 @@ func Set_tx(tx pgx.Tx, entity string, entityId uuid.UUID, filterPosition int, if subQuery { if _, err := tx.Exec(db.Ctx, ` - INSERT INTO app.query (id, query_filter_query_id, + INSERT INTO app.query (id, fixed_limit, query_filter_query_id, query_filter_position, query_filter_side) - VALUES ($1,$2,$3,$4) - `, query.Id, entityId, filterPosition, filterSide); err != nil { + VALUES ($1,$2,$3,$4,$5) + `, query.Id, query.FixedLimit, entityId, + filterPosition, filterSide); err != nil { + return err } } else { if _, err := tx.Exec(db.Ctx, fmt.Sprintf(` - INSERT INTO app.query (id, %s_id) - VALUES ($1,$2) - `, entity), query.Id, entityId); err != nil { + INSERT INTO app.query (id, fixed_limit, %s_id) + VALUES ($1,$2,$3) + `, entity), query.Id, query.FixedLimit, entityId); err != nil { return err } } @@ -226,9 +229,9 @@ func Set_tx(tx pgx.Tx, entity string, entityId uuid.UUID, filterPosition int, if _, err := tx.Exec(db.Ctx, ` UPDATE app.query - SET relation_id = $1 - WHERE id = $2 - `, query.RelationId, query.Id); err != nil { + SET relation_id = $1, fixed_limit = $2 + WHERE id = $3 + `, query.RelationId, query.FixedLimit, query.Id); err != nil { return err } @@ -411,14 +414,14 @@ func getFilterSide(queryId uuid.UUID, filterPosition int, side int) (types.Query if err := db.Pool.QueryRow(db.Ctx, ` SELECT attribute_id, attribute_index, attribute_nested, brackets, - content, field_id, role_id, query_aggregator, value + content, field_id, preset_id, role_id, query_aggregator, value FROM app.query_filter_side WHERE query_id = $1 AND query_filter_position = $2 AND side = $3 `, queryId, filterPosition, side).Scan(&s.AttributeId, &s.AttributeIndex, - &s.AttributeNested, &s.Brackets, &s.Content, &s.FieldId, &s.RoleId, - &s.QueryAggregator, &s.Value); err != nil { + &s.AttributeNested, &s.Brackets, &s.Content, &s.FieldId, &s.PresetId, + &s.RoleId, &s.QueryAggregator, &s.Value); err != nil { return s, err } @@ -469,16 +472,19 @@ func setFilters_tx(tx pgx.Tx, queryId uuid.UUID, queryChoiceId pgtype.UUID, func SetFilterSide_tx(tx pgx.Tx, queryId uuid.UUID, filterPosition int, side int, s types.QueryFilterSide) error { + // fix imports < 2.5: New filter side option: Preset + s.PresetId = compatible.FixPgxNull(s.PresetId).(pgtype.UUID) + if _, err := tx.Exec(db.Ctx, ` INSERT INTO app.query_filter_side ( query_id, query_filter_position, side, attribute_id, attribute_index, attribute_nested, brackets, content, field_id, - role_id, query_aggregator, value + preset_id, role_id, query_aggregator, value ) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) `, queryId, filterPosition, side, s.AttributeId, s.AttributeIndex, - s.AttributeNested, s.Brackets, s.Content, s.FieldId, s.RoleId, - s.QueryAggregator, s.Value); err != nil { + s.AttributeNested, s.Brackets, s.Content, s.FieldId, s.PresetId, + s.RoleId, s.QueryAggregator, s.Value); err != nil { return err } diff --git a/schema/relation/relation.go b/schema/relation/relation.go index 393daf9f..9c22709b 100644 --- a/schema/relation/relation.go +++ b/schema/relation/relation.go @@ -77,13 +77,20 @@ func Get(moduleId uuid.UUID) ([]types.Relation, error) { } r.ModuleId = moduleId r.Attributes = make([]types.Attribute, 0) + + r.Policies, err = getPolicies(r.Id) + if err != nil { + return relations, err + } + relations = append(relations, r) } return relations, nil } func Set_tx(tx pgx.Tx, moduleId uuid.UUID, id uuid.UUID, name string, - retentionCount pgtype.Int4, retentionDays pgtype.Int4) error { + retentionCount pgtype.Int4, retentionDays pgtype.Int4, + policies []types.RelationPolicy) error { if err := db.CheckIdentifier(name); err != nil { return err @@ -154,6 +161,11 @@ func Set_tx(tx pgx.Tx, moduleId uuid.UUID, id uuid.UUID, name string, } } } + + // set policies + if err := setPolicies_tx(tx, id, policies); err != nil { + return err + } return nil } diff --git a/schema/relation/relationPolicy.go b/schema/relation/relationPolicy.go new file mode 100644 index 00000000..0d646771 --- /dev/null +++ b/schema/relation/relationPolicy.go @@ -0,0 +1,71 @@ +package relation + +import ( + "r3/db" + "r3/types" + + "github.com/gofrs/uuid" + "github.com/jackc/pgx/v4" +) + +func delPolicies_tx(tx pgx.Tx, relationId uuid.UUID) error { + _, err := tx.Exec(db.Ctx, ` + DELETE FROM app.relation_policy + WHERE relation_id = $1 + `, relationId) + return err +} + +func getPolicies(relationId uuid.UUID) ([]types.RelationPolicy, error) { + + policies := make([]types.RelationPolicy, 0) + + rows, err := db.Pool.Query(db.Ctx, ` + SELECT role_id, pg_function_id_excl, pg_function_id_incl, + action_delete, action_select, action_update + FROM app.relation_policy + WHERE relation_id = $1 + ORDER BY position ASC + `, relationId) + if err != nil { + return policies, err + } + defer rows.Close() + + for rows.Next() { + var p types.RelationPolicy + + if err := rows.Scan(&p.RoleId, &p.PgFunctionIdExcl, + &p.PgFunctionIdIncl, &p.ActionDelete, &p.ActionSelect, + &p.ActionUpdate); err != nil { + + return policies, err + } + policies = append(policies, p) + } + return policies, nil +} + +func setPolicies_tx(tx pgx.Tx, relationId uuid.UUID, policies []types.RelationPolicy) error { + + if err := delPolicies_tx(tx, relationId); err != nil { + return err + } + + for i, p := range policies { + _, err := tx.Exec(db.Ctx, ` + INSERT INTO app.relation_policy ( + relation_id, position, role_id, + pg_function_id_excl, pg_function_id_incl, + action_delete, action_select, action_update + ) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8) + `, relationId, i, p.RoleId, p.PgFunctionIdExcl, p.PgFunctionIdIncl, + p.ActionDelete, p.ActionSelect, p.ActionUpdate) + + if err != nil { + return err + } + } + return nil +} diff --git a/schema/relation/relation_preview.go b/schema/relation/relationPreview.go similarity index 100% rename from schema/relation/relation_preview.go rename to schema/relation/relationPreview.go diff --git a/transfer/transfer_import.go b/transfer/transfer_import.go index 1425b41a..b212eaa0 100644 --- a/transfer/transfer_import.go +++ b/transfer/transfer_import.go @@ -184,10 +184,11 @@ func import_tx(tx pgx.Tx, mod types.Module, firstRun bool, lastRun bool, log.Info("transfer", fmt.Sprintf("set module '%s' v%d, %s", mod.Name, mod.ReleaseBuild, mod.Id)) - if err := importCheckResultAndApply(tx, module.Set_tx(tx, mod.Id, mod.ParentId, mod.FormId, mod.IconId, - mod.Name, mod.Color1, mod.Position, mod.LanguageMain, mod.ReleaseBuild, - mod.ReleaseBuildApp, mod.ReleaseDate, mod.DependsOn, mod.Languages, - mod.Captions), mod.Id, idMapSkipped); err != nil { + if err := importCheckResultAndApply(tx, module.Set_tx(tx, mod.Id, + mod.ParentId, mod.FormId, mod.IconId, mod.Name, mod.Color1, + mod.Position, mod.LanguageMain, mod.ReleaseBuild, + mod.ReleaseBuildApp, mod.ReleaseDate, mod.DependsOn, mod.StartForms, + mod.Languages, mod.Captions), mod.Id, idMapSkipped); err != nil { return err } @@ -223,8 +224,8 @@ func import_tx(tx pgx.Tx, mod types.Module, firstRun bool, lastRun bool, log.Info("transfer", fmt.Sprintf("set relation %s", e.Id)) if err := importCheckResultAndApply(tx, relation.Set_tx(tx, - e.ModuleId, e.Id, e.Name, e.RetentionCount, e.RetentionDays), - e.Id, idMapSkipped); err != nil { + e.ModuleId, e.Id, e.Name, e.RetentionCount, e.RetentionDays, + e.Policies), e.Id, idMapSkipped); err != nil { return err } @@ -558,7 +559,11 @@ func parseModulesFromPaths(filePaths []string, moduleIdMapMeta map[uuid.UUID]imp return } - // add dependencies first + // add itself before dependencies (avoids infinite loops from circular dependencies) + modules = append(modules, m) + moduleIdsAdded = append(moduleIdsAdded, m.Id) + + // add dependencies for _, dependId := range m.DependsOn { if _, exists := moduleIdMapMeta[dependId]; !exists { @@ -567,22 +572,18 @@ func parseModulesFromPaths(filePaths []string, moduleIdMapMeta map[uuid.UUID]imp } addModule(moduleIdMapMeta[dependId].module) } - - // add itself - modules = append(modules, m) - moduleIdsAdded = append(moduleIdsAdded, m.Id) } for _, meta := range moduleIdMapMeta { addModule(meta.module) } - // log optimized import order + // log chosen installation order logNames := make([]string, len(modules)) for i, m := range modules { logNames[i] = m.Name } - log.Info("transfer", fmt.Sprintf("import has decided on optimized order: %s", + log.Info("transfer", fmt.Sprintf("import has decided on installation order: %s", strings.Join(logNames, ", "))) return modules, nil diff --git a/types/types_config.go b/types/types_config.go index ed9c3949..bf267db4 100644 --- a/types/types_config.go +++ b/types/types_config.go @@ -1,20 +1,15 @@ package types type FileType struct { - Builder bool `json:"builder"` - Db FileTypeDb `json:"db"` Paths struct { - Captions string `json:"captions"` Certificates string `json:"certificates"` EmbeddedDbBin string `json:"embeddedDbBin"` EmbeddedDbData string `json:"embeddedDbData"` Files string `json:"files"` - Packages string `json:"packages"` Temp string `json:"temp"` Transfer string `json:"transfer"` - Web string `json:"web"` } `json:"paths"` Web struct { diff --git a/types/types_handler.go b/types/types_handler.go index 60c46eff..0f31fff6 100644 --- a/types/types_handler.go +++ b/types/types_handler.go @@ -3,6 +3,8 @@ package types // a server side event, affecting one or many clients (by associated login ID) type ClientEvent struct { LoginId int64 // affected login (0=all logins) + BuilderOff bool // inform client: builder mode disabled + BuilderOn bool // inform client: builder mode enabled Kick bool // kick login (usually because it was disabled) KickNonAdmin bool // kick login if not admin (usually because maintenance mode was enabled) Renew bool // renew login (permissions changed) diff --git a/types/types_repo.go b/types/types_repo.go index 1ee6f40b..3ba40eb2 100644 --- a/types/types_repo.go +++ b/types/types_repo.go @@ -2,12 +2,14 @@ package types import ( "github.com/gofrs/uuid" + "github.com/jackc/pgtype" ) type RepoModule struct { ModuleId uuid.UUID `json:"moduleId"` FileId uuid.UUID `json:"fileId"` Name string `json:"name"` + ChangeLog pgtype.Varchar `json:"changeLog"` Author string `json:"author"` InStore bool `json:"inStore"` ReleaseBuild int `json:"releaseBuild"` diff --git a/types/types_schema.go b/types/types_schema.go index e1f74221..6e63f403 100644 --- a/types/types_schema.go +++ b/types/types_schema.go @@ -6,39 +6,54 @@ import ( ) type Module struct { - Id uuid.UUID `json:"id"` - ParentId pgtype.UUID `json:"parentId"` // module parent ID - FormId pgtype.UUID `json:"formId"` // start form - IconId pgtype.UUID `json:"iconId"` // module icon in header/menu - Name string `json:"name"` // name of module, is used for DB schema - Color1 string `json:"color1"` // primary module color (used for header) - Position int `json:"position"` // position of module in nav. contexts (home, header) - LanguageMain string `json:"languageMain"` // language code of main language (for fallback) - ReleaseBuild int `json:"releaseBuild"` // build of this module, incremented with each release - ReleaseBuildApp int `json:"releaseBuildApp"` // build of app at last release - ReleaseDate int64 `json:"releaseDate"` // date of last release - DependsOn []uuid.UUID `json:"dependsOn"` // modules that this module is dependent on - Languages []string `json:"languages"` // language codes that this module supports - Relations []Relation `json:"relations"` - Forms []Form `json:"forms"` - Menus []Menu `json:"menus"` - Icons []Icon `json:"icons"` - Roles []Role `json:"roles"` - LoginForms []LoginForm `json:"loginForms"` - PgFunctions []PgFunction `json:"pgFunctions"` - Captions CaptionMap `json:"captions"` + Id uuid.UUID `json:"id"` + ParentId pgtype.UUID `json:"parentId"` // module parent ID + FormId pgtype.UUID `json:"formId"` // default start form + IconId pgtype.UUID `json:"iconId"` // module icon in header/menu + Name string `json:"name"` // name of module, is used for DB schema + Color1 string `json:"color1"` // primary module color (used for header) + Position int `json:"position"` // position of module in nav. contexts (home, header) + LanguageMain string `json:"languageMain"` // language code of main language (for fallback) + ReleaseBuild int `json:"releaseBuild"` // build of this module, incremented with each release + ReleaseBuildApp int `json:"releaseBuildApp"` // build of app at last release + ReleaseDate int64 `json:"releaseDate"` // date of last release + DependsOn []uuid.UUID `json:"dependsOn"` // modules that this module is dependent on + StartForms []ModuleStartForm `json:"startForms"` // start forms, assigned via role membership + Languages []string `json:"languages"` // language codes that this module supports + Relations []Relation `json:"relations"` + Forms []Form `json:"forms"` + Menus []Menu `json:"menus"` + Icons []Icon `json:"icons"` + Roles []Role `json:"roles"` + LoginForms []LoginForm `json:"loginForms"` + PgFunctions []PgFunction `json:"pgFunctions"` + Captions CaptionMap `json:"captions"` +} +type ModuleStartForm struct { + Position int `json:"position"` + RoleId uuid.UUID `json:"roleId"` + FormId uuid.UUID `json:"formId"` } type Relation struct { - Id uuid.UUID `json:"id"` - ModuleId uuid.UUID `json:"moduleId"` - AttributeIdPk uuid.UUID `json:"attributeIdPk"` // read only, ID of PK attribute - Name string `json:"name"` - RetentionCount pgtype.Int4 `json:"retentionCount"` - RetentionDays pgtype.Int4 `json:"retentionDays"` - Attributes []Attribute `json:"attributes"` // read only, all relation attributes - Indexes []PgIndex `json:"indexes"` // read only, all relation indexes - Presets []Preset `json:"presets"` // read only, all relation presets - Triggers []PgTrigger `json:"triggers"` // read only, all relation triggers + Id uuid.UUID `json:"id"` + ModuleId uuid.UUID `json:"moduleId"` + AttributeIdPk uuid.UUID `json:"attributeIdPk"` // read only, ID of PK attribute + Name string `json:"name"` + RetentionCount pgtype.Int4 `json:"retentionCount"` + RetentionDays pgtype.Int4 `json:"retentionDays"` + Attributes []Attribute `json:"attributes"` // read only, all relation attributes + Indexes []PgIndex `json:"indexes"` // read only, all relation indexes + Policies []RelationPolicy `json:"policies"` // read only, all relation policies + Presets []Preset `json:"presets"` // read only, all relation presets + Triggers []PgTrigger `json:"triggers"` // read only, all relation triggers +} +type RelationPolicy struct { + RoleId uuid.UUID `json:"roleId"` + PgFunctionIdExcl uuid.NullUUID `json:"pgFunctionIdExcl"` + PgFunctionIdIncl uuid.NullUUID `json:"pgFunctionIdIncl"` + ActionDelete bool `json:"actionDelete"` + ActionSelect bool `json:"actionSelect"` + ActionUpdate bool `json:"actionUpdate"` } type Preset struct { Id uuid.UUID `json:"id"` @@ -146,26 +161,27 @@ type FieldButton struct { Captions CaptionMap `json:"captions"` } type FieldCalendar struct { - Id uuid.UUID `json:"id"` - IconId pgtype.UUID `json:"iconId"` - Content string `json:"content"` - State string `json:"state"` - OnMobile bool `json:"onMobile"` - FormIdOpen pgtype.UUID `json:"formIdOpen"` - AttributeIdDate0 uuid.UUID `json:"attributeIdDate0"` - AttributeIdDate1 uuid.UUID `json:"attributeIdDate1"` - AttributeIdColor pgtype.UUID `json:"attributeIdColor"` - IndexDate0 int `json:"indexDate0"` - IndexDate1 int `json:"indexDate1"` - IndexColor pgtype.Int4 `json:"indexColor"` - Gantt bool `json:"gantt"` // gantt presentation - GanttSteps pgtype.Varchar `json:"ganttSteps"` // gantt step type (hours, days) - GanttStepsToggle bool `json:"ganttStepsToggle"` // user can toggle between gantt step types - Ics bool `json:"ics"` // calendar available as ICS download - DateRange0 int64 `json:"dateRange0"` // ICS/gantt time range before NOW (seconds) - DateRange1 int64 `json:"dateRange1"` // ICS/gantt time range after NOW (seconds) - Columns []Column `json:"columns"` - Query Query `json:"query"` + Id uuid.UUID `json:"id"` + IconId pgtype.UUID `json:"iconId"` + Content string `json:"content"` + State string `json:"state"` + OnMobile bool `json:"onMobile"` + FormIdOpen pgtype.UUID `json:"formIdOpen"` + AttributeIdDate0 uuid.UUID `json:"attributeIdDate0"` + AttributeIdDate1 uuid.UUID `json:"attributeIdDate1"` + AttributeIdColor pgtype.UUID `json:"attributeIdColor"` + AttributeIdRecord pgtype.UUID `json:"attributeIdRecord"` + IndexDate0 int `json:"indexDate0"` + IndexDate1 int `json:"indexDate1"` + IndexColor pgtype.Int4 `json:"indexColor"` + Gantt bool `json:"gantt"` // gantt presentation + GanttSteps pgtype.Varchar `json:"ganttSteps"` // gantt step type (hours, days) + GanttStepsToggle bool `json:"ganttStepsToggle"` // user can toggle between gantt step types + Ics bool `json:"ics"` // calendar available as ICS download + DateRange0 int64 `json:"dateRange0"` // ICS/gantt time range before NOW (seconds) + DateRange1 int64 `json:"dateRange1"` // ICS/gantt time range after NOW (seconds) + Columns []Column `json:"columns"` + Query Query `json:"query"` } type FieldChart struct { Id uuid.UUID `json:"id"` diff --git a/types/types_schema_query.go b/types/types_schema_query.go index dbf0b590..1f0e7e1a 100644 --- a/types/types_schema_query.go +++ b/types/types_schema_query.go @@ -20,11 +20,12 @@ var ( type Query struct { Id uuid.UUID `json:"id"` RelationId pgtype.UUID `json:"relationId"` // query source relation + FixedLimit int `json:"fixedLimit"` // fixed limit, used for queries like 'top 5 of X' Joins []QueryJoin `json:"joins"` // query joins over multiple relations Filters []QueryFilter `json:"filters"` // default query filter Orders []QueryOrder `json:"orders"` // default query sort Lookups []QueryLookup `json:"lookups"` // import lookups via PG indexes - Choices []QueryChoice `json:"choices"` // optional filters, selectable by users + Choices []QueryChoice `json:"choices"` // named filter sets, selectable by users } type QueryJoin struct { @@ -56,6 +57,7 @@ type QueryFilterSide struct { AttributeNested int `json:"attributeNested"` // nesting level of attribute (0=main query, 1=1st sub query) AttributeIndex int `json:"attributeIndex"` // relation index of attribute FieldId pgtype.UUID `json:"fieldId"` // frontend field value + PresetId pgtype.UUID `json:"presetId"` // preset ID of record to be compared RoleId pgtype.UUID `json:"roleId"` // role ID assigned to user Brackets int `json:"brackets"` // opening/closing brackets (side 0/1) diff --git a/www/comps/admin/adminConfig.js b/www/comps/admin/adminConfig.js index 4a91653f..9370b896 100644 --- a/www/comps/admin/adminConfig.js +++ b/www/comps/admin/adminConfig.js @@ -68,7 +68,19 @@ let MyAdminConfig = { + + + + {{ capApp.builderMode }} + + @@ -698,6 +710,34 @@ let MyAdminConfig = { that.$root.genericError(null,error); }; }, + informBuilderMode:function() { + if(this.configInput.builderMode === '0') + return; + + this.$store.commit('dialog',{ + captionBody:this.capApp.dialog.builderMode, + captionTop:this.capApp.dialog.pleaseRead, + buttons:[{ + caption:this.capGen.button.close, + cancel:true, + image:'cancel.png' + }] + }); + }, + informProductionMode:function() { + if(this.configInput.productionMode !== '0') + return; + + this.$store.commit('dialog',{ + captionBody:this.capApp.dialog.productionMode, + captionTop:this.capApp.dialog.pleaseRead, + buttons:[{ + caption:this.capGen.button.close, + cancel:true, + image:'cancel.png' + }] + }); + }, publicKeyShow:function(name,key) { this.$store.commit('dialog',{ captionBody:key, @@ -746,6 +786,13 @@ let MyAdminConfig = { trans.send(this.$root.genericError); } + // inform clients about changed builder mode + if(req.payload.builderMode !== this.config.builderMode) { + let trans = new wsHub.transaction(); + trans.add('login','informBuilderState',{}); + trans.send(this.$root.genericError); + } + // update store config this.$store.commit('config',JSON.parse(JSON.stringify(this.configInput))); diff --git a/www/comps/admin/adminModules.js b/www/comps/admin/adminModules.js index 48df097f..39940762 100644 --- a/www/comps/admin/adminModules.js +++ b/www/comps/admin/adminModules.js @@ -19,9 +19,6 @@ let MyAdminModulesItem = { - - '{{ module.name }}' v{{ module.releaseBuild }} - {{ module.releaseDate === 0 ? '-' : getUnixFormat(module.releaseDate,'Y-m-d') }} @@ -46,6 +43,14 @@ let MyAdminModulesItem = { :image="!installStarted ? 'download.png' : 'load.gif'" /> + + + @@ -95,35 +99,54 @@ let MyAdminModulesItem = { }; }, computed:{ - dependOnUsDisplay:function() { - if(this.dependOnUsNames.length === 0) - return ''; - - return this.capApp.dependOnUs.replace('{NAMES}',this.dependOnUsNames.join(', ')); + hasChanges:function() { + return this.position !== this.options.position + || this.hidden !== this.options.hidden + || this.owner !== this.options.owner; }, - dependOnUsNames:function() { + moduleNamesDependendOnUs:function() { let out = []; - for(let i = 0, j = this.modules.length; i < j; i++) { - let m = this.modules[i]; - - if(m.id === this.id) - continue; + for(let i = 0, j = this.moduleIdsDependendOnUs.length; i < j; i++) { + let m = this.moduleIdMap[this.moduleIdsDependendOnUs[i]]; + out.push(m.name); + } + return out; + }, + moduleIdsDependendOnUs:function() { + let out = []; + let that = this; + + let addDependendIds = function(m) { - for(let x = 0, y = m.dependsOn.length; x < y; x++) { - if(m.dependsOn[x] === this.id) { - out.push(m.name); + // check all other modules for dependency to parent module + for(let i = 0, j = that.modules.length; i < j; i++) { + + let childId = that.modules[i].id; + + // root, parent module or was already added + if(childId === that.module.id || childId === m.id || out.includes(childId)) + continue; + + for(let x = 0, y = that.modules[i].dependsOn.length; x < y; x++) { + + if(that.modules[i].dependsOn[x] !== m.id) + continue; + + out.push(childId); + + // add dependencies from child as well + addDependendIds(that.modules[i]); break; } } - } + }; + + // get dependencies if this module (root) + addDependendIds(this.module); + return out; }, - hasChanges:function() { - return this.position !== this.options.position - || this.hidden !== this.options.hidden - || this.owner !== this.options.owner; - }, // repository isInRepo:function() { @@ -149,8 +172,16 @@ let MyAdminModulesItem = { return false; }, + // simple + changeLog:function() { + if(this.repoModule === false) return ''; + + return this.repoModule.changeLog; + }, + // stores modules: function() { return this.$store.getters['schema/modules']; }, + moduleIdMap: function() { return this.$store.getters['schema/moduleIdMap']; }, builderEnabled:function() { return this.$store.getters.builderEnabled; }, capApp: function() { return this.$store.getters.captions.admin.modules; }, capGen: function() { return this.$store.getters.captions.generic; }, @@ -164,6 +195,19 @@ let MyAdminModulesItem = { getUnixFormat, srcBase64Icon, + changeLogShow:function() { + this.$store.commit('dialog',{ + captionTop:this.capApp.changeLog, + captionBody:this.changeLog, + image:'time.png', + width:1000, + buttons:[{ + cancel:true, + caption:this.capGen.button.close, + image:'cancel.png' + }] + }); + }, ownerEnable:function() { this.owner = true; }, @@ -174,8 +218,8 @@ let MyAdminModulesItem = { } this.$store.commit('dialog',{ - captionTop:this.capApp.dialog.ownerTitle, captionBody:this.capApp.dialog.owner, + captionTop:this.capApp.dialog.ownerTitle, image:'warning.png', buttons:[{ cancel:true, @@ -191,12 +235,21 @@ let MyAdminModulesItem = { // backend calls delAsk:function() { + let appNames = ''; + + if(this.moduleNamesDependendOnUs.length !== 0) + appNames = this.capApp.dialog.deleteApps.replace('{LIST}', + `
  • ${this.moduleNamesDependendOnUs.join('
  • ')}
  • ` + ); + this.$store.commit('dialog',{ - captionBody:this.capApp.dialog.delete, + captionBody:this.capApp.dialog.delete.replace('{APPS}',appNames), + captionTop:this.capApp.dialog.deleteTitle.replace('{APP}',this.module.name), + image:'warning.png', buttons:[{ cancel:true, caption:this.capGen.button.delete, - exec:this.del, + exec:this.delAsk2, image:'delete.png' },{ caption:this.capGen.button.cancel, @@ -204,12 +257,34 @@ let MyAdminModulesItem = { }] }); }, + delAsk2:function() { + this.$nextTick(function() { + this.$store.commit('dialog',{ + captionBody:this.capApp.dialog.deleteMulti.replace('{COUNT}',this.moduleNamesDependendOnUs.length + 1), + captionTop:this.capApp.dialog.deleteTitle.replace('{APP}',this.module.name), + image:'warning.png', + buttons:[{ + cancel:true, + caption:this.capGen.button.delete, + exec:this.del, + image:'delete.png' + },{ + caption:this.capGen.button.cancel, + image:'cancel.png' + }] + }); + }); + }, del:function() { let trans = new wsHub.transactionBlocking(); - trans.add('module','del',{ - id:this.id - },this.delOk); - trans.send(this.$root.genericError); + trans.add('module','del',{id:this.id}); + + // add dependencies to delete + for(let i = 0, j = this.moduleIdsDependendOnUs.length; i < j; i++) { + trans.add('module','del',{id:this.moduleIdsDependendOnUs[i]}); + } + + trans.send(this.$root.genericError,this.delOk); }, delOk:function(res) { this.$root.schemaReload(); @@ -297,12 +372,6 @@ let MyAdminModules = { {{ capGen.application }} - -
    - - {{ capGen.version }} -
    -
    @@ -315,6 +384,12 @@ let MyAdminModules = { {{ capApp.update }}
    + +
    + + {{ capApp.changeLog }} +
    +
    diff --git a/www/comps/app.css b/www/comps/app.css index 9b30775e..2562d1c7 100644 --- a/www/comps/app.css +++ b/www/comps/app.css @@ -69,7 +69,7 @@ --light-contrast: calc(100% - (var(--light-depth)/3)); /* stuff to contrast with read content, backgrounds mostly */ /* depth based colors */ - --color-bg: hsl(315,10%, var(--light-contrast)); + --color-bg: hsl(315,10%,var(--light-contrast)); --color-bg-font: hsl(0, 0%, var(--light-contrast)); --color-border: hsl(0, 0%, calc(60% - (var(--light-depth) * 3) )); --color-font: hsl(0, 0%, var(--light-read)); @@ -78,8 +78,9 @@ --color-accent1: hsl(311,57%,calc(40% - var(--light-depth))); /* field captions */ --color-accent2: hsl(127,86%,calc(96% - var(--light-depth))); /* calendar/gantt weekend */ - --color-accent3: hsl(199,11%,calc(92% - var(--light-depth))); /* list/calendar/gantt entry hover */ - --color-accent3-alt: hsl(199,11%,calc(86% - var(--light-depth))); /* list/calendar/gantt entry active */ + --color-accent3: hsl(199,11%,calc(92% - var(--light-depth))); /* menu/list/calendar entry hover */ + --color-accent3-alt: hsl(199,11%,calc(86% - var(--light-depth))); /* menu/list/calendar entry active */ + --color-accent4: hsl(339, 4%,calc(61% - var(--light-depth))); /* gantt entry */ --color-error: hsl(0, 84%,calc(61% - var(--light-depth))); --color-success: hsl(113,44%,calc(44% - var(--light-depth))); --color-action: hsl(146,37%,calc(37% - var(--light-depth))); @@ -134,6 +135,7 @@ body { --depth:9; } .gantt-header.lower { --depth:5; } .gantt-group { --depth:0; } .gantt-group:nth-child(odd) { --depth:2; } +.gantt-line { --depth:-2; } .button { --depth:4; } .button.background:focus { --depth:0; } .button.background:hover { --depth:0; } @@ -741,7 +743,8 @@ table td.maximum{ /* HTML documentation */ .html-docs{ text-align:justify; - line-height:140%; + font-size:110%; + line-height:150%; padding:0px 16px !important; } .html-docs h1{ @@ -753,6 +756,22 @@ table td.maximum{ .html-docs h3{ margin:14px 0px 0px 0px; } +.html-docs img{ + width:100%; + margin:5px 0px; + box-shadow:1px 1px 3px var(--color-shade); +} +.html-docs li img{ + margin:10px 0px; +} +.html-docs p, +.html-docs img{ + max-width:1000px; +} +.html-docs ol, +.html-docs ul{ + max-width:950px; +} /* transitions*/ @@ -870,6 +889,7 @@ table td.maximum{ --color-accent2: hsl(127,06%,calc(25% - (1.50% * var(--depth)) )); --color-accent3: hsl(199,11%,calc(39% - (1.50% * var(--depth)) )); --color-accent3-alt:hsl(199,11%,calc(32% - (1.50% * var(--depth)) )); + --color-accent4: hsl(339, 4%,calc(61% - (1.50% * var(--depth)) )); --color-form-builder-actions:hsl(195,17%,29%,1.00); --color-form-builder-columns:hsl(247,17%,46%,0.42); diff --git a/www/comps/app.js b/www/comps/app.js index fa8398c9..6f60b83a 100644 --- a/www/comps/app.js +++ b/www/comps/app.js @@ -2,7 +2,7 @@ import MyDialog from './dialog.js'; import MyFeedback from './feedback.js'; import MyHeader from './header.js'; import MyLogin from './login.js'; -import {hasAccessToAnyMenu} from './shared/access.js'; +import {getStartFormId} from './shared/access.js'; import {genericError} from './shared/error.js'; import {getCaptionForModule} from './shared/language.js'; import {openLink} from './shared/generic.js'; @@ -124,8 +124,8 @@ let MyApp = { return false; // module is accessible if start form is set and user has access to any menu - let accessible = module.formId !== null - && that.hasAccessToAnyMenu(module.menus,that.menuAccess); + let formIdStart = getStartFormId(module,that.access); + let accessible = formIdStart !== null; // ignore hidden modules if(that.moduleIdMapOptions[module.id].hidden) @@ -160,7 +160,7 @@ let MyApp = { accessible:accessible, caption:caption, children:children, - formId:module.formId, + formId:formIdStart, iconId:module.iconId, id:module.id, name:module.name, @@ -194,6 +194,7 @@ let MyApp = { httpMode:function() { return location.protocol === 'http:'; }, // stores + access: function() { return this.$store.getters.access; }, activated: function() { return this.$store.getters['local/activated']; }, appVersion: function() { return this.$store.getters['local/appVersion']; }, customLogo: function() { return this.$store.getters['local/customLogo']; }, @@ -209,7 +210,6 @@ let MyApp = { isAtDialog: function() { return this.$store.getters.isAtDialog; }, isAtFeedback: function() { return this.$store.getters.isAtFeedback; }, isMobile: function() { return this.$store.getters.isMobile; }, - menuAccess: function() { return this.$store.getters.access.menu; }, pageTitle: function() { return this.$store.getters.pageTitle; }, settings: function() { return this.$store.getters.settings; } }, @@ -228,7 +228,7 @@ let MyApp = { // externals genericError, getCaptionForModule, - hasAccessToAnyMenu, + getStartFormId, openLink, // general app states @@ -261,8 +261,6 @@ let MyApp = { this.stateChange(); }, wsBackendRequest:function(res) { - - let trans; switch(res.ressource) { // affects admins only (reloads happen in maintenance mode only) // add busy counters to also block admins that did not request the schema reload @@ -279,12 +277,17 @@ let MyApp = { this.initSchema(); break; + // affects admins only (builder can be actived only in maintenance mode) + case 'builder_mode_changed': + this.$store.commit('builder',res.payload); + break; + // affects everyone logged in case 'reauthorized': if(!this.appReady) return; - trans = new wsHub.transaction(); + let trans = new wsHub.transaction(); trans.add('lookup','get',{name:'access'},this.retrievedAccess); trans.send(this.genericError); break; @@ -343,6 +346,7 @@ let MyApp = { this.$store.commit('builder',r.payload.builder); this.$store.commit('productionMode',r.payload.productionMode); this.$store.commit('pageTitle',this.pageTitle); // apply new app short name to page + this.$store.commit('schema/languageCodes',r.payload.languageCodes); this.$store.commit('schema/timestamp',r.payload.schemaTimestamp); this.publicLoaded = true; this.stateChange(); diff --git a/www/comps/builder/builder.css b/www/comps/builder/builder.css index 054b9bef..f3590933 100644 --- a/www/comps/builder/builder.css +++ b/www/comps/builder/builder.css @@ -294,8 +294,30 @@ flex-flow:row nowrap; margin:0px 0px 6px 0px; } +.builder-query .fixed-limit{ + display:flex; + flex-flow:row nowrap; + align-items:center; + justify-content:space-between; +} /* relation */ +.builder-relations .sub-component{ + display:flex; + flex-flow:column nowrap; + align-items:flex-start; + padding:6px 12px 12px 12px; + margin:0px 15px 10px; + border:1px solid var(--color-border); + border-top:none; + border-radius:0px 0px 5px 5px; +} +.builder-relations .sub-component table{ + margin:10px 0px 20px; +} +.builder-relations .sub-component table td{ + padding:3px 5px; +} .builder-relation{ flex:1 1 auto; flex-direction:column; diff --git a/www/comps/builder/builderDocs.js b/www/comps/builder/builderDocs.js index 6958a0d9..b37a5155 100644 --- a/www/comps/builder/builderDocs.js +++ b/www/comps/builder/builderDocs.js @@ -33,6 +33,7 @@ let MyBuilderDocs = { return this.docs .replace(/href="#(.*?)"/g,'href="'+window.location+'#'+this.idPlaceholder+`$1`+'"') .replace(/id="(.*?)"/g,'id="'+this.idPlaceholder+`$1`+'"') + .replace(/src="(.*?)"/g,'src="docs/'+`$1`+'"') ; }, diff --git a/www/comps/builder/builderFieldColumns.js b/www/comps/builder/builderFieldColumns.js index 09b78dd6..7becf6f7 100644 --- a/www/comps/builder/builderFieldColumns.js +++ b/www/comps/builder/builderFieldColumns.js @@ -171,22 +171,24 @@ let MyBuilderFieldColumnOptions = { v-if="isSubQuery" @set-choices="setQuery('choices',$event)" @set-filters="setQuery('filters',$event)" + @set-fixed-limit="setQuery('fixedLimit',$event)" @set-joins="setQuery('joins',$event)" @set-lookups="setQuery('lookups',$event)" @set-orders="setQuery('orders',$event)" @set-relation-id="setQuery('relationId',$event)" - :allow-choices="false" - :allow-orders="true" - :builder-language="builderLanguage" + :allowChoices="false" + :allowOrders="true" + :builderLanguage="builderLanguage" :choices="column.query.choices" - :data-fields="dataFields" + :dataFields="dataFields" :filters="column.query.filters" + :fixedLimit="column.query.fixedLimit" :joins="column.query.joins" - :joins-parents="[joins]" + :joinsParents="[joins]" :lookups="column.query.lookups" - :module-id="moduleId" + :moduleId="moduleId" :orders="column.query.orders" - :relation-id="column.query.relationId" + :relationId="column.query.relationId" />
    `, props:{ @@ -249,9 +251,9 @@ let MyBuilderFieldColumnOptions = { else return this.$emit('set',name,0); }, setQuery:function(name,value) { - let query = JSON.parse(JSON.stringify(this.column.query)); - query[name] = value; - this.set('query',query); + let v = JSON.parse(JSON.stringify(this.column.query)); + v[name] = value; + this.set('query',v); }, setIndexAttribute:function(indexAttributeId) { let v = indexAttributeId.split('_'); diff --git a/www/comps/builder/builderFieldOptions.js b/www/comps/builder/builderFieldOptions.js index 980f8483..9bc999c9 100644 --- a/www/comps/builder/builderFieldOptions.js +++ b/www/comps/builder/builderFieldOptions.js @@ -510,21 +510,13 @@ let MyBuilderFieldOptions = { - + {{ capApp.gantt }} - - - - {{ capApp.ics }} - - @@ -555,7 +547,17 @@ let MyBuilderFieldOptions = { -