mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Compare commits
6 Commits
c122b9ac90
...
1fd8e13668
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fd8e13668 | ||
|
|
ba52f201b9 | ||
|
|
d69b442019 | ||
|
|
53021a9da1 | ||
|
|
f8ff9f30fb | ||
|
|
f08004a4f1 |
91
DOCUMENTATION.md
Normal file
91
DOCUMENTATION.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Bonob Source Code Documentation
|
||||||
|
|
||||||
|
This document provides an overview of the source files in the `src` directory, explaining the purpose and functionality of each.
|
||||||
|
|
||||||
|
### `api_tokens.ts`
|
||||||
|
|
||||||
|
Manages API tokens for authentication. It includes an in-memory implementation for storing and retrieving tokens, using SHA256 to mint new tokens.
|
||||||
|
|
||||||
|
### `app.ts`
|
||||||
|
|
||||||
|
This is the main entry point of the application. It initializes the server, configures the music service (Subsonic), and sets up the integration with Sonos. It reads the application config, sets up the Subsonic connection, and starts the Express server.
|
||||||
|
|
||||||
|
### `b64.ts`
|
||||||
|
|
||||||
|
Provides simple utility functions for Base64 encoding and decoding of strings.
|
||||||
|
|
||||||
|
### `burn.ts`
|
||||||
|
|
||||||
|
Handles the creation and parsing of "Bonob URNs" (BURNs), which are unique resource identifiers used within the system. It supports encryption and shorthand notations for more compact URNs.
|
||||||
|
|
||||||
|
### `clock.ts`
|
||||||
|
|
||||||
|
Provides an abstraction for time-related functions, which is useful for testing. It includes a `SystemClock` that uses the actual system time and a `FixedClock` for tests. It also contains logic for detecting special dates like Christmas and Halloween for seasonal features.
|
||||||
|
|
||||||
|
### `config.ts`
|
||||||
|
|
||||||
|
Manages the application's configuration by reading environment variables. It defines settings for the server, Sonos integration, Subsonic connection, and other features like scrobbling.
|
||||||
|
|
||||||
|
### `encryption.ts`
|
||||||
|
|
||||||
|
Implements encryption and decryption functionality using JSON Web Signatures (JWS). It provides a simple interface for encrypting and decrypting strings.
|
||||||
|
|
||||||
|
### `i8n.ts`
|
||||||
|
|
||||||
|
Handles internationalization (i18n) by providing translations for different languages supported by the Sonos app. It includes translations for UI elements and messages.
|
||||||
|
|
||||||
|
### `icon.ts`
|
||||||
|
|
||||||
|
Manages SVG icons used in the Sonos app. It allows for transformations like changing colors and applying special styles for festivals and holidays.
|
||||||
|
|
||||||
|
### `link_codes.ts`
|
||||||
|
|
||||||
|
Implements a system for linking Sonos devices with user accounts. It generates temporary codes that users can use to log in and associate their accounts.
|
||||||
|
|
||||||
|
### `logger.ts`
|
||||||
|
|
||||||
|
Configures the application-wide logger using Winston. It sets up logging levels and formats.
|
||||||
|
|
||||||
|
### `music_service.ts`
|
||||||
|
|
||||||
|
Defines the interfaces for a generic music service. This includes methods for authentication, browsing content (artists, albums, tracks), streaming audio, and managing playlists.
|
||||||
|
|
||||||
|
### `register.ts`
|
||||||
|
|
||||||
|
A command-line script used to register the Bonob service with Sonos devices on the local network.
|
||||||
|
|
||||||
|
### `registrar.ts`
|
||||||
|
|
||||||
|
Contains the core logic for registering the Bonob service with Sonos devices. It fetches service details from the Bonob server and sends the registration request to a Sonos device.
|
||||||
|
|
||||||
|
### `server.ts`
|
||||||
|
|
||||||
|
Sets up the Express web server. It defines the routes for the web interface, the Sonos Music API (SMAPI) endpoints, and audio streaming. It also handles user authentication and session management.
|
||||||
|
|
||||||
|
### `smapi_auth.ts`
|
||||||
|
|
||||||
|
Handles authentication for the Sonos Music API (SMAPI). It is responsible for issuing and verifying JWTs (JSON Web Tokens) that secure the communication between Sonos devices and the Bonob server.
|
||||||
|
|
||||||
|
### `smapi_token_store.ts`
|
||||||
|
|
||||||
|
Provides an interface and two implementations (in-memory and file-based) for storing SMAPI authentication tokens. This allows the server to persist user sessions.
|
||||||
|
|
||||||
|
### `smapi.ts`
|
||||||
|
|
||||||
|
Implements the Sonos Music API (SMAPI) using SOAP. This file is responsible for handling all the requests from Sonos devices, such as browsing music, searching, and getting track metadata.
|
||||||
|
|
||||||
|
### `sonos.ts`
|
||||||
|
|
||||||
|
Manages interactions with Sonos devices on the local network. This includes device discovery and service registration.
|
||||||
|
|
||||||
|
### `subsonic.ts`
|
||||||
|
|
||||||
|
Implements the `MusicService` interface for Subsonic-compatible media servers (like Navidrome). It handles all communication with the Subsonic API to fetch music data and stream audio.
|
||||||
|
|
||||||
|
### `url_builder.ts`
|
||||||
|
|
||||||
|
A utility class for building and manipulating URLs in a structured way.
|
||||||
|
|
||||||
|
### `utils.ts`
|
||||||
|
|
||||||
|
Contains miscellaneous utility functions used throughout the application, such as a function for tidying up XML strings.
|
||||||
384
package-lock.json
generated
384
package-lock.json
generated
@@ -22,6 +22,7 @@
|
|||||||
"@types/xmldom": "^0.1.34",
|
"@types/xmldom": "^0.1.34",
|
||||||
"@xmldom/xmldom": "^0.9.7",
|
"@xmldom/xmldom": "^0.9.7",
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.7.8",
|
||||||
|
"better-sqlite3": "^12.4.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"eta": "^2.2.0",
|
"eta": "^2.2.0",
|
||||||
"express": "^4.18.3",
|
"express": "^4.18.3",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"xpath": "^0.0.34"
|
"xpath": "^0.0.34"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/chai": "^5.0.1",
|
"@types/chai": "^5.0.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "^10.0.10",
|
||||||
@@ -1592,6 +1594,16 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/better-sqlite3": {
|
||||||
|
"version": "7.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||||
|
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.5",
|
"version": "1.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||||
@@ -2178,6 +2190,26 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/basic-auth": {
|
"node_modules/basic-auth": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
@@ -2194,6 +2226,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
},
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "12.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
|
||||||
|
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
|
"prebuild-install": "^7.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20.x || 22.x || 23.x || 24.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
@@ -2203,6 +2249,26 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.5.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/blob-util": {
|
"node_modules/blob-util": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
|
||||||
@@ -2328,6 +2394,30 @@
|
|||||||
"node-int64": "^0.4.0"
|
"node-int64": "^0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-equal-constant-time": {
|
"node_modules/buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
@@ -2490,6 +2580,12 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/ci-info": {
|
"node_modules/ci-info": {
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||||
@@ -2772,6 +2868,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dedent": {
|
"node_modules/dedent": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz",
|
||||||
@@ -2795,6 +2906,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-extend": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deepmerge": {
|
"node_modules/deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
@@ -3024,6 +3144,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
@@ -3155,6 +3284,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expand-template": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||||
|
"license": "(MIT OR WTFPL)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expect": {
|
"node_modules/expect": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
|
||||||
@@ -3320,6 +3458,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||||
@@ -3485,6 +3629,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-constants": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "11.2.0",
|
"version": "11.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
||||||
@@ -3605,6 +3755,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/github-from-package": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
@@ -3799,6 +3955,26 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore-by-default": {
|
"node_modules/ignore-by-default": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||||
@@ -3897,6 +4073,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/iobuffer": {
|
"node_modules/iobuffer": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.3.2.tgz",
|
||||||
@@ -5193,6 +5375,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimalistic-assert": {
|
"node_modules/minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
@@ -5210,6 +5404,21 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mkdirp-classic": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ml-array-max": {
|
"node_modules/ml-array-max": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-1.2.4.tgz",
|
||||||
@@ -5482,6 +5691,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/napi-build-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
@@ -5508,6 +5723,30 @@
|
|||||||
"integrity": "sha512-+z6QY1SxkDk6CQJAeaIZKmcNubBCRP7J8DMQUBglz/sSkNsZoJ1kULjqk9skNPPplzs4i9PFhYrvNDdtQleF/A==",
|
"integrity": "sha512-+z6QY1SxkDk6CQJAeaIZKmcNubBCRP7J8DMQUBglz/sSkNsZoJ1kULjqk9skNPPplzs4i9PFhYrvNDdtQleF/A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/node-abi": {
|
||||||
|
"version": "3.78.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz",
|
||||||
|
"integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-abi/node_modules/semver": {
|
||||||
|
"version": "7.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
@@ -5898,6 +6137,32 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prebuild-install": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"expand-template": "^2.0.3",
|
||||||
|
"github-from-package": "0.0.0",
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"mkdirp-classic": "^0.5.3",
|
||||||
|
"napi-build-utils": "^2.0.0",
|
||||||
|
"node-abi": "^3.3.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"rc": "^1.2.7",
|
||||||
|
"simple-get": "^4.0.0",
|
||||||
|
"tar-fs": "^2.0.0",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prebuild-install": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-format": {
|
"node_modules/pretty-format": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||||
@@ -5960,6 +6225,16 @@
|
|||||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pure-rand": {
|
"node_modules/pure-rand": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz",
|
||||||
@@ -6034,6 +6309,30 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-extend": "^0.6.0",
|
||||||
|
"ini": "~1.3.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"strip-json-comments": "~2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rc": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rc/node_modules/strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
@@ -6417,6 +6716,51 @@
|
|||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-concat": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/simple-swizzle": {
|
"node_modules/simple-swizzle": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||||
@@ -6746,6 +7090,34 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/test-exclude": {
|
"node_modules/test-exclude": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||||
@@ -6990,6 +7362,18 @@
|
|||||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/two-product": {
|
"node_modules/two-product": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/two-product/-/two-product-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/two-product/-/two-product-1.0.2.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@types/xmldom": "^0.1.34",
|
"@types/xmldom": "^0.1.34",
|
||||||
"@xmldom/xmldom": "^0.9.7",
|
"@xmldom/xmldom": "^0.9.7",
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.7.8",
|
||||||
|
"better-sqlite3": "^12.4.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"eta": "^2.2.0",
|
"eta": "^2.2.0",
|
||||||
"express": "^4.18.3",
|
"express": "^4.18.3",
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
"xpath": "^0.0.34"
|
"xpath": "^0.0.34"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/chai": "^5.0.1",
|
"@types/chai": "^5.0.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "^10.0.10",
|
||||||
|
|||||||
16
src/app.ts
16
src/app.ts
@@ -18,7 +18,7 @@ import sonos, { bonobService } from "./sonos";
|
|||||||
import { MusicService } from "./music_service";
|
import { MusicService } from "./music_service";
|
||||||
import { SystemClock } from "./clock";
|
import { SystemClock } from "./clock";
|
||||||
import { JWTSmapiLoginTokens } from "./smapi_auth";
|
import { JWTSmapiLoginTokens } from "./smapi_auth";
|
||||||
import { FileSmapiTokenStore } from "./smapi_token_store";
|
import { SQLiteSmapiTokenStore } from "./smapi_token_store";
|
||||||
|
|
||||||
const config = readConfig();
|
const config = readConfig();
|
||||||
const clock = SystemClock;
|
const clock = SystemClock;
|
||||||
@@ -82,6 +82,16 @@ const version = fs.existsSync(GIT_INFO)
|
|||||||
? fs.readFileSync(GIT_INFO).toString().trim()
|
? fs.readFileSync(GIT_INFO).toString().trim()
|
||||||
: "v??";
|
: "v??";
|
||||||
|
|
||||||
|
// Initialize SQLite token store
|
||||||
|
const smapiTokenStore = new SQLiteSmapiTokenStore(config.tokenStore.dbPath);
|
||||||
|
|
||||||
|
// Migrate existing JSON tokens if they exist
|
||||||
|
const legacyJsonPath = "/config/tokens.json";
|
||||||
|
if (fs.existsSync(legacyJsonPath)) {
|
||||||
|
logger.info(`Found legacy JSON token file at ${legacyJsonPath}, attempting migration...`);
|
||||||
|
smapiTokenStore.migrateFromJSON(legacyJsonPath);
|
||||||
|
}
|
||||||
|
|
||||||
const app = server(
|
const app = server(
|
||||||
sonosSystem,
|
sonosSystem,
|
||||||
bonob,
|
bonob,
|
||||||
@@ -97,7 +107,8 @@ const app = server(
|
|||||||
version,
|
version,
|
||||||
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
||||||
externalImageResolver: artistImageFetcher,
|
externalImageResolver: artistImageFetcher,
|
||||||
smapiTokenStore: new FileSmapiTokenStore("/config/tokens.json")
|
smapiTokenStore,
|
||||||
|
tokenCleanupIntervalMinutes: config.tokenStore.cleanupIntervalMinutes
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -126,6 +137,7 @@ process.on('SIGTERM', () => {
|
|||||||
expressServer.close(() => {
|
expressServer.close(() => {
|
||||||
logger.info('HTTP server closed');
|
logger.info('HTTP server closed');
|
||||||
});
|
});
|
||||||
|
smapiTokenStore.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -105,5 +105,9 @@ export default function () {
|
|||||||
scrobbleTracks: bnbEnvVar<boolean>("SCROBBLE_TRACKS", { default: true, parser: asBoolean }),
|
scrobbleTracks: bnbEnvVar<boolean>("SCROBBLE_TRACKS", { default: true, parser: asBoolean }),
|
||||||
reportNowPlaying:
|
reportNowPlaying:
|
||||||
bnbEnvVar<boolean>("REPORT_NOW_PLAYING", { default: true, parser: asBoolean }),
|
bnbEnvVar<boolean>("REPORT_NOW_PLAYING", { default: true, parser: asBoolean }),
|
||||||
|
tokenStore: {
|
||||||
|
dbPath: bnbEnvVar<string>("TOKEN_DB_PATH", { default: "/config/tokens.db" })!,
|
||||||
|
cleanupIntervalMinutes: bnbEnvVar<number>("TOKEN_CLEANUP_INTERVAL", { default: 60, parser: asInt })!,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export type ServerOpts = {
|
|||||||
smapiAuthTokens: SmapiAuthTokens;
|
smapiAuthTokens: SmapiAuthTokens;
|
||||||
externalImageResolver: ImageFetcher;
|
externalImageResolver: ImageFetcher;
|
||||||
smapiTokenStore: SmapiTokenStore;
|
smapiTokenStore: SmapiTokenStore;
|
||||||
|
tokenCleanupIntervalMinutes: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_SERVER_OPTS: ServerOpts = {
|
const DEFAULT_SERVER_OPTS: ServerOpts = {
|
||||||
@@ -130,6 +131,7 @@ const DEFAULT_SERVER_OPTS: ServerOpts = {
|
|||||||
),
|
),
|
||||||
externalImageResolver: axiosImageFetcher,
|
externalImageResolver: axiosImageFetcher,
|
||||||
smapiTokenStore: new InMemorySmapiTokenStore(),
|
smapiTokenStore: new InMemorySmapiTokenStore(),
|
||||||
|
tokenCleanupIntervalMinutes: 60,
|
||||||
};
|
};
|
||||||
|
|
||||||
function server(
|
function server(
|
||||||
@@ -747,7 +749,8 @@ function server(
|
|||||||
i8n,
|
i8n,
|
||||||
serverOpts.smapiAuthTokens,
|
serverOpts.smapiAuthTokens,
|
||||||
serverOpts.smapiTokenStore,
|
serverOpts.smapiTokenStore,
|
||||||
serverOpts.logRequests
|
serverOpts.logRequests,
|
||||||
|
serverOpts.tokenCleanupIntervalMinutes
|
||||||
);
|
);
|
||||||
|
|
||||||
if (serverOpts.applyContextPath) {
|
if (serverOpts.applyContextPath) {
|
||||||
|
|||||||
37
src/smapi.ts
37
src/smapi.ts
@@ -289,6 +289,7 @@ const playlist = (bonobUrl: URLBuilder, playlist: Playlist) => ({
|
|||||||
title: playlist.name,
|
title: playlist.name,
|
||||||
albumArtURI: coverArtURI(bonobUrl, playlist).href(),
|
albumArtURI: coverArtURI(bonobUrl, playlist).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
|
canEnumerate: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
userContent: false,
|
userContent: false,
|
||||||
@@ -406,7 +407,8 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
i8n: I8N,
|
i8n: I8N,
|
||||||
smapiAuthTokens: SmapiAuthTokens,
|
smapiAuthTokens: SmapiAuthTokens,
|
||||||
tokenStore: SmapiTokenStore,
|
tokenStore: SmapiTokenStore,
|
||||||
_logRequests: boolean
|
_logRequests: boolean,
|
||||||
|
tokenCleanupIntervalMinutes: number = 60
|
||||||
) {
|
) {
|
||||||
const sonosSoap = new SonosSoap(bonobUrl, linkCodes, smapiAuthTokens, clock, tokenStore);
|
const sonosSoap = new SonosSoap(bonobUrl, linkCodes, smapiAuthTokens, clock, tokenStore);
|
||||||
|
|
||||||
@@ -420,14 +422,16 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
logger.error("Failed to cleanup expired tokens on startup", { error });
|
logger.error("Failed to cleanup expired tokens on startup", { error });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up expired tokens every hour
|
// Clean up expired tokens periodically
|
||||||
|
const cleanupIntervalMs = tokenCleanupIntervalMinutes * 60 * 1000;
|
||||||
|
logger.info(`Token cleanup will run every ${tokenCleanupIntervalMinutes} minute(s)`);
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
try {
|
try {
|
||||||
tokenStore.cleanupExpired(smapiAuthTokens);
|
tokenStore.cleanupExpired(smapiAuthTokens);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to cleanup expired tokens", { error });
|
logger.error("Failed to cleanup expired tokens", { error });
|
||||||
}
|
}
|
||||||
}, 60 * 60 * 1000).unref(); // Run every hour, but don't prevent process exit
|
}, cleanupIntervalMs).unref(); // Don't prevent process exit
|
||||||
|
|
||||||
const urlWithToken = (accessToken: string) =>
|
const urlWithToken = (accessToken: string) =>
|
||||||
bonobUrl.append({
|
bonobUrl.append({
|
||||||
@@ -802,6 +806,30 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
// </getExtendedMetadataResult>
|
// </getExtendedMetadataResult>
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
case "playlists":
|
||||||
|
return musicLibrary
|
||||||
|
.playlists()
|
||||||
|
.then((it) =>
|
||||||
|
Promise.all(
|
||||||
|
it.map((playlist) => ({
|
||||||
|
id: playlist.id,
|
||||||
|
name: playlist.name,
|
||||||
|
coverArt: playlist.coverArt,
|
||||||
|
entries: [],
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(slice2(paging))
|
||||||
|
.then(([page, total]) => ({
|
||||||
|
getExtendedMetadataResult: {
|
||||||
|
count: page.length,
|
||||||
|
index: paging._index,
|
||||||
|
total,
|
||||||
|
mediaCollection: page.map((it) =>
|
||||||
|
playlist(urlWithToken(apiKey), it)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}));
|
||||||
default:
|
default:
|
||||||
throw `Unsupported getExtendedMetadata id=${id}`;
|
throw `Unsupported getExtendedMetadata id=${id}`;
|
||||||
}
|
}
|
||||||
@@ -877,7 +905,8 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
id: "playlists",
|
id: "playlists",
|
||||||
title: lang("playlists"),
|
title: lang("playlists"),
|
||||||
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
|
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
|
||||||
itemType: "playlist",
|
itemType: "container",
|
||||||
|
canEnumerate: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
userContent: true,
|
userContent: true,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import logger from "./logger";
|
|||||||
import { SmapiToken, SmapiAuthTokens } from "./smapi_auth";
|
import { SmapiToken, SmapiAuthTokens } from "./smapi_auth";
|
||||||
import { either as E } from "fp-ts";
|
import { either as E } from "fp-ts";
|
||||||
|
|
||||||
|
export { SQLiteSmapiTokenStore } from "./sqlite_smapi_token_store";
|
||||||
|
|
||||||
export interface SmapiTokenStore {
|
export interface SmapiTokenStore {
|
||||||
get(token: string): SmapiToken | undefined;
|
get(token: string): SmapiToken | undefined;
|
||||||
set(token: string, fullSmapiToken: SmapiToken): void;
|
set(token: string, fullSmapiToken: SmapiToken): void;
|
||||||
|
|||||||
200
src/sqlite_smapi_token_store.ts
Normal file
200
src/sqlite_smapi_token_store.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import logger from "./logger";
|
||||||
|
import { SmapiToken, SmapiAuthTokens } from "./smapi_auth";
|
||||||
|
import { either as E } from "fp-ts";
|
||||||
|
import { SmapiTokenStore } from "./smapi_token_store";
|
||||||
|
|
||||||
|
export class SQLiteSmapiTokenStore implements SmapiTokenStore {
|
||||||
|
private db!: Database.Database;
|
||||||
|
private readonly dbPath: string;
|
||||||
|
|
||||||
|
constructor(dbPath: string) {
|
||||||
|
this.dbPath = dbPath;
|
||||||
|
this.initializeDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeDatabase(): void {
|
||||||
|
try {
|
||||||
|
// Ensure the directory exists
|
||||||
|
const dir = path.dirname(this.dbPath);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
logger.info(`Created token storage directory: ${dir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database connection
|
||||||
|
this.db = new Database(this.dbPath);
|
||||||
|
|
||||||
|
// Create table if it doesn't exist
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS smapi_tokens (
|
||||||
|
token_key TEXT PRIMARY KEY,
|
||||||
|
token TEXT NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create index for faster lookups
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_created_at ON smapi_tokens(created_at)
|
||||||
|
`);
|
||||||
|
|
||||||
|
const count = this.db.prepare("SELECT COUNT(*) as count FROM smapi_tokens").get() as { count: number };
|
||||||
|
logger.info(`SQLite token store initialized at ${this.dbPath} with ${count.count} token(s)`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to initialize SQLite token store at ${this.dbPath}`, { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(tokenKey: string): SmapiToken | undefined {
|
||||||
|
try {
|
||||||
|
const stmt = this.db.prepare("SELECT token, key FROM smapi_tokens WHERE token_key = ?");
|
||||||
|
const row = stmt.get(tokenKey) as { token: string; key: string } | undefined;
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: row.token,
|
||||||
|
key: row.key,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to get token from SQLite store`, { error });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(tokenKey: string, fullSmapiToken: SmapiToken): void {
|
||||||
|
try {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO smapi_tokens (token_key, token, key)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`);
|
||||||
|
stmt.run(tokenKey, fullSmapiToken.token, fullSmapiToken.key);
|
||||||
|
logger.debug(`Saved token to SQLite store`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to save token to SQLite store`, { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(tokenKey: string): void {
|
||||||
|
try {
|
||||||
|
const stmt = this.db.prepare("DELETE FROM smapi_tokens WHERE token_key = ?");
|
||||||
|
stmt.run(tokenKey);
|
||||||
|
logger.debug(`Deleted token from SQLite store`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to delete token from SQLite store`, { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): { [tokenKey: string]: SmapiToken } {
|
||||||
|
try {
|
||||||
|
const stmt = this.db.prepare("SELECT token_key, token, key FROM smapi_tokens");
|
||||||
|
const rows = stmt.all() as Array<{ token_key: string; token: string; key: string }>;
|
||||||
|
|
||||||
|
const tokens: { [tokenKey: string]: SmapiToken } = {};
|
||||||
|
for (const row of rows) {
|
||||||
|
tokens[row.token_key] = {
|
||||||
|
token: row.token,
|
||||||
|
key: row.key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to get all tokens from SQLite store`, { error });
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupExpired(smapiAuthTokens: SmapiAuthTokens): number {
|
||||||
|
try {
|
||||||
|
const tokens = this.getAll();
|
||||||
|
const tokenKeys = Object.keys(tokens);
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
for (const tokenKey of tokenKeys) {
|
||||||
|
const smapiToken = tokens[tokenKey];
|
||||||
|
if (smapiToken) {
|
||||||
|
const verifyResult = smapiAuthTokens.verify(smapiToken);
|
||||||
|
// Only delete if token verification fails with InvalidTokenError
|
||||||
|
// Do NOT delete ExpiredTokenError as those can still be refreshed
|
||||||
|
if (E.isLeft(verifyResult)) {
|
||||||
|
const error = verifyResult.left;
|
||||||
|
// Only delete invalid tokens, not expired ones (which can be refreshed)
|
||||||
|
if (error._tag === 'InvalidTokenError') {
|
||||||
|
logger.debug(`Deleting invalid token from SQLite store`);
|
||||||
|
this.delete(tokenKey);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
logger.info(`Cleaned up ${deletedCount} invalid token(s) from SQLite store`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedCount;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to cleanup expired tokens from SQLite store`, { error });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate tokens from a JSON file to the SQLite database
|
||||||
|
* @param jsonFilePath Path to the JSON file containing tokens
|
||||||
|
* @returns Number of tokens migrated
|
||||||
|
*/
|
||||||
|
migrateFromJSON(jsonFilePath: string): number {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(jsonFilePath)) {
|
||||||
|
logger.info(`No JSON token file found at ${jsonFilePath}, skipping migration`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = fs.readFileSync(jsonFilePath, "utf8");
|
||||||
|
const tokens: { [tokenKey: string]: SmapiToken } = JSON.parse(data);
|
||||||
|
const tokenKeys = Object.keys(tokens);
|
||||||
|
|
||||||
|
let migratedCount = 0;
|
||||||
|
for (const tokenKey of tokenKeys) {
|
||||||
|
const token = tokens[tokenKey];
|
||||||
|
if (token) {
|
||||||
|
this.set(tokenKey, token);
|
||||||
|
migratedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Migrated ${migratedCount} token(s) from ${jsonFilePath} to SQLite`);
|
||||||
|
|
||||||
|
// Optionally rename the old JSON file to .bak
|
||||||
|
const backupPath = `${jsonFilePath}.bak`;
|
||||||
|
fs.renameSync(jsonFilePath, backupPath);
|
||||||
|
logger.info(`Backed up original JSON file to ${backupPath}`);
|
||||||
|
|
||||||
|
return migratedCount;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to migrate tokens from JSON file ${jsonFilePath}`, { error });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the database connection
|
||||||
|
*/
|
||||||
|
close(): void {
|
||||||
|
try {
|
||||||
|
this.db.close();
|
||||||
|
logger.info("SQLite token store connection closed");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to close SQLite token store connection", { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,7 +94,7 @@ class SonosDriver {
|
|||||||
.get(this.bonobUrl.append({ pathname: "/" }).pathname())
|
.get(this.bonobUrl.append({ pathname: "/" }).pathname())
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const m = response.text.match(/ action="(.*)" /i);
|
const m = response.text.match(/ action="([^"]+)"/i);
|
||||||
return m![1]!;
|
return m![1]!;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ describe("server", () => {
|
|||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(`<h2>${lang("devices")} \(0\)</h2>`);
|
expect(res.text).toMatch(new RegExp(`${lang("devices")}.*\\(0\\)`));
|
||||||
expect(res.text).not.toMatch(/class=device/);
|
expect(res.text).not.toMatch(/class=device/);
|
||||||
expect(res.text).toContain(lang("noSonosDevices"));
|
expect(res.text).toContain(lang("noSonosDevices"));
|
||||||
});
|
});
|
||||||
@@ -276,7 +276,7 @@ describe("server", () => {
|
|||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(`<h2>${lang("devices")} \(0\)</h2>`);
|
expect(res.text).toMatch(new RegExp(`${lang("devices")}.*\\(0\\)`));
|
||||||
expect(res.text).not.toMatch(/class=device/);
|
expect(res.text).not.toMatch(/class=device/);
|
||||||
expect(res.text).toContain(lang("noSonosDevices"));
|
expect(res.text).toContain(lang("noSonosDevices"));
|
||||||
});
|
});
|
||||||
@@ -290,7 +290,7 @@ describe("server", () => {
|
|||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(`<h2>${lang("services")} \(0\)</h2>`);
|
expect(res.text).toMatch(new RegExp(`${lang("services")}.*\\(0\\)`));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -352,9 +352,9 @@ describe("server", () => {
|
|||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(`<h2>${lang("devices")} \(2\)</h2>`);
|
expect(res.text).toMatch(new RegExp(`${lang("devices")}.*\\(2\\)`));
|
||||||
expect(res.text).toMatch(/device1\s+\(172.0.0.1:4301\)/);
|
expect(res.text).toMatch(/device1.*172\.0\.0\.1:4301/);
|
||||||
expect(res.text).toMatch(/device2\s+\(172.0.0.2:4302\)/);
|
expect(res.text).toMatch(/device2.*172\.0\.0\.2:4302/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -366,11 +366,11 @@ describe("server", () => {
|
|||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(`<h2>${lang("services")} \(4\)</h2>`);
|
expect(res.text).toMatch(new RegExp(`${lang("services")}.*\\(4\\)`));
|
||||||
expect(res.text).toMatch(/s1\s+\(1\)/);
|
expect(res.text).toMatch(/s1.*SID:\s*1/);
|
||||||
expect(res.text).toMatch(/s2\s+\(2\)/);
|
expect(res.text).toMatch(/s2.*SID:\s*2/);
|
||||||
expect(res.text).toMatch(/s3\s+\(3\)/);
|
expect(res.text).toMatch(/s3.*SID:\s*3/);
|
||||||
expect(res.text).toMatch(/s4\s+\(4\)/);
|
expect(res.text).toMatch(/s4.*SID:\s*4/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -382,14 +382,11 @@ describe("server", () => {
|
|||||||
.send();
|
.send();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(
|
||||||
`<input type="submit" value="${lang("register")}">`
|
`<input type="submit" value="${lang("register")}" id="submit">`
|
||||||
);
|
|
||||||
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
|
|
||||||
expect(res.text).toMatch(
|
|
||||||
`<h3>${lang("noExistingServiceRegistration")}</h3>`
|
|
||||||
);
|
);
|
||||||
|
expect(res.text).toContain(lang("noExistingServiceRegistration"));
|
||||||
expect(res.text).not.toMatch(
|
expect(res.text).not.toMatch(
|
||||||
`<input type="submit" value="${lang("removeRegistration")}">`
|
`value="${lang("removeRegistration")}"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -440,14 +437,11 @@ describe("server", () => {
|
|||||||
.send();
|
.send();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(
|
||||||
`<input type="submit" value="${lang("register")}">`
|
`<input type="submit" value="${lang("register")}" id="submit">`
|
||||||
);
|
);
|
||||||
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
|
expect(res.text).toContain(lang("existingServiceConfig"));
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(
|
||||||
`<h3>${lang("existingServiceConfig")}</h3>`
|
`<input type="submit" value="${lang("removeRegistration")}" id="submit"`
|
||||||
);
|
|
||||||
expect(res.text).toMatch(
|
|
||||||
`<input type="submit" value="${lang("removeRegistration")}">`
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -632,13 +626,13 @@ describe("server", () => {
|
|||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(`<title>${lang("login")}</title>`);
|
expect(res.text).toMatch(`<title>${lang("login")}</title>`);
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(
|
||||||
`<h1 class="login one-word-per-line">${lang("logInToBonob")}</h1>`
|
`<h1>${lang("logInToBonob")}</h1>`
|
||||||
);
|
);
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(
|
||||||
`<label for="username">${lang("username")}:</label>`
|
`<label for="username">${lang("username")}</label>`
|
||||||
);
|
);
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(
|
||||||
`<label for="password">${lang("password")}:</label>`
|
`<label for="password">${lang("password")}</label>`
|
||||||
);
|
);
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(
|
||||||
`<input type="submit" value="${lang("login")}" id="submit">`
|
`<input type="submit" value="${lang("login")}" id="submit">`
|
||||||
|
|||||||
@@ -1160,7 +1160,8 @@ describe("wsdl api", () => {
|
|||||||
id: "playlists",
|
id: "playlists",
|
||||||
title: "Playlists",
|
title: "Playlists",
|
||||||
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
|
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
|
||||||
itemType: "playlist",
|
itemType: "container",
|
||||||
|
canEnumerate: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
readOnly: "false",
|
readOnly: "false",
|
||||||
renameable: "false",
|
renameable: "false",
|
||||||
@@ -1260,7 +1261,8 @@ describe("wsdl api", () => {
|
|||||||
id: "playlists",
|
id: "playlists",
|
||||||
title: "Afspeellijsten",
|
title: "Afspeellijsten",
|
||||||
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
|
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
|
||||||
itemType: "playlist",
|
itemType: "container",
|
||||||
|
canEnumerate: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
readOnly: "false",
|
readOnly: "false",
|
||||||
renameable: "false",
|
renameable: "false",
|
||||||
@@ -1497,6 +1499,7 @@ describe("wsdl api", () => {
|
|||||||
playlist
|
playlist
|
||||||
).href(),
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
|
canEnumerate: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
readOnly: "false",
|
readOnly: "false",
|
||||||
userContent: "false",
|
userContent: "false",
|
||||||
@@ -1529,6 +1532,7 @@ describe("wsdl api", () => {
|
|||||||
playlist
|
playlist
|
||||||
).href(),
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
|
canEnumerate: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
readOnly: "false",
|
readOnly: "false",
|
||||||
userContent: "false",
|
userContent: "false",
|
||||||
|
|||||||
234
tests/sqlite_smapi_token_store.test.ts
Normal file
234
tests/sqlite_smapi_token_store.test.ts
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { SQLiteSmapiTokenStore } from "../src/sqlite_smapi_token_store";
|
||||||
|
import { SmapiToken } from "../src/smapi_auth";
|
||||||
|
import { JWTSmapiLoginTokens } from "../src/smapi_auth";
|
||||||
|
import { SystemClock } from "../src/clock";
|
||||||
|
|
||||||
|
describe("SQLiteSmapiTokenStore", () => {
|
||||||
|
const testDbPath = path.join(__dirname, "test-tokens.db");
|
||||||
|
const testJsonPath = path.join(__dirname, "test-tokens.json");
|
||||||
|
let tokenStore: SQLiteSmapiTokenStore;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Clean up any existing test files
|
||||||
|
if (fs.existsSync(testDbPath)) {
|
||||||
|
fs.unlinkSync(testDbPath);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(testJsonPath)) {
|
||||||
|
fs.unlinkSync(testJsonPath);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(`${testJsonPath}.bak`)) {
|
||||||
|
fs.unlinkSync(`${testJsonPath}.bak`);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStore = new SQLiteSmapiTokenStore(testDbPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
tokenStore.close();
|
||||||
|
|
||||||
|
// Clean up test files
|
||||||
|
if (fs.existsSync(testDbPath)) {
|
||||||
|
fs.unlinkSync(testDbPath);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(testJsonPath)) {
|
||||||
|
fs.unlinkSync(testJsonPath);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(`${testJsonPath}.bak`)) {
|
||||||
|
fs.unlinkSync(`${testJsonPath}.bak`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Database Initialization", () => {
|
||||||
|
it("should create database file on initialization", () => {
|
||||||
|
expect(fs.existsSync(testDbPath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create parent directory if it doesn't exist", () => {
|
||||||
|
const nestedPath = path.join(__dirname, "nested", "dir", "tokens.db");
|
||||||
|
const nestedStore = new SQLiteSmapiTokenStore(nestedPath);
|
||||||
|
|
||||||
|
expect(fs.existsSync(nestedPath)).toBe(true);
|
||||||
|
|
||||||
|
nestedStore.close();
|
||||||
|
fs.unlinkSync(nestedPath);
|
||||||
|
fs.rmdirSync(path.dirname(nestedPath));
|
||||||
|
fs.rmdirSync(path.dirname(path.dirname(nestedPath)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Token Operations", () => {
|
||||||
|
const testToken: SmapiToken = {
|
||||||
|
token: "test-jwt-token",
|
||||||
|
key: "test-key-123",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should set and get a token", () => {
|
||||||
|
tokenStore.set("token1", testToken);
|
||||||
|
const retrieved = tokenStore.get("token1");
|
||||||
|
|
||||||
|
expect(retrieved).toEqual(testToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for non-existent token", () => {
|
||||||
|
const retrieved = tokenStore.get("non-existent");
|
||||||
|
expect(retrieved).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update existing token", () => {
|
||||||
|
tokenStore.set("token1", testToken);
|
||||||
|
|
||||||
|
const updatedToken: SmapiToken = {
|
||||||
|
token: "updated-jwt-token",
|
||||||
|
key: "updated-key-456",
|
||||||
|
};
|
||||||
|
|
||||||
|
tokenStore.set("token1", updatedToken);
|
||||||
|
const retrieved = tokenStore.get("token1");
|
||||||
|
|
||||||
|
expect(retrieved).toEqual(updatedToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should delete a token", () => {
|
||||||
|
tokenStore.set("token1", testToken);
|
||||||
|
tokenStore.delete("token1");
|
||||||
|
const retrieved = tokenStore.get("token1");
|
||||||
|
|
||||||
|
expect(retrieved).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get all tokens", () => {
|
||||||
|
const token1: SmapiToken = { token: "jwt1", key: "key1" };
|
||||||
|
const token2: SmapiToken = { token: "jwt2", key: "key2" };
|
||||||
|
const token3: SmapiToken = { token: "jwt3", key: "key3" };
|
||||||
|
|
||||||
|
tokenStore.set("tokenKey1", token1);
|
||||||
|
tokenStore.set("tokenKey2", token2);
|
||||||
|
tokenStore.set("tokenKey3", token3);
|
||||||
|
|
||||||
|
const allTokens = tokenStore.getAll();
|
||||||
|
|
||||||
|
expect(Object.keys(allTokens).length).toBe(3);
|
||||||
|
expect(allTokens["tokenKey1"]).toEqual(token1);
|
||||||
|
expect(allTokens["tokenKey2"]).toEqual(token2);
|
||||||
|
expect(allTokens["tokenKey3"]).toEqual(token3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty object when no tokens exist", () => {
|
||||||
|
const allTokens = tokenStore.getAll();
|
||||||
|
expect(allTokens).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Token Cleanup", () => {
|
||||||
|
it("should cleanup invalid tokens", () => {
|
||||||
|
const smapiAuthTokens = new JWTSmapiLoginTokens(SystemClock, "test-secret", "1h");
|
||||||
|
|
||||||
|
// Create valid tokens
|
||||||
|
const validToken1 = smapiAuthTokens.issue("service-token-1");
|
||||||
|
const validToken2 = smapiAuthTokens.issue("service-token-2");
|
||||||
|
|
||||||
|
// Create invalid token (wrong secret)
|
||||||
|
const invalidAuthTokens = new JWTSmapiLoginTokens(SystemClock, "different-secret", "1h");
|
||||||
|
const invalidToken = invalidAuthTokens.issue("service-token-3");
|
||||||
|
|
||||||
|
tokenStore.set("valid1", validToken1);
|
||||||
|
tokenStore.set("valid2", validToken2);
|
||||||
|
tokenStore.set("invalid", invalidToken);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
const deletedCount = tokenStore.cleanupExpired(smapiAuthTokens);
|
||||||
|
|
||||||
|
expect(deletedCount).toBe(1);
|
||||||
|
expect(tokenStore.get("valid1")).toBeDefined();
|
||||||
|
expect(tokenStore.get("valid2")).toBeDefined();
|
||||||
|
expect(tokenStore.get("invalid")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not cleanup expired tokens that can be refreshed", () => {
|
||||||
|
// Note: This test would require mocking time to create an expired token
|
||||||
|
// For now, we just verify the function runs without error
|
||||||
|
const smapiAuthTokens = new JWTSmapiLoginTokens(SystemClock, "test-secret", "1h");
|
||||||
|
const validToken = smapiAuthTokens.issue("service-token-1");
|
||||||
|
|
||||||
|
tokenStore.set("token1", validToken);
|
||||||
|
const deletedCount = tokenStore.cleanupExpired(smapiAuthTokens);
|
||||||
|
|
||||||
|
expect(deletedCount).toBe(0);
|
||||||
|
expect(tokenStore.get("token1")).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("JSON Migration", () => {
|
||||||
|
it("should migrate tokens from JSON file", () => {
|
||||||
|
const jsonTokens = {
|
||||||
|
token1: { token: "jwt1", key: "key1" },
|
||||||
|
token2: { token: "jwt2", key: "key2" },
|
||||||
|
token3: { token: "jwt3", key: "key3" },
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(testJsonPath, JSON.stringify(jsonTokens, null, 2), "utf8");
|
||||||
|
|
||||||
|
const migratedCount = tokenStore.migrateFromJSON(testJsonPath);
|
||||||
|
|
||||||
|
expect(migratedCount).toBe(3);
|
||||||
|
expect(tokenStore.get("token1")).toEqual(jsonTokens.token1);
|
||||||
|
expect(tokenStore.get("token2")).toEqual(jsonTokens.token2);
|
||||||
|
expect(tokenStore.get("token3")).toEqual(jsonTokens.token3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create backup of original JSON file", () => {
|
||||||
|
const jsonTokens = {
|
||||||
|
token1: { token: "jwt1", key: "key1" },
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(testJsonPath, JSON.stringify(jsonTokens, null, 2), "utf8");
|
||||||
|
tokenStore.migrateFromJSON(testJsonPath);
|
||||||
|
|
||||||
|
expect(fs.existsSync(`${testJsonPath}.bak`)).toBe(true);
|
||||||
|
expect(fs.existsSync(testJsonPath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 0 when JSON file does not exist", () => {
|
||||||
|
const migratedCount = tokenStore.migrateFromJSON(testJsonPath);
|
||||||
|
expect(migratedCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty JSON file", () => {
|
||||||
|
fs.writeFileSync(testJsonPath, JSON.stringify({}), "utf8");
|
||||||
|
const migratedCount = tokenStore.migrateFromJSON(testJsonPath);
|
||||||
|
|
||||||
|
expect(migratedCount).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Persistence", () => {
|
||||||
|
it("should persist tokens across instances", () => {
|
||||||
|
const testToken: SmapiToken = { token: "jwt1", key: "key1" };
|
||||||
|
|
||||||
|
tokenStore.set("token1", testToken);
|
||||||
|
tokenStore.close();
|
||||||
|
|
||||||
|
// Create new instance with same database
|
||||||
|
const newStore = new SQLiteSmapiTokenStore(testDbPath);
|
||||||
|
const retrieved = newStore.get("token1");
|
||||||
|
|
||||||
|
expect(retrieved).toEqual(testToken);
|
||||||
|
|
||||||
|
newStore.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Close", () => {
|
||||||
|
it("should close database connection without error", () => {
|
||||||
|
expect(() => tokenStore.close()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple close calls gracefully", () => {
|
||||||
|
tokenStore.close();
|
||||||
|
// Second close should not throw
|
||||||
|
expect(() => tokenStore.close()).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
<% layout('./layout', { title: it.lang("failure") }) %>
|
<% layout('./layout', { title: it.lang("failure") }) %>
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<h1 class="failure"><%= it.message %></h1>
|
<div class="message-container">
|
||||||
<h1 class="cause"><%= it.cause || "" %></h1>
|
<div class="logo">✗</div>
|
||||||
|
<h1><%= it.message %></h1>
|
||||||
|
<% if (it.cause) { %>
|
||||||
|
<p style="color: #dc3545; margin-top: 15px;"><%= it.cause %></p>
|
||||||
|
<% } %>
|
||||||
|
<p style="color: #dc3545; font-weight: 600; margin-top: 10px;"><%= it.lang("failure") %></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,45 +1,61 @@
|
|||||||
<% layout('./layout') %>
|
<% layout('./layout') %>
|
||||||
|
|
||||||
<div id="content">
|
<div id="content" class="index-content">
|
||||||
<div width="100%" style="text-align:right;color:grey"><%= it.version %></div>
|
<div style="text-align:right;color:#999;font-size:0.85rem;margin-bottom:20px"><%= it.version %></div>
|
||||||
<h1><%= it.bonobService.name %> (<%= it.bonobService.sid %>)</h1>
|
<div class="logo">🎵</div>
|
||||||
<h3><%= it.lang("expectedConfig") %></h3>
|
<h1><%= it.bonobService.name %></h1>
|
||||||
<div><%= JSON.stringify(it.bonobService) %></div>
|
<p style="color:#999;margin-bottom:30px">Service ID: <%= it.bonobService.sid %></p>
|
||||||
<br/>
|
|
||||||
<% if(it.devices.length > 0) { %>
|
<% if(it.devices.length > 0) { %>
|
||||||
<form action="<%= it.createRegistrationRoute %>" method="POST">
|
<form action="<%= it.createRegistrationRoute %>" method="POST" style="margin-bottom:30px">
|
||||||
<input type="submit" value="<%= it.lang("register") %>">
|
<input type="submit" value="<%= it.lang("register") %>" id="submit">
|
||||||
</form>
|
</form>
|
||||||
<br/>
|
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<h3><%= it.lang("noSonosDevices") %></h3>
|
<p style="color:#dc3545;font-weight:600;margin:30px 0"><%= it.lang("noSonosDevices") %></p>
|
||||||
<br/>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if(it.registeredBonobService) { %>
|
<% if(it.registeredBonobService) { %>
|
||||||
<h3><%= it.lang("existingServiceConfig") %></h3>
|
<div style="margin:20px 0;padding:15px;background:#f8f9fa;border-radius:8px">
|
||||||
<div><%= JSON.stringify(it.registeredBonobService) %></div>
|
<h3 style="font-size:1.1rem;margin-bottom:10px;color:#667eea"><%= it.lang("existingServiceConfig") %></h3>
|
||||||
|
<pre style="font-size:0.85rem;text-align:left;overflow-x:auto"><%= JSON.stringify(it.registeredBonobService, null, 2) %></pre>
|
||||||
|
</div>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<h3><%= it.lang("noExistingServiceRegistration") %></h3>
|
<p style="color:#999;margin:20px 0"><%= it.lang("noExistingServiceRegistration") %></p>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if(it.registeredBonobService) { %>
|
<% if(it.registeredBonobService) { %>
|
||||||
<br/>
|
<form action="<%= it.removeRegistrationRoute %>" method="POST" style="margin:20px 0">
|
||||||
<form action="<%= it.removeRegistrationRoute %>" method="POST">
|
<input type="submit" value="<%= it.lang("removeRegistration") %>" id="submit" style="background:#dc3545">
|
||||||
<input type="submit" value="<%= it.lang("removeRegistration") %>">
|
|
||||||
</form>
|
</form>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<br/>
|
<div style="margin-top:40px;padding-top:30px;border-top:2px solid #e0e0e0">
|
||||||
<h2><%= it.lang("devices") %> (<%= it.devices.length %>)</h2>
|
<h2 style="font-size:1.5rem;margin-bottom:15px"><%= it.lang("devices") %> (<%= it.devices.length %>)</h2>
|
||||||
<ul>
|
<% if(it.devices.length > 0) { %>
|
||||||
|
<ul style="list-style:none;text-align:left;padding:0">
|
||||||
<% it.devices.forEach(function(d){ %>
|
<% it.devices.forEach(function(d){ %>
|
||||||
<li><%= d.name %> (<%= d.ip %>:<%= d.port %>)</li>
|
<li style="padding:10px;margin:5px 0;background:#f8f9fa;border-radius:6px">
|
||||||
|
<strong><%= d.name %></strong> <span style="color:#999">(<%= d.ip %>:<%= d.port %>)</span>
|
||||||
|
</li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
<h2><%= it.lang("services") %> (<%= it.services.length %>)</h2>
|
<% } else { %>
|
||||||
<ul>
|
<p style="color:#999">No devices found</p>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:30px">
|
||||||
|
<h2 style="font-size:1.5rem;margin-bottom:15px"><%= it.lang("services") %> (<%= it.services.length %>)</h2>
|
||||||
|
<% if(it.services.length > 0) { %>
|
||||||
|
<ul style="list-style:none;text-align:left;padding:0">
|
||||||
<% it.services.forEach(function(s){ %>
|
<% it.services.forEach(function(s){ %>
|
||||||
<li><%= s.name %> (<%= s.sid %>)</li>
|
<li style="padding:10px;margin:5px 0;background:#f8f9fa;border-radius:6px">
|
||||||
|
<strong><%= s.name %></strong> <span style="color:#999">(SID: <%= s.sid %>)</span>
|
||||||
|
</li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
|
<% } else { %>
|
||||||
|
<p style="color:#999">No services registered</p>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,46 +1,189 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><%= it.title || "bonob" %></title>
|
<title><%= it.title || "bonob" %></title>
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#content {
|
div#content {
|
||||||
margin: auto;
|
background: white;
|
||||||
width: 60%;
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 50px 40px;
|
||||||
|
max-width: 450px;
|
||||||
|
width: 100%;
|
||||||
|
animation: slideIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 700%;
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
font-size: 300%;
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input[type="text"],
|
||||||
font-size: 300%;
|
input[type="password"] {
|
||||||
width: 80%;
|
width: 100%;
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="password"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
input#submit {
|
input#submit {
|
||||||
margin-top: 100px
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 10px;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.one-word-per-line {
|
input#submit:hover {
|
||||||
word-spacing: 100000px;
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login{
|
input#submit:active {
|
||||||
width: min-intrinsic;
|
transform: translateY(0);
|
||||||
width: -webkit-min-content;
|
}
|
||||||
width: -moz-min-content;
|
|
||||||
width: min-content;
|
.logo {
|
||||||
display: table-caption;
|
text-align: center;
|
||||||
display: -ms-grid;
|
margin-bottom: 20px;
|
||||||
-ms-grid-columns: min-content;
|
font-size: 3rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success and failure page styles */
|
||||||
|
.message-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-container h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-container p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Index page styles */
|
||||||
|
.index-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-content h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-content p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-content a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-content a:hover {
|
||||||
|
color: #764ba2;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
div#content {
|
||||||
|
padding: 40px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"] {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
<% layout('./layout', { title: it.lang("login") }) %>
|
<% layout('./layout', { title: it.lang("login") }) %>
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<h1 class="login one-word-per-line"><%= it.lang("logInToBonob") %></h1>
|
<div class="logo">🎵</div>
|
||||||
|
<h1><%= it.lang("logInToBonob") %></h1>
|
||||||
<form action="<%= it.loginRoute %>" method="POST">
|
<form action="<%= it.loginRoute %>" method="POST">
|
||||||
<label for="username"><%= it.lang("username") %>:</label><br>
|
<div class="form-group">
|
||||||
<input type="text" id="username" name="username"><br><br>
|
<label for="username"><%= it.lang("username") %></label>
|
||||||
<label for="password"><%= it.lang("password") %>:</label><br>
|
<input type="text" id="username" name="username" required autofocus>
|
||||||
<input type="password" id="password" name="password"><br>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password"><%= it.lang("password") %></label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
<input type="hidden" name="linkCode" value="<%= it.linkCode %>">
|
<input type="hidden" name="linkCode" value="<%= it.linkCode %>">
|
||||||
<input type="submit" value="<%= it.lang("login") %>" id="submit">
|
<input type="submit" value="<%= it.lang("login") %>" id="submit">
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<% layout('./layout', { title: it.lang("success") }) %>
|
<% layout('./layout', { title: it.lang("success") }) %>
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<h1 class="success"><%= it.message %></h1>
|
<div class="message-container">
|
||||||
|
<div class="logo">✓</div>
|
||||||
|
<h1><%= it.message %></h1>
|
||||||
|
<p style="color: #28a745; font-weight: 600; margin-top: 10px;"><%= it.lang("success") %></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
Reference in New Issue
Block a user