@ti-engine/web-framework 1.20.1 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +52 -0
- package/bin/build/hash-password.js +36 -0
- package/bin/config/local-users.example.json +8 -0
- package/bin/localization/web-server-labels.json +165 -1
- package/bin/static/fragments/components/component-sidebar.html +25 -0
- package/bin/static/fragments/frame-about.html +96 -0
- package/bin/static/fragments/frame-login.html +6 -1
- package/bin/static/fragments/frame-profile.html +103 -3
- package/bin/static/scripts/ti-framework.css +143 -0
- package/bin/static/scripts/ti-framework.js +339 -0
- package/bin/web-app-manager.js +235 -2
- package/bin/web-server.js +18 -1
- package/bin/web-server.json +3 -0
- package/components/application-info.js +190 -0
- package/components/auth-manager.js +159 -18
- package/components/definitions.types.js +65 -0
- package/components/local-user-directory.js +428 -0
- package/components/web-config-env.js +6 -1
- package/components/web-handlers.js +10 -2
- package/package.json +15 -2
- package/types/bin/web-app-manager.d.ts +95 -1
- package/types/bin/web-server.d.ts +19 -1
- package/types/components/application-info.d.ts +53 -0
- package/types/components/auth-manager.d.ts +7 -0
- package/types/components/definitions.types.d.ts +166 -0
- package/types/components/local-user-directory.d.ts +111 -0
- package/types/components/web-config-env.d.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,98 @@
|
|
|
2
2
|
|
|
3
3
|
This document will contain the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
|
4
4
|
|
|
5
|
+
## Version 1.23.0
|
|
6
|
+
|
|
7
|
+
Local (username/password) authentication is real. It had never been implemented: the constructor overwrote whatever
|
|
8
|
+
was configured with `admin`/`admin` behind a "for testing purposes only" TODO, the check was a plain `===` on both
|
|
9
|
+
fields, and the session user it produced carried no email — which since `competence` began resolving identity by
|
|
10
|
+
email meant a local sign-in could not reach an application at all.
|
|
11
|
+
|
|
12
|
+
* feat(local-user-directory): new `#local-user-directory` module — a JSON file of user records loaded on boot and
|
|
13
|
+
reconciled into Redis under `ti:web:auth:local-users`. Records carry `username`, `email`, `name` and a
|
|
14
|
+
`passwordHash`; `email` is required, because it is the field a consuming application resolves an identity by. The
|
|
15
|
+
file is the source of truth: a boot reconcile adds, updates and **removes**, so deleting a user revokes access
|
|
16
|
+
* feat(local-user-directory): scrypt password hashing via `node:crypto` — no new dependency — with a per-user random
|
|
17
|
+
salt and the cost parameters recorded in each hash, so they can be raised later without invalidating existing
|
|
18
|
+
hashes. Verification is timing-safe, and an unknown username still performs a hash computation so the login form
|
|
19
|
+
is not a username-enumeration oracle
|
|
20
|
+
* feat(auth-manager)!: **the hardcoded `admin`/`admin` pair is gone.** Local sign-ins are verified against the
|
|
21
|
+
directory, and `authorize()` returns a `User` carrying `userID`, `username`, `email` and `name` instead of a
|
|
22
|
+
random-UUID stub. Any deployment relying on the hardcoded credentials must provision a users file
|
|
23
|
+
* fix(auth-manager): a local user's `userID` is stable across logins. It was a fresh UUID each time, so
|
|
24
|
+
`auth.admins` could never match a local user by userID — only by username
|
|
25
|
+
* feat(build): `npm run hash-password` generates a record's hash, reading the password from **stdin** rather than
|
|
26
|
+
argv, which would put it in shell history and in `ps`
|
|
27
|
+
* feat(web-config-env): `TI_WEB_AUTH_LOCAL_USERS_PATH` overrides `auth.local.usersPath`
|
|
28
|
+
* fix(auth-manager): close a fail-open found by the whole-branch review — `#authenticateLocal` and `authorize()`
|
|
29
|
+
consulted the Redis-backed directory directly, so a users file that failed to load (missing, unreadable, or
|
|
30
|
+
unconfigured) still authenticated against records reconciled by an **earlier successful boot**, even while
|
|
31
|
+
logging that every local sign-in would be refused. A new `#localDirectoryUsable` flag is required by both
|
|
32
|
+
before any lookup, and is set only after a load that reconciled at least one record. `authorize()` also now
|
|
33
|
+
refuses a `disabled` record on its own, rather than relying on `authenticate()` having already been called
|
|
34
|
+
* fix(auth-manager): a Redis error during directory reconcile is now logged with only its `message`/`code` —
|
|
35
|
+
the raw ioredis error carries the failed command's full arguments, which for this call includes every user's
|
|
36
|
+
salt and scrypt hash, so logging it verbatim printed the entire directory's credential material at WARNING level
|
|
37
|
+
* build(release): bump package version from `1.22.0` to `1.23.0`
|
|
38
|
+
|
|
39
|
+
**Not included, and required before `local` is the sole method on an internet-facing deployment:** rate limiting,
|
|
40
|
+
lockout after repeated failures, and password policy.
|
|
41
|
+
|
|
42
|
+
## Version 1.22.0
|
|
43
|
+
|
|
44
|
+
An application may now refuse a sign-in from its `augmentSession` hook, and that refusal is genuinely fail-closed.
|
|
45
|
+
Sign-in failures also present identically across every auth method, which local (username/password) auth needs before
|
|
46
|
+
it can be offered as a production option.
|
|
47
|
+
|
|
48
|
+
* fix(web-handlers): destroy the session when an augment hook throws. `session.user` is assigned in place before the
|
|
49
|
+
hook runs and `verifySession` only checks that it exists, so a merely-rejected session was still persisted by
|
|
50
|
+
express-session at response end and would have admitted the refused user
|
|
51
|
+
* feat(web-server): document the `augmentSession` refusal contract — throwing refuses the login, destroys the session,
|
|
52
|
+
and redirects to the login page carrying the exception code
|
|
53
|
+
* feat(web-handlers): redirect any HTML-accepting, non-HTMX **401** to `/?error=<code>`, not only `GET` requests, so a
|
|
54
|
+
local-auth POST failure presents exactly like an OAuth callback failure. Non-401 responses are unaffected
|
|
55
|
+
* feat(web-app): render the sign-in failure message on the login page. `#ti-error` was an empty element and the
|
|
56
|
+
`getUrlParam` helper had no call sites, so a failed sign-in previously returned a blank login form
|
|
57
|
+
* feat(exports): expose the authorization helpers as `@ti-engine/web-framework/authorization`, so an application can
|
|
58
|
+
reuse `isAdminIdentity` for its own allowlist decisions instead of reimplementing the match
|
|
59
|
+
* build(release): bump package version from `1.21.0` to `1.22.0`
|
|
60
|
+
|
|
61
|
+
## Version 1.21.0
|
|
62
|
+
|
|
63
|
+
Two read-only screens the framework now provides for every consumer: **Profile** — which has been a registered
|
|
64
|
+
fragment rendering a two-line placeholder since the shell was written — and a new **About**, so a running instance
|
|
65
|
+
can finally answer "which build is this?" without shell access. Both are the same kind of screen (facts, grouped
|
|
66
|
+
into labelled sections), so the framework owns the screen — fragment, Alpine component, CSS — and an application
|
|
67
|
+
supplies only the content, through two virtual descriptor methods. Design record:
|
|
68
|
+
`docs/superpowers/specs/2026-08-13-profile-and-about-screens-design.md` (CA-99).
|
|
69
|
+
|
|
70
|
+
* feat(web-application): add `getProfileInfo( session )` and `getApplicationInfo( session )` — virtual methods
|
|
71
|
+
returning a display-ready `{ identity, sections }` descriptor, dispatched from `processDataRequest` for the
|
|
72
|
+
`profile` and `about` views. A subclass overrides one or both and inherits the entire screen. Every string in a
|
|
73
|
+
descriptor is resolved server-side, where the session language and the label catalogue are
|
|
74
|
+
* feat(application-info): new `#application-info` module — the pure `buildApplicationInfo()` normalizes a
|
|
75
|
+
`package.json`-shaped manifest (display name derived from the package name, author contact stripped, a `git+….git`
|
|
76
|
+
repository URL reduced to a browser-openable homepage) and applies the `TI_WEB_APP_NAME` / `TI_WEB_APP_VERSION` /
|
|
77
|
+
`TI_WEB_APP_RELEASE_DATE` overrides; `readApplicationManifest()` is the one impure half and returns `{}` rather
|
|
78
|
+
than throwing, so a missing or malformed manifest cannot take a request down
|
|
79
|
+
* feat(web-application): register the `about` fragment, and add `buildComponentsConfig( session )` supplying a
|
|
80
|
+
default sidebar user menu (Profile · About · sign-out) so a consuming application gets a working user menu without
|
|
81
|
+
configuring one
|
|
82
|
+
* feat(static): new `frame-about.html` and a rebuilt `frame-profile.html` — content-free renderers over the
|
|
83
|
+
descriptor — plus the `tiScreenProfile` / `tiScreenAbout` Alpine components. The About screen's release, component
|
|
84
|
+
and runtime sections are assembled client-side, where `getLabel` takes a fallback, which is what keeps them
|
|
85
|
+
readable inside an application that loads only its own label catalogue
|
|
86
|
+
* feat(static): add the reusable `.ti-identity-*` / `.ti-info-*` CSS primitives (identity header with avatar or app
|
|
87
|
+
mark, and the two-up grid of label/value sections), plus `.ti-kv-value.mono` / `.mono`-and-`.muted` modifiers
|
|
88
|
+
* feat(localization): add `interface.profile.*`, `interface.about.*`, `interface.topbar.profile|about` and
|
|
89
|
+
`interface.user-menu.*` defaults in en/bg
|
|
90
|
+
* fix(web-application): a framework-owned screen that resolves its own strings server-side would render the
|
|
91
|
+
not-found placeholder inside a consuming application, because an application configures exactly one labels path —
|
|
92
|
+
its own — and never loads `web-server-labels.json`. Every such lookup now passes a readable English literal as the
|
|
93
|
+
`getLabel` fallback added in `@ti-engine/core` 1.10.0 (**requires it**), rather than comparing the result against
|
|
94
|
+
a hard-coded copy of core's placeholder string
|
|
95
|
+
* build(release): bump package version from `1.20.1` to `1.21.0`
|
|
96
|
+
|
|
5
97
|
## Version 1.20.1
|
|
6
98
|
|
|
7
99
|
* fix(types): emit `/// <reference types="node" />` into `web-server.d.ts`, which names `node:http`.
|
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ The web server configuration (host, port, TLS, cookies, etc.) is normally provid
|
|
|
18
18
|
* `TI_WEB_TLS_CERT_PATH` / `TI_WEB_TLS_KEY_PATH` override the TLS certificate/key paths (only used when TLS is enabled).
|
|
19
19
|
* `TI_WEB_COOKIE_SECRET` sets the session cookie signing secret. Set a stable, private value for durable sessions and multi-replica deployments (otherwise a random per-process value is used).
|
|
20
20
|
* `TI_WEB_AUTH_METHODS` (comma-separated) **replaces** the enabled authentication methods (`auth.enabledMethods`), e.g. `openid-google` or `local,openid-google`.
|
|
21
|
+
* `TI_WEB_AUTH_LOCAL_USERS_PATH` overrides the local user directory's file path (`auth.local.usersPath`), which backs `local` sign-in. An explicitly empty value means *no directory*, so every local sign-in is refused. See [Local (username/password) authentication](#local-usernamepassword-authentication).
|
|
21
22
|
* `TI_WEB_AUTH_ADMINS` (comma-separated) **replaces** the admin allowlist (`auth.admins`). Entries are matched against the session user's user ID, username or email, so an OpenID deployment lists emails. An explicitly empty value means *no admins*.
|
|
22
23
|
* `TI_WEB_TRUSTED_ORIGINS` (comma-separated) **replaces** the trusted request origins (`trustedOrigins`) — needed behind proxies that do not present the real external origin.
|
|
23
24
|
* `TI_WEB_STATIC_MAX_AGE` (whole seconds) overrides `staticCache.maxAge`. See [Static asset caching](#static-asset-caching).
|
|
@@ -26,6 +27,57 @@ The web server configuration (host, port, TLS, cookies, etc.) is normally provid
|
|
|
26
27
|
|
|
27
28
|
OpenID Connect providers are configured with their own variables — `TI_AZURE_AUTH_CLIENT_ID` / `TI_AZURE_AUTH_CLIENT_SECRET` / `TI_AZURE_AUTH_CALLBACK_URL` / `TI_AZURE_AUTH_DISCOVERY_URL`, and the `TI_GCLOUD_AUTH_*` equivalents. A callback URL may be given either as the full absolute URL registered with the provider (`https://your-host/login/azure-callback`) or as a path (`/login/azure-callback`): the server always listens on the path, while the `redirect_uri` sent to the provider is the absolute value verbatim if one was configured, and otherwise assembled from the request's forwarded protocol/host.
|
|
28
29
|
|
|
30
|
+
## Authentication and authorization
|
|
31
|
+
|
|
32
|
+
`TiWebServer#augmentSession` is the hook through which an application derives its own session roles — from an identity store, the org chart, or wherever a deployment keeps that mapping — once per login, before the framework's own additive `admin` role (`auth.admins`, see [Environment variables](#environment-variables)) is applied on top; the default is a no-op that returns the session unchanged. Throwing from the hook refuses the sign-in rather than admitting a session the application could not map to a principal: the framework destroys the freshly regenerated session so nothing usable survives the refusal, the login handler responds `401`, and the error handler sends the browser back to the login page with the exception code in the `?error=` query parameter — the same path a failed OpenID callback takes, regardless of which auth method was used.
|
|
33
|
+
|
|
34
|
+
## Local (username/password) authentication
|
|
35
|
+
|
|
36
|
+
`local` is one of the configurable `auth.enabledMethods` sign-in methods (see [Environment variables](#environment-variables), `TI_WEB_AUTH_METHODS`). It is backed by a JSON file of user records — there is no built-in account of any kind.
|
|
37
|
+
|
|
38
|
+
### The users file
|
|
39
|
+
|
|
40
|
+
`auth.local.usersPath` (override: `TI_WEB_AUTH_LOCAL_USERS_PATH`) points at a JSON file holding an array of records:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
"username": "jdoe",
|
|
46
|
+
"email": "jane.doe@example.com",
|
|
47
|
+
"name": "Jane Doe",
|
|
48
|
+
"passwordHash": "scrypt$16384$8$1$<salt-base64>$<hash-base64>"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
* `username`, `email`, `name` and `passwordHash` are required. `email` is required because a consuming application resolves the signed-in identity by it, the same way it would for an OpenID identity — a record with no email cannot reach an application at all.
|
|
54
|
+
* `userID` is optional. When omitted, one is derived from the username, so it stays stable across restarts and logins; supply it explicitly only when something else needs to match a specific value (e.g. `auth.admins`).
|
|
55
|
+
* `disabled: true` keeps the record (and its username) in the file while refusing every sign-in for it.
|
|
56
|
+
|
|
57
|
+
Generate `passwordHash` with the bundled CLI. It reads the password from **stdin**, never an argument, so it never lands in shell history or a process listing (`ps`), and it never echoes the password back:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm run hash-password -w @ti-engine/web-framework
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Type or pipe the password, then EOF; only the resulting hash is written to stdout.
|
|
64
|
+
|
|
65
|
+
That `npm run` form only works inside this monorepo (it is a workspace script). A consumer of the published `@ti-engine/web-framework` package has no `bin` entry to run it by name — the script ships under `bin/build/` regardless, so invoke it by its path inside `node_modules` instead:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node ./node_modules/@ti-engine/web-framework/bin/build/hash-password.js
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### The file is the source of truth
|
|
72
|
+
|
|
73
|
+
On every boot, the file is read and reconciled into the running directory: an added record starts working, a changed `passwordHash` takes effect, and — this is the point — **a record removed from the file is removed from the directory**, revoking that user's access on the next restart. Editing the file and restarting is the whole revocation mechanism; there is no separate delete action.
|
|
74
|
+
|
|
75
|
+
### Every failure refuses rather than admits
|
|
76
|
+
|
|
77
|
+
`local` enabled with no `auth.local.usersPath` configured, a file that cannot be read, a file that is not valid JSON, or a file that yields zero valid records after validation — each of these logs a startup **WARNING** and refuses every local sign-in, rather than admitting one or falling back to a default. A failed *read* deliberately does not reconcile, so a temporarily broken volume mount leaves previously stored records untouched instead of wiping them; those records stay inert (unused) while the load keeps failing, because sign-ins are refused anyway.
|
|
78
|
+
|
|
79
|
+
**There is no rate limiting, no lockout after repeated failures, and no password policy.** Treat `local` on an internet-facing deployment as a deliberate risk until those exist.
|
|
80
|
+
|
|
29
81
|
## Static asset caching
|
|
30
82
|
|
|
31
83
|
Everything under `/static` is served with a `Cache-Control` policy configured by the `staticCache` block:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
|
|
3
|
+
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
|
+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
|
+
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a password hash for a local-users file entry.
|
|
13
|
+
*
|
|
14
|
+
* Usage: npm run hash-password -w @ti-engine/web-framework
|
|
15
|
+
*
|
|
16
|
+
* The password is read from stdin, never from an argument: an argv value lands in shell history and is visible to
|
|
17
|
+
* every other user on the machine through `ps`. Only the resulting hash is written to stdout — the password itself
|
|
18
|
+
* is never echoed, logged, or written to a file by this tool.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const directory = require( "#local-user-directory" );
|
|
22
|
+
|
|
23
|
+
let input = "";
|
|
24
|
+
process.stdin.setEncoding( "utf8" );
|
|
25
|
+
process.stdin.on( "data", ( chunk ) => {
|
|
26
|
+
input += chunk;
|
|
27
|
+
} );
|
|
28
|
+
process.stdin.on( "end", () => {
|
|
29
|
+
// Strip only the trailing newline a shell or editor adds; a password may legitimately contain spaces.
|
|
30
|
+
const password = input.replace( /\r?\n$/, "" );
|
|
31
|
+
if ( password.length === 0 ) {
|
|
32
|
+
process.stderr.write( "hash-password: no password on stdin\n" );
|
|
33
|
+
process.exit( 1 );
|
|
34
|
+
}
|
|
35
|
+
process.stdout.write( directory.hashPassword( password ) + "\n" );
|
|
36
|
+
} );
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
"interface": {
|
|
3
3
|
"default": {
|
|
4
4
|
"login": {
|
|
5
|
+
"error-sign-in-failed": {
|
|
6
|
+
"en": "We couldn't sign you in. Check your credentials, or contact your administrator if your account is not set up for this application.",
|
|
7
|
+
"bg": "Неуспешно влизане. Проверете данните си за достъп или се свържете с администратора, ако акаунтът ви не е настроен за това приложение."
|
|
8
|
+
},
|
|
5
9
|
"password": {
|
|
6
10
|
"en": "Password",
|
|
7
11
|
"bg": "Парола"
|
|
@@ -23,6 +27,166 @@
|
|
|
23
27
|
"bg": "Потребител"
|
|
24
28
|
}
|
|
25
29
|
}
|
|
30
|
+
},
|
|
31
|
+
"topbar": {
|
|
32
|
+
"profile": {
|
|
33
|
+
"en": "My Profile",
|
|
34
|
+
"bg": "Моят профил"
|
|
35
|
+
},
|
|
36
|
+
"about": {
|
|
37
|
+
"en": "About",
|
|
38
|
+
"bg": "Информация"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"user-menu": {
|
|
42
|
+
"profile": {
|
|
43
|
+
"en": "Your profile",
|
|
44
|
+
"bg": "Вашият профил"
|
|
45
|
+
},
|
|
46
|
+
"about": {
|
|
47
|
+
"en": "About",
|
|
48
|
+
"bg": "Информация"
|
|
49
|
+
},
|
|
50
|
+
"logout": {
|
|
51
|
+
"en": "Logout",
|
|
52
|
+
"bg": "Изход"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"profile": {
|
|
56
|
+
"eyebrow": {
|
|
57
|
+
"en": "Account",
|
|
58
|
+
"bg": "Профил"
|
|
59
|
+
},
|
|
60
|
+
"title": {
|
|
61
|
+
"en": "My Profile",
|
|
62
|
+
"bg": "Моят профил"
|
|
63
|
+
},
|
|
64
|
+
"intro": {
|
|
65
|
+
"en": "Your information as it is recorded in the system. This screen is read-only — to have something corrected, contact your manager.",
|
|
66
|
+
"bg": "Вашата информация, както е записана в системата. Екранът е само за преглед — за корекция се обърнете към вашия мениджър."
|
|
67
|
+
},
|
|
68
|
+
"empty-title": {
|
|
69
|
+
"en": "Profile unavailable",
|
|
70
|
+
"bg": "Профилът е недостъпен"
|
|
71
|
+
},
|
|
72
|
+
"empty-desc": {
|
|
73
|
+
"en": "Your profile information could not be loaded. Try again in a moment.",
|
|
74
|
+
"bg": "Информацията за профила не можа да бъде заредена. Опитайте отново след малко."
|
|
75
|
+
},
|
|
76
|
+
"section-account": {
|
|
77
|
+
"en": "Account",
|
|
78
|
+
"bg": "Профил"
|
|
79
|
+
},
|
|
80
|
+
"section-access": {
|
|
81
|
+
"en": "Access",
|
|
82
|
+
"bg": "Достъп"
|
|
83
|
+
},
|
|
84
|
+
"field-name": {
|
|
85
|
+
"en": "Full name",
|
|
86
|
+
"bg": "Име"
|
|
87
|
+
},
|
|
88
|
+
"field-username": {
|
|
89
|
+
"en": "Username",
|
|
90
|
+
"bg": "Потребител"
|
|
91
|
+
},
|
|
92
|
+
"field-email": {
|
|
93
|
+
"en": "E-mail",
|
|
94
|
+
"bg": "Имейл"
|
|
95
|
+
},
|
|
96
|
+
"field-user-id": {
|
|
97
|
+
"en": "User ID",
|
|
98
|
+
"bg": "Потребителски ID"
|
|
99
|
+
},
|
|
100
|
+
"field-language": {
|
|
101
|
+
"en": "Language",
|
|
102
|
+
"bg": "Език"
|
|
103
|
+
},
|
|
104
|
+
"field-roles": {
|
|
105
|
+
"en": "Roles",
|
|
106
|
+
"bg": "Роли"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"about": {
|
|
110
|
+
"eyebrow": {
|
|
111
|
+
"en": "Application",
|
|
112
|
+
"bg": "Приложение"
|
|
113
|
+
},
|
|
114
|
+
"title": {
|
|
115
|
+
"en": "About",
|
|
116
|
+
"bg": "Информация"
|
|
117
|
+
},
|
|
118
|
+
"intro": {
|
|
119
|
+
"en": "The application you are running, the release it is built from, and the ti-engine components behind it.",
|
|
120
|
+
"bg": "Приложението, което използвате, версията, от която е изградено, и компонентите на ti-engine зад него."
|
|
121
|
+
},
|
|
122
|
+
"empty-title": {
|
|
123
|
+
"en": "Details unavailable",
|
|
124
|
+
"bg": "Информацията е недостъпна"
|
|
125
|
+
},
|
|
126
|
+
"empty-desc": {
|
|
127
|
+
"en": "The application details could not be loaded. Try again in a moment.",
|
|
128
|
+
"bg": "Информацията за приложението не можа да бъде заредена. Опитайте отново след малко."
|
|
129
|
+
},
|
|
130
|
+
"released": {
|
|
131
|
+
"en": "Released",
|
|
132
|
+
"bg": "Издадена на"
|
|
133
|
+
},
|
|
134
|
+
"section-release": {
|
|
135
|
+
"en": "Release",
|
|
136
|
+
"bg": "Версия"
|
|
137
|
+
},
|
|
138
|
+
"section-components": {
|
|
139
|
+
"en": "Framework components",
|
|
140
|
+
"bg": "Компоненти на платформата"
|
|
141
|
+
},
|
|
142
|
+
"section-components-desc": {
|
|
143
|
+
"en": "The ti-engine packages this application is built on.",
|
|
144
|
+
"bg": "Пакетите на ti-engine, върху които е изградено приложението."
|
|
145
|
+
},
|
|
146
|
+
"section-runtime": {
|
|
147
|
+
"en": "Runtime",
|
|
148
|
+
"bg": "Среда"
|
|
149
|
+
},
|
|
150
|
+
"section-runtime-desc": {
|
|
151
|
+
"en": "Visible to administrators only.",
|
|
152
|
+
"bg": "Видимо само за администратори."
|
|
153
|
+
},
|
|
154
|
+
"field-version": {
|
|
155
|
+
"en": "Version",
|
|
156
|
+
"bg": "Версия"
|
|
157
|
+
},
|
|
158
|
+
"field-release-date": {
|
|
159
|
+
"en": "Release date",
|
|
160
|
+
"bg": "Дата на издаване"
|
|
161
|
+
},
|
|
162
|
+
"field-license": {
|
|
163
|
+
"en": "License",
|
|
164
|
+
"bg": "Лиценз"
|
|
165
|
+
},
|
|
166
|
+
"field-author": {
|
|
167
|
+
"en": "Author",
|
|
168
|
+
"bg": "Автор"
|
|
169
|
+
},
|
|
170
|
+
"field-package": {
|
|
171
|
+
"en": "Package",
|
|
172
|
+
"bg": "Пакет"
|
|
173
|
+
},
|
|
174
|
+
"field-homepage": {
|
|
175
|
+
"en": "Homepage",
|
|
176
|
+
"bg": "Уебсайт"
|
|
177
|
+
},
|
|
178
|
+
"runtime-node": {
|
|
179
|
+
"en": "Node.js",
|
|
180
|
+
"bg": "Node.js"
|
|
181
|
+
},
|
|
182
|
+
"runtime-platform": {
|
|
183
|
+
"en": "Platform",
|
|
184
|
+
"bg": "Платформа"
|
|
185
|
+
},
|
|
186
|
+
"runtime-application": {
|
|
187
|
+
"en": "Application ID",
|
|
188
|
+
"bg": "ID на приложението"
|
|
189
|
+
}
|
|
26
190
|
}
|
|
27
191
|
}
|
|
28
|
-
}
|
|
192
|
+
}
|
|
@@ -29,5 +29,30 @@
|
|
|
29
29
|
</span>
|
|
30
30
|
<span class="ti-sidebar-item-label" x-text="theme === 'daylight' ? 'Glass Theme' : 'Daylight Theme'">Toggle Theme</span>
|
|
31
31
|
</button>
|
|
32
|
+
|
|
33
|
+
<!--
|
|
34
|
+
User menu. Its entries come from `componentsConfig.userProfileMenu` in the /app/config payload, which
|
|
35
|
+
TiWebAppManager#buildComponentsConfig fills with the framework's own screens (Profile, About) plus
|
|
36
|
+
sign-out — so this menu works in a consuming application that configures nothing.
|
|
37
|
+
-->
|
|
38
|
+
<ti-component-sidebar-flyout-placeholder config-key="userProfileMenu">
|
|
39
|
+
<button class="ti-sidebar-user" type="button"
|
|
40
|
+
x-on:click.stop="toggle()"
|
|
41
|
+
x-on:ti-close-all-flyout="close()"
|
|
42
|
+
x-bind:aria-expanded="isOpen"
|
|
43
|
+
x-bind:aria-controls="$id( 'component-flyout' )"
|
|
44
|
+
x-ref="flyoutButton"
|
|
45
|
+
x-text-label:data-tip="interface.topbar.user-profile"
|
|
46
|
+
data-tip="Your profile">
|
|
47
|
+
<span class="ti-avatar sm"
|
|
48
|
+
x-bind:style="$store.tiToolbox.generateAvatarStyle( $store.tiApplication.user && $store.tiApplication.user.userID, $store.tiApplication.user && $store.tiApplication.user.name )"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
x-text="( ( $store.tiApplication.user && $store.tiApplication.user.name ) || '?' ).charAt( 0 ).toUpperCase()">?</span>
|
|
51
|
+
<span class="ti-sidebar-user-text">
|
|
52
|
+
<span class="ti-sidebar-user-name" x-text="( $store.tiApplication.user && $store.tiApplication.user.name ) || ''"></span>
|
|
53
|
+
<span class="ti-sidebar-user-sub" x-text="( $store.tiApplication.user && $store.tiApplication.user.email ) || ''"></span>
|
|
54
|
+
</span>
|
|
55
|
+
</button>
|
|
56
|
+
</ti-component-sidebar-flyout-placeholder>
|
|
32
57
|
</div>
|
|
33
58
|
</nav>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
About screen — what this application is and which build of it you are looking at.
|
|
3
|
+
|
|
4
|
+
Like the Profile screen, this is a content-free renderer over a server descriptor: the identity block comes
|
|
5
|
+
from the top-level fields of TiWebAppManager#getApplicationInfo, and everything below it is the normalized
|
|
6
|
+
section list tiScreenAbout assembles (release facts, ti-engine component versions, admin-only runtime facts,
|
|
7
|
+
then whatever sections the application contributed).
|
|
8
|
+
|
|
9
|
+
Alpine runs in CSP mode: no inline style attributes and no optional chaining in expressions.
|
|
10
|
+
-->
|
|
11
|
+
<div class="ti-page" x-data="tiScreenAbout">
|
|
12
|
+
|
|
13
|
+
<div class="ti-page-head">
|
|
14
|
+
<div class="ti-page-eyebrow" x-text-label="interface.about.eyebrow">Application</div>
|
|
15
|
+
<h1 class="ti-page-title" x-text-label="interface.about.title">About</h1>
|
|
16
|
+
<p class="ti-page-subtitle" x-text-label="interface.about.intro">The application you are running, the release
|
|
17
|
+
it is built from, and the ti-engine components behind it.</p>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<template x-if="!busy && !application">
|
|
21
|
+
<div class="ti-panel">
|
|
22
|
+
<div class="ti-empty-state">
|
|
23
|
+
<div class="ti-empty-state-icon" aria-hidden="true">
|
|
24
|
+
<span class="ti-icon info-circle xl"></span>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="ti-empty-state-title" x-text-label="interface.about.empty-title">Details unavailable</div>
|
|
27
|
+
<div class="ti-empty-state-desc" x-text-label="interface.about.empty-desc">The application details
|
|
28
|
+
could not be loaded. Try again in a moment.</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<template x-if="application">
|
|
34
|
+
<div>
|
|
35
|
+
|
|
36
|
+
<!-- Identity header — app mark, name, version pill and the one-line description. -->
|
|
37
|
+
<section class="ti-panel">
|
|
38
|
+
<div class="ti-identity">
|
|
39
|
+
<div class="ti-identity-mark" aria-hidden="true">
|
|
40
|
+
<span x-text="applicationInitial()"></span>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="ti-identity-meta">
|
|
43
|
+
<div class="ti-identity-name">
|
|
44
|
+
<span x-text="application.name"></span>
|
|
45
|
+
<span class="ti-tag mono" x-show="application.version"
|
|
46
|
+
x-text="versionTag()"></span>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="ti-identity-caption" x-show="application.description"
|
|
49
|
+
x-text="application.description"></div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="ti-identity-aside">
|
|
52
|
+
<span class="ti-status-pill muted" x-show="application.releaseDate">
|
|
53
|
+
<span class="ti-icon calendar-blank xs" aria-hidden="true"></span>
|
|
54
|
+
<span x-text="releaseDateText()"></span>
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<!-- Sections — release facts, components, runtime (admin only), then application-contributed ones. -->
|
|
61
|
+
<div class="ti-info-sections">
|
|
62
|
+
<template x-for="(section, sectionIndex) in sections" x-bind:key="sectionIndex">
|
|
63
|
+
<section class="ti-panel ti-info-section" x-bind:class="section.wide ? 'wide' : ''">
|
|
64
|
+
<div class="ti-panel-head bar">
|
|
65
|
+
<div class="ti-panel-head-icon">
|
|
66
|
+
<span class="ti-icon md" x-bind:class="section.icon" aria-hidden="true"></span>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="ti-panel-head-text">
|
|
69
|
+
<div class="ti-panel-title" x-text="section.title"></div>
|
|
70
|
+
<div class="ti-panel-subtitle" x-show="section.description"
|
|
71
|
+
x-text="section.description"></div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="ti-info-grid">
|
|
75
|
+
<template x-for="(item, itemIndex) in section.items" x-bind:key="itemIndex">
|
|
76
|
+
<div class="ti-info-item" x-bind:class="item.wide ? 'wide' : ''">
|
|
77
|
+
<span class="ti-kv-label" x-text="item.label"></span>
|
|
78
|
+
<template x-if="item.href">
|
|
79
|
+
<a class="ti-kv-value ti-info-link" x-bind:href="item.href"
|
|
80
|
+
target="_blank" rel="noopener noreferrer" x-text="item.value"></a>
|
|
81
|
+
</template>
|
|
82
|
+
<template x-if="!item.href">
|
|
83
|
+
<span class="ti-kv-value" x-bind:class="item.valueClass"
|
|
84
|
+
x-text="item.value"></span>
|
|
85
|
+
</template>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
</template>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
</div>
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
<!-- Login card -->
|
|
13
13
|
<div class="ti-login-card">
|
|
14
14
|
<!-- Error message -->
|
|
15
|
-
<div id="ti-error"
|
|
15
|
+
<div id="ti-error"
|
|
16
|
+
class="ti-login-error"
|
|
17
|
+
x-data="tiLoginError"
|
|
18
|
+
x-bind:class="{ visible: hasError }"
|
|
19
|
+
role="alert"
|
|
20
|
+
x-text-label="interface.default.login.error-sign-in-failed"></div>
|
|
16
21
|
|
|
17
22
|
<!--ti-auth-method:local-->
|
|
18
23
|
<!-- Local auth form -->
|
|
@@ -1,3 +1,103 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<!--
|
|
2
|
+
Profile screen — a read-only view of the signed-in user's own information.
|
|
3
|
+
|
|
4
|
+
The markup is deliberately content-free: it renders the descriptor returned by
|
|
5
|
+
TiWebAppManager#getProfileInfo (`{ identity, sections }`), which the application overrides to decide WHAT is
|
|
6
|
+
shown while inheriting HOW it is shown. Every string in the descriptor arrives display-ready from the server,
|
|
7
|
+
so nothing here formats or translates data.
|
|
8
|
+
|
|
9
|
+
Alpine runs in CSP mode: no inline style attributes and no optional chaining in expressions — all shaping is
|
|
10
|
+
done in tiScreenProfile before the values reach the template.
|
|
11
|
+
-->
|
|
12
|
+
<div class="ti-page" x-data="tiScreenProfile">
|
|
13
|
+
|
|
14
|
+
<div class="ti-page-head">
|
|
15
|
+
<div class="ti-page-eyebrow" x-text-label="interface.profile.eyebrow">Account</div>
|
|
16
|
+
<h1 class="ti-page-title" x-text-label="interface.profile.title">My Profile</h1>
|
|
17
|
+
<p class="ti-page-subtitle" x-text-label="interface.profile.intro">Your information as it is recorded in the
|
|
18
|
+
system. This screen is read-only — to have something corrected, contact your manager.</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<template x-if="!busy && !profile">
|
|
22
|
+
<div class="ti-panel">
|
|
23
|
+
<div class="ti-empty-state">
|
|
24
|
+
<div class="ti-empty-state-icon" aria-hidden="true">
|
|
25
|
+
<span class="ti-icon user xl"></span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="ti-empty-state-title" x-text-label="interface.profile.empty-title">Profile unavailable</div>
|
|
28
|
+
<div class="ti-empty-state-desc" x-text-label="interface.profile.empty-desc">Your profile information
|
|
29
|
+
could not be loaded. Try again in a moment.</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<template x-if="profile">
|
|
35
|
+
<div>
|
|
36
|
+
|
|
37
|
+
<!-- Identity header — avatar, name, meta line and the status pills beside it. -->
|
|
38
|
+
<section class="ti-panel">
|
|
39
|
+
<div class="ti-identity">
|
|
40
|
+
<div class="ti-avatar xl" x-bind:style="avatarStyle()" aria-hidden="true">
|
|
41
|
+
<span x-text="avatarInitial()"></span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="ti-identity-meta">
|
|
44
|
+
<div class="ti-identity-name">
|
|
45
|
+
<span x-text="profile.identity.name"></span>
|
|
46
|
+
<template x-if="profile.identity.badge">
|
|
47
|
+
<span class="ti-tag" x-bind:class="profile.identity.badge.tone"
|
|
48
|
+
x-text="profile.identity.badge.text"></span>
|
|
49
|
+
</template>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="ti-identity-sub" x-show="profile.identity.subtitle"
|
|
52
|
+
x-text="profile.identity.subtitle"></div>
|
|
53
|
+
<div class="ti-identity-caption" x-show="profile.identity.caption"
|
|
54
|
+
x-text="profile.identity.caption"></div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="ti-identity-aside">
|
|
57
|
+
<template x-for="(tag, tagIndex) in profile.identity.tags" x-bind:key="tagIndex">
|
|
58
|
+
<span x-bind:class="tag.pillClass">
|
|
59
|
+
<span class="dot" x-show="tag.dot"></span>
|
|
60
|
+
<span x-text="tag.text"></span>
|
|
61
|
+
</span>
|
|
62
|
+
</template>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
|
|
67
|
+
<!-- Sections — one panel per titled group of label/value pairs. -->
|
|
68
|
+
<div class="ti-info-sections">
|
|
69
|
+
<template x-for="(section, sectionIndex) in profile.sections" x-bind:key="sectionIndex">
|
|
70
|
+
<section class="ti-panel ti-info-section" x-bind:class="section.wide ? 'wide' : ''">
|
|
71
|
+
<div class="ti-panel-head bar">
|
|
72
|
+
<div class="ti-panel-head-icon">
|
|
73
|
+
<span class="ti-icon md" x-bind:class="section.icon" aria-hidden="true"></span>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="ti-panel-head-text">
|
|
76
|
+
<div class="ti-panel-title" x-text="section.title"></div>
|
|
77
|
+
<div class="ti-panel-subtitle" x-show="section.description"
|
|
78
|
+
x-text="section.description"></div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="ti-info-grid">
|
|
82
|
+
<template x-for="(item, itemIndex) in section.items" x-bind:key="itemIndex">
|
|
83
|
+
<div class="ti-info-item" x-bind:class="item.wide ? 'wide' : ''">
|
|
84
|
+
<span class="ti-kv-label" x-text="item.label"></span>
|
|
85
|
+
<template x-if="item.href">
|
|
86
|
+
<a class="ti-kv-value ti-info-link" x-bind:href="item.href"
|
|
87
|
+
target="_blank" rel="noopener noreferrer" x-text="item.value"></a>
|
|
88
|
+
</template>
|
|
89
|
+
<template x-if="!item.href">
|
|
90
|
+
<span class="ti-kv-value" x-bind:class="item.valueClass"
|
|
91
|
+
x-text="item.value"></span>
|
|
92
|
+
</template>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
</template>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
|
|
103
|
+
</div>
|