feature: the credential store keeps credentials of any kind in config.json with encrypted secrets, with its routes and its dialog.
Release / release (push) Successful in 1m4s
Release / release (push) Successful in 1m4s
This commit is contained in:
1 parent
57ac5296cb
commit
b0b7a4bb7f
13 files changed
+723
-30
No files matched your search
@@ -7,39 +7,43 @@ source: Node 26 runs the server helpers, and the application's Vite build compil
|
||||
## Using it
|
||||
|
||||
The package is `@koszewscy/sk-react-toolkit`, published to the npm registry of Gitea,
|
||||
`https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/`. An application imports it as
|
||||
`sk-react-toolkit`, by an alias. Its `.npmrc` names the registry of the scope:
|
||||
`https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/`. An application depends on a
|
||||
published version and imports `@koszewscy/sk-react-toolkit/server`, `/web` and `/vite`. Its
|
||||
`.npmrc` names the registry of the scope:
|
||||
|
||||
```ini
|
||||
@koszewscy:registry=https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/
|
||||
```
|
||||
|
||||
```json
|
||||
{ "dependencies": { "sk-react-toolkit": "npm:@koszewscy/sk-react-toolkit@^0.1.0" } }
|
||||
{ "dependencies": { "@koszewscy/sk-react-toolkit": "^0.2.0" } }
|
||||
```
|
||||
|
||||
While the toolkit and an application change together, the application depends on the checkout
|
||||
next to its own repository instead, the TypeScript source:
|
||||
|
||||
```json
|
||||
{ "dependencies": { "sk-react-toolkit": "file:../sk-react-toolkit" } }
|
||||
```
|
||||
|
||||
npm links the checkout into `node_modules`, and `npm ci` keeps the link. The web application adds
|
||||
the toolkit's resolve options and font plugin to its `vite.config.ts`, so React, MUI and Emotion
|
||||
come from its own `node_modules`, and allows the dev server to read the checkout:
|
||||
The web application adds the toolkit's resolve options and font plugin to its `vite.config.ts`, so
|
||||
React, MUI and Emotion come from its own `node_modules`:
|
||||
|
||||
```ts
|
||||
import { font, fontChoice, TOOLKIT_RESOLVE } from 'sk-react-toolkit/vite'
|
||||
import { font, fontChoice, TOOLKIT_RESOLVE } from '@koszewscy/sk-react-toolkit/vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [react(), font(fontChoice(loadEnv(mode, import.meta.dirname)))],
|
||||
resolve: TOOLKIT_RESOLVE,
|
||||
server: { fs: { allow: ['..', '../../sk-react-toolkit'] } },
|
||||
}))
|
||||
```
|
||||
|
||||
To change the toolkit and an application together, `npm link --no-save` replaces the installed
|
||||
package with the checkout, the TypeScript source, in each package of the application:
|
||||
|
||||
```sh
|
||||
npm link --no-save ../sk-react-toolkit
|
||||
npm --prefix web link --no-save ../../sk-react-toolkit
|
||||
```
|
||||
|
||||
`package.json` and the lock file keep the published version; `npm ci` or `npm install` replaces
|
||||
the link with it again. A change of the toolkit reaches the application through a release of the
|
||||
toolkit and an update of the dependency.
|
||||
|
||||
The peer dependencies are the application's: `fastify` and `@fastify/static` for the server
|
||||
helpers; `react`, `react-dom`, `@mui/material`, `@mui/icons-material` and Emotion for the web
|
||||
code; `vite` for the Vite helpers.
|
||||
@@ -57,6 +61,8 @@ code; `vite` for the Vite helpers.
|
||||
| `gitVersion(dir, variable)` | `git describe` of the checkout, else the environment variable, else `dev` |
|
||||
| `exitOnSignals(close)` | closes the server on Ctrl-C and SIGTERM, ending the process after 5 seconds at most |
|
||||
| `readJsonObject(path, options)` | a JSON object file and its version, the SHA-256 of its bytes; optionally `{}` for a missing file |
|
||||
| `encryptionKey()`, `encrypt(text, key)`, `decrypt(encrypted, key)` | the key of the secrets and AES-256-GCM encryption; see [Credential store](#credential-store) |
|
||||
| `CredentialStore`, `credentialProblems`, `credentialValues`, `registerCredentialRoutes`, `POSTGRESQL_KIND` | the credential store; see [Credential store](#credential-store) |
|
||||
|
||||
## sk-react-toolkit/web
|
||||
|
||||
@@ -71,6 +77,7 @@ code; `vite` for the Vite helpers.
|
||||
| `TextInput` | a text field with clear and show/hide buttons |
|
||||
| `RowDialog`, `useValidation` | a dialog that edits a value validated by the server as it changes, and the validation hook |
|
||||
| `createBrowserSettings(keys, event)` | switches kept in localStorage and followed across tabs |
|
||||
| `CredentialsButton` | the dialog of a credential store; see [Credential store](#credential-store) |
|
||||
|
||||
## sk-react-toolkit/vite
|
||||
|
||||
@@ -79,6 +86,62 @@ code; `vite` for the Vite helpers.
|
||||
| `font(choice)`, `fontChoice(env)`, `FONTS` | the font plugin of `VITE_FONT`: `cdn` (Google Fonts links, the default), `bundled` (`@fontsource/roboto`, a dependency of the application) or `none` |
|
||||
| `TOOLKIT_RESOLVE` | the resolve options that keep one copy of React, MUI and Emotion |
|
||||
|
||||
## Credential store
|
||||
|
||||
A credential store is a list of credentials in a key of the application's `config.json`, of the
|
||||
kinds the application gives, such as a database connection or a service principal. Each entry has
|
||||
a `name`, unique and not one of the store's reserved names, a `kind`, and the fields of its kind:
|
||||
|
||||
```json
|
||||
{
|
||||
"credentials": [
|
||||
{
|
||||
"name": "state",
|
||||
"kind": "postgresql",
|
||||
"host": "db.example.com",
|
||||
"port": 5432,
|
||||
"database": "platform",
|
||||
"user": "tofu",
|
||||
"ssl_mode": "verify-full",
|
||||
"password": "<encrypted>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A kind (`CredentialKind`) names its fields (`CredentialField`), each by its key in `config.json`,
|
||||
with a label, a help text, a type, `text`, `port`, `guid` or `choice` with its `choices`, whether
|
||||
it is required, and whether it is secret. `POSTGRESQL_KIND` is the kind of a PostgreSQL
|
||||
connection: `host`, `port`, `database`, `user`, `ssl_mode` (`disable`, `require` or
|
||||
`verify-full`) and a secret `password`.
|
||||
|
||||
A secret field is encrypted with AES-256-GCM, stored as base64 of a random IV, the ciphertext and
|
||||
the authentication tag. The key is the SHA-256 of `ENCRYPTION_KEY`, else of the content of the
|
||||
file `ENCRYPTION_KEY_FILE` names, without its final newline. Without either, no secret can be
|
||||
saved or used; a secret encrypted with another key cannot be decrypted and has to be entered
|
||||
again.
|
||||
|
||||
`CredentialStore` describes a store: the path of `config.json`, the key, the kinds, the reserved
|
||||
names and whether a missing `config.json` reads as `{}`.
|
||||
|
||||
- `credentialProblems(value, store)`: the problems of the key's value by `<key>.<index>.<field>`,
|
||||
for the application's own check of `config.json`.
|
||||
- `credentialValues(store, name)`: a credential with its secrets decrypted; HttpError 404 for a
|
||||
name that does not exist, 400 without a key or for a secret the key does not decrypt.
|
||||
- `registerCredentialRoutes(app, store, prefix, log)`: `GET <prefix>/read`, `POST
|
||||
<prefix>/validate` and `POST <prefix>/write`. Read answers the credentials, `CredentialProfile`,
|
||||
with `values` (the fields that are not secret, as text), `secrets` (null for each secret, never
|
||||
answered) and `stored` (which secrets are kept), the kinds, the reserved names, the path of
|
||||
`config.json`, its version and whether the server has a key. A write takes the version read and
|
||||
the credentials; a secret is null to keep the secret of the credential named `original`, `''`
|
||||
for none, or the new secret, which is encrypted. A file that changed since the version read is
|
||||
409, credentials with problems 422; the other keys of `config.json` are kept.
|
||||
|
||||
`CredentialsButton` (`prefix`, `title`, `icon`, `note`, `onSaved`) opens the store's credentials in
|
||||
a dialog: a list at the left with Add for each kind and Remove, and the fields of the credential
|
||||
selected, as its kind names them. A secret shows only whether it is kept: Set or Change replaces
|
||||
it and Remove drops it.
|
||||
|
||||
## Development
|
||||
|
||||
`npm install` installs the peer dependencies as development dependencies; `npm run typecheck`
|
||||
|
||||
Reference in new issue
Block a user