mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
chore: refactor interfaces (#2849)
* chore: refactor interfaces * chore: fix uptime
This commit is contained in:
202
web/package-lock.json
generated
202
web/package-lock.json
generated
@@ -25,18 +25,18 @@
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/argon2": "^0.2.2",
|
||||
"@start9labs/start-sdk": "file:../sdk/baseDist",
|
||||
"@taiga-ui/addon-charts": "4.24.0",
|
||||
"@taiga-ui/addon-commerce": "4.24.0",
|
||||
"@taiga-ui/addon-mobile": "4.24.0",
|
||||
"@taiga-ui/addon-table": "4.24.0",
|
||||
"@taiga-ui/cdk": "4.24.0",
|
||||
"@taiga-ui/core": "4.24.0",
|
||||
"@taiga-ui/event-plugins": "4.4.0",
|
||||
"@taiga-ui/icons": "4.24.0",
|
||||
"@taiga-ui/kit": "4.24.0",
|
||||
"@taiga-ui/layout": "4.24.0",
|
||||
"@taiga-ui/legacy": "4.24.0",
|
||||
"@taiga-ui/polymorpheus": "4.8.0",
|
||||
"@taiga-ui/addon-charts": "4.28.0",
|
||||
"@taiga-ui/addon-commerce": "4.28.0",
|
||||
"@taiga-ui/addon-mobile": "4.28.0",
|
||||
"@taiga-ui/addon-table": "4.28.0",
|
||||
"@taiga-ui/cdk": "4.28.0",
|
||||
"@taiga-ui/core": "4.28.0",
|
||||
"@taiga-ui/event-plugins": "4.4.1",
|
||||
"@taiga-ui/icons": "4.28.0",
|
||||
"@taiga-ui/kit": "4.28.0",
|
||||
"@taiga-ui/layout": "4.28.0",
|
||||
"@taiga-ui/legacy": "4.28.0",
|
||||
"@taiga-ui/polymorpheus": "4.9.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
@@ -3420,9 +3420,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@maskito/angular": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.2.1.tgz",
|
||||
"integrity": "sha512-Qb9qY6AeG23KWuROF2gcHzUxXiaaMIFuoA/ekqI4TAtBDEO2D7ImQ0oTkdsFfbDLZoI3phmy70H4f+gidEnfag==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.4.0.tgz",
|
||||
"integrity": "sha512-iMFP/siEgU9Ki+g1PReZlA5+LlBMp6inqXGG5KCezhmDleZnG5lL9gxk3+ktJvKu+2kayLcwyBeUKXPwMBVt9w==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -3431,35 +3431,35 @@
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@angular/forms": ">=16.0.0",
|
||||
"@maskito/core": "^3.2.1"
|
||||
"@maskito/core": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maskito/core": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.2.1.tgz",
|
||||
"integrity": "sha512-gpoJbFFsq2CFz9smqEvZfTPVMt/gkRoiL14idL1sLXEDbWZyeKce0SgusHzHXaOkEegly4BIJGazCNG2hsPNYg==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.4.0.tgz",
|
||||
"integrity": "sha512-gFM6qk675YwOEGhxu9Xm6/sl1TZBRab6+B3Gstqml7xJopHHZ0rUOrWXwmX0z2JI+1PsgUL/ftV/CSZ8CpIONg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@maskito/kit": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.2.1.tgz",
|
||||
"integrity": "sha512-p9Tmr4BMZs7geNWMYMQaP4Dc+Sre8OeE6Vynz1RGsJJdjxmocY2mihXKldecS8c9aXfHvLU658F9YtorL3Akjg==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.4.0.tgz",
|
||||
"integrity": "sha512-jkexr7wjAqFeMpyc7s0IlinL+3F9xC4BYUHDQcEqlAJisDgVFtGCZZK/RvV1C+HGDn2gtzzVrJ3G/OY66k6EXg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@maskito/core": "^3.2.1"
|
||||
"@maskito/core": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maskito/phone": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.2.1.tgz",
|
||||
"integrity": "sha512-F1hrnpP1UyMhVO0nZAMoA0kkS8rVTc4kVUZ8mcxz98weIAKYo3wiuLOMXb1cg7/ejcpEnqDTRe1rzBxwtRAKeQ==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.4.0.tgz",
|
||||
"integrity": "sha512-KR6JuuWhTumIOCUV3CzPhh1niCXcuqsogNsLW3YfdmeVo8GygS9isnHNbSaAA/b9OnmIEkh25mur6x3yEJuYjA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@maskito/core": "^3.2.1",
|
||||
"@maskito/kit": "^3.2.1",
|
||||
"@maskito/core": "^3.4.0",
|
||||
"@maskito/kit": "^3.4.0",
|
||||
"libphonenumber-js": ">=1.0.0"
|
||||
}
|
||||
},
|
||||
@@ -4415,9 +4415,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-charts": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.24.0.tgz",
|
||||
"integrity": "sha512-x75l4Dj1l7ForatbIdk4O8NzM6fQlKRp9UsXzMrKSoKj9O3jGHsFyMV67FhBS9oVitU4+3MIgPLuj09qpG1gBg==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.28.0.tgz",
|
||||
"integrity": "sha512-Lvi2R8Y50kBbfbru31YHon+CEpnOzAx0G4GnqjN2goTLNQ6iX7pgUeyRyiXI4ay1yLrzVIOZJhSmBwWSDocZEg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4426,15 +4426,15 @@
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.24.0",
|
||||
"@taiga-ui/core": "^4.24.0",
|
||||
"@taiga-ui/cdk": "^4.28.0",
|
||||
"@taiga-ui/core": "^4.28.0",
|
||||
"@taiga-ui/polymorpheus": "^4.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-commerce": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.24.0.tgz",
|
||||
"integrity": "sha512-YAbhTZHXnN5TT9oO+KJX/NAehuHgMZ/QoVIuzQR16HiIoKTQQWznSipThnX2hinXrLNBas0O2Teo4Ij5kGIEOQ==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.28.0.tgz",
|
||||
"integrity": "sha512-VYygBL7oySCZYLBimGJPx/VGGtUGhpes3XwBHAPBmmyiVxct0kxXzhCQdAvNMQcSvDzXDBjg3wmJiUbZA/uHGQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4443,22 +4443,22 @@
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@angular/forms": ">=16.0.0",
|
||||
"@maskito/angular": "^3.2.1",
|
||||
"@maskito/core": "^3.2.1",
|
||||
"@maskito/kit": "^3.2.1",
|
||||
"@maskito/angular": "^3.4.0",
|
||||
"@maskito/core": "^3.4.0",
|
||||
"@maskito/kit": "^3.4.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.24.0",
|
||||
"@taiga-ui/core": "^4.24.0",
|
||||
"@taiga-ui/i18n": "^4.24.0",
|
||||
"@taiga-ui/kit": "^4.24.0",
|
||||
"@taiga-ui/cdk": "^4.28.0",
|
||||
"@taiga-ui/core": "^4.28.0",
|
||||
"@taiga-ui/i18n": "^4.28.0",
|
||||
"@taiga-ui/kit": "^4.28.0",
|
||||
"@taiga-ui/polymorpheus": "^4.8.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-mobile": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.24.0.tgz",
|
||||
"integrity": "sha512-YdkhhgWsmtgi85sPbjibqa/BxdqKkETav/6O0KgFmBGvS4MF/Hr2sOXWMDUfskSALee9BJBNPdf8NrV/zNpcXw==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.28.0.tgz",
|
||||
"integrity": "sha512-1RRaX37Ddl24q4nHrMEz6iDqHWi/mkTyXQ+kADX7+ydx9JkbU2H4R+qXrOx4+GUi93Y05HvAWCNCToBu3Ytt2A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4468,18 +4468,18 @@
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.24.0",
|
||||
"@taiga-ui/core": "^4.24.0",
|
||||
"@taiga-ui/kit": "^4.24.0",
|
||||
"@taiga-ui/layout": "^4.24.0",
|
||||
"@taiga-ui/cdk": "^4.28.0",
|
||||
"@taiga-ui/core": "^4.28.0",
|
||||
"@taiga-ui/kit": "^4.28.0",
|
||||
"@taiga-ui/layout": "^4.28.0",
|
||||
"@taiga-ui/polymorpheus": "^4.8.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-table": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.24.0.tgz",
|
||||
"integrity": "sha512-da/5FUmyrG6+pS9XgBY7B78EU6O1M9GPh9fxTtYTDjRgEpFdb/4TX0J79v5InSTGQV77ITVkpqbTB1moRgUVfA==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.28.0.tgz",
|
||||
"integrity": "sha512-C8MW6kJ3T9zy51rSxqYApll+S84oizK6C85gZyDM3gEV2RAlK2DP+r657ZlEwEgobrFCtBZe++TT7ZoKpQBBHg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4488,18 +4488,18 @@
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/intersection-observer": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.24.0",
|
||||
"@taiga-ui/core": "^4.24.0",
|
||||
"@taiga-ui/i18n": "^4.24.0",
|
||||
"@taiga-ui/kit": "^4.24.0",
|
||||
"@taiga-ui/cdk": "^4.28.0",
|
||||
"@taiga-ui/core": "^4.28.0",
|
||||
"@taiga-ui/i18n": "^4.28.0",
|
||||
"@taiga-ui/kit": "^4.28.0",
|
||||
"@taiga-ui/polymorpheus": "^4.8.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/cdk": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.24.0.tgz",
|
||||
"integrity": "sha512-vsI+Jyzj4yfMolCg42OOyDvT30g1WFoupfOGlUk8A9R2HTVhMTeFeoA186A7c5KLI3e9fxj/60Rc9LbIbacnDA==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.28.0.tgz",
|
||||
"integrity": "sha512-P2vK+4WDnSt/nnilqxvDS4lyMAEH/M73z9YSzyH5mEwVTNxD3m82jJgpHqV5Re7geooAyaKqS6MJwDxaN0+9eQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
@@ -4522,15 +4522,15 @@
|
||||
"@ng-web-apis/platform": "^4.11.1",
|
||||
"@ng-web-apis/resize-observer": "^4.11.1",
|
||||
"@ng-web-apis/screen-orientation": "^4.11.1",
|
||||
"@taiga-ui/event-plugins": "^4.4.0",
|
||||
"@taiga-ui/event-plugins": "^4.4.1",
|
||||
"@taiga-ui/polymorpheus": "^4.8.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/core": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.24.0.tgz",
|
||||
"integrity": "sha512-tHHxdwXtvMI8W2Ct1YgLZGg5UEDRPq3j3fYwLV0P/G+17TU1rVeF9/JxVe8ZdZ1Mo55LNLDflTnenC13GK0R/w==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.28.0.tgz",
|
||||
"integrity": "sha512-4eP6PJvmHZCrV/9apxfu6Bgj7L72yjVg1R5c4j1MsVmMESLCCRGlk0hPPvuxVQ+ZYrOZwNeWyKHPZDPL5uQawA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4544,17 +4544,17 @@
|
||||
"@angular/router": ">=16.0.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@ng-web-apis/mutation-observer": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.24.0",
|
||||
"@taiga-ui/event-plugins": "^4.4.0",
|
||||
"@taiga-ui/i18n": "^4.24.0",
|
||||
"@taiga-ui/cdk": "^4.28.0",
|
||||
"@taiga-ui/event-plugins": "^4.4.1",
|
||||
"@taiga-ui/i18n": "^4.28.0",
|
||||
"@taiga-ui/polymorpheus": "^4.8.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/event-plugins": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.4.0.tgz",
|
||||
"integrity": "sha512-Tv8C0p5EZXl7s1Vc+MrLbAblbYvyswomY/xvyFcI9NgMj6JyfsStu6jpCiRMfzojz3G70PRFsk0+WwI19lRJCQ==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.4.1.tgz",
|
||||
"integrity": "sha512-gwEkgyZsbAdRfmb98KlKWivYVF88eP0bOtbHwfj8Ec8DgJ5809qFqeWvJEIxZZ829iox1m8z2UuVrqN2/tI1tQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -4566,9 +4566,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/i18n": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.24.0.tgz",
|
||||
"integrity": "sha512-F3s+SGJ6QkGIWDpTcJvqiE++WHHF95o11tokQvzFq9pCLfMrxehNx38Dv/JnZK3FA2vYlVqdaQc0OsRMfMDRMw==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.28.0.tgz",
|
||||
"integrity": "sha512-kM7bbqllzir4nEk3X+YMKATm23UoKJeWSGmwnjLEmhWkpNAGqfErDRbE2puf+jXy7eufGhaB7ht/mK4+HkLXbw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -4581,18 +4581,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/icons": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.24.0.tgz",
|
||||
"integrity": "sha512-VGWKMuVayab+/MJ60JuxRM2zTJG2WelDmUuTMdYqnh+zAWTn7FoPjDgAREiT7m84XryZdKtkUqGK6aLI+J62Sg==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.28.0.tgz",
|
||||
"integrity": "sha512-1QS7gvYHuTRUUodE58OXm+4Ree5FhFe0co0Lj+3sqeqkYb495z5q3CXBNiXD3y8IcDTjNuYkxKxEthbPnQrsVQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/kit": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.24.0.tgz",
|
||||
"integrity": "sha512-qz+OpsAyHWUT6fcfBPd+RJTv98J5USezh028B7TpSLSSyRE6mbklKIj2gteJQVnwVS0AXwm69WgQDXMVolbJnw==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.28.0.tgz",
|
||||
"integrity": "sha512-JEHUZhWU0vgPorvO3l9POzWKPbFQA57jFh9Iv5/RlWxMI8EUI+OKH5J8z1ptX+RJE2dWB9+Yi84zasgr8TWcSA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4602,25 +4602,25 @@
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@angular/forms": ">=16.0.0",
|
||||
"@angular/router": ">=16.0.0",
|
||||
"@maskito/angular": "^3.2.1",
|
||||
"@maskito/core": "^3.2.1",
|
||||
"@maskito/kit": "^3.2.1",
|
||||
"@maskito/phone": "^3.2.1",
|
||||
"@maskito/angular": "^3.4.0",
|
||||
"@maskito/core": "^3.4.0",
|
||||
"@maskito/kit": "^3.4.0",
|
||||
"@maskito/phone": "^3.4.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@ng-web-apis/intersection-observer": "^4.11.1",
|
||||
"@ng-web-apis/mutation-observer": "^4.11.1",
|
||||
"@ng-web-apis/resize-observer": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.24.0",
|
||||
"@taiga-ui/core": "^4.24.0",
|
||||
"@taiga-ui/i18n": "^4.24.0",
|
||||
"@taiga-ui/cdk": "^4.28.0",
|
||||
"@taiga-ui/core": "^4.28.0",
|
||||
"@taiga-ui/i18n": "^4.28.0",
|
||||
"@taiga-ui/polymorpheus": "^4.8.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/layout": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.24.0.tgz",
|
||||
"integrity": "sha512-xwWB2iXeQWipzw0jTvqFL6V/20bhyM5lNT2AyAo6EcI8fX1N8x/RThaz9crf1jexajynHm4qirGT1M8tblg+Pw==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.28.0.tgz",
|
||||
"integrity": "sha512-NlXdEmXGhYvTWeSSpGlT9XS0SU1aQDuFAMFBSDVsZqLPWh2DTnNsxSf1/b6UYMmX5JKXhH/bRVvX97N5L5XZqQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4628,17 +4628,17 @@
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@taiga-ui/cdk": "^4.24.0",
|
||||
"@taiga-ui/core": "^4.24.0",
|
||||
"@taiga-ui/kit": "^4.24.0",
|
||||
"@taiga-ui/cdk": "^4.28.0",
|
||||
"@taiga-ui/core": "^4.28.0",
|
||||
"@taiga-ui/kit": "^4.28.0",
|
||||
"@taiga-ui/polymorpheus": "^4.8.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/legacy": {
|
||||
"version": "4.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.24.0.tgz",
|
||||
"integrity": "sha512-J3CJwDuaVV1zMUunZWru6Syg1cFPL0uF8qWyAis+tgllfYxFpUstvJfKhqezSauXifFsDqcLJYKGu3/fMLIsmA==",
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.28.0.tgz",
|
||||
"integrity": "sha512-mWE5w7alYsT8GMBNTfcvrf/sJjh1li2/mTykH/aoWklgYHHmSt6moY4Myi8wKdlRFBzi82eXsvJcUSCwD8Y5ew==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4648,9 +4648,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/polymorpheus": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/polymorpheus/-/polymorpheus-4.8.0.tgz",
|
||||
"integrity": "sha512-gNXk8SVxXf/5wtmm6XeFMQ9RzY0xbM9E4vFxSGwnNegVZtv3T08YX2uoxPgUbgck2/GS9N5B5KvjjbVa0T0L9A==",
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/polymorpheus/-/polymorpheus-4.9.0.tgz",
|
||||
"integrity": "sha512-TbIIwslbEnxunKuL9OyPZdmefrvJEK6HYiADEKQHUMUs4Pk2UbhMckUieURo83yPDamk/Mww+Nu/g60J/4uh2w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
@@ -9496,9 +9496,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.11.19",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.19.tgz",
|
||||
"integrity": "sha512-bW/Yp/9dod6fmyR+XqSUL1N5JE7QRxQ3KrBIbYS1FTv32e5i3SEtQVX+71CYNv8maWNSOgnlCoNp9X78f/cKiA==",
|
||||
"version": "1.12.6",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz",
|
||||
"integrity": "sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
|
||||
@@ -47,18 +47,18 @@
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/argon2": "^0.2.2",
|
||||
"@start9labs/start-sdk": "file:../sdk/baseDist",
|
||||
"@taiga-ui/addon-charts": "4.24.0",
|
||||
"@taiga-ui/addon-commerce": "4.24.0",
|
||||
"@taiga-ui/addon-mobile": "4.24.0",
|
||||
"@taiga-ui/addon-table": "4.24.0",
|
||||
"@taiga-ui/cdk": "4.24.0",
|
||||
"@taiga-ui/core": "4.24.0",
|
||||
"@taiga-ui/event-plugins": "4.4.0",
|
||||
"@taiga-ui/icons": "4.24.0",
|
||||
"@taiga-ui/kit": "4.24.0",
|
||||
"@taiga-ui/layout": "4.24.0",
|
||||
"@taiga-ui/legacy": "4.24.0",
|
||||
"@taiga-ui/polymorpheus": "4.8.0",
|
||||
"@taiga-ui/addon-charts": "4.28.0",
|
||||
"@taiga-ui/addon-commerce": "4.28.0",
|
||||
"@taiga-ui/addon-mobile": "4.28.0",
|
||||
"@taiga-ui/addon-table": "4.28.0",
|
||||
"@taiga-ui/cdk": "4.28.0",
|
||||
"@taiga-ui/core": "4.28.0",
|
||||
"@taiga-ui/event-plugins": "4.4.1",
|
||||
"@taiga-ui/icons": "4.28.0",
|
||||
"@taiga-ui/kit": "4.28.0",
|
||||
"@taiga-ui/layout": "4.28.0",
|
||||
"@taiga-ui/legacy": "4.28.0",
|
||||
"@taiga-ui/polymorpheus": "4.9.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
|
||||
@@ -127,6 +127,11 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
||||
inset 0 1px rgba(255, 255, 255, 0.15),
|
||||
inset 0 0 1rem rgba(0, 0, 0, 0.25),
|
||||
var(--tui-shadow-medium);
|
||||
|
||||
[tuiOption] {
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
[tuiSidebar] > div.t-wrapper {
|
||||
|
||||
@@ -105,11 +105,6 @@ import { ABOUT } from './about.component'
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
[tuiOption] {
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
[tuiIconButton] {
|
||||
box-shadow: inset -1.25rem 0 0 -1rem var(--status);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { toAcmeName } from 'src/app/utils/acme'
|
||||
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'acme',
|
||||
})
|
||||
export class AcmePipe implements PipeTransform {
|
||||
transform(value: string | null = null): string {
|
||||
return toAcmeName(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { CopyService } from '@start9labs/shared'
|
||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||
import {
|
||||
TuiButton,
|
||||
tuiButtonOptionsProvider,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
} from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'td[actions]',
|
||||
template: `
|
||||
<div class="desktop">
|
||||
<ng-content />
|
||||
@if (interface.serviceInterface().type === 'ui') {
|
||||
<a
|
||||
tuiIconButton
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[href]="actions()"
|
||||
>
|
||||
Launch UI
|
||||
</a>
|
||||
}
|
||||
<button tuiIconButton iconStart="@tui.qr-code" (click)="showQR()">
|
||||
Show QR
|
||||
</button>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(actions())"
|
||||
>
|
||||
Copy URL
|
||||
</button>
|
||||
</div>
|
||||
<div class="mobile">
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.ellipsis-vertical"
|
||||
tuiDropdownOpen
|
||||
[tuiDropdown]="dropdown"
|
||||
>
|
||||
Actions
|
||||
<ng-template #dropdown let-close>
|
||||
<tui-data-list>
|
||||
<tui-opt-group>
|
||||
@if (interface.serviceInterface().type === 'ui') {
|
||||
<a
|
||||
tuiOption
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[href]="actions()"
|
||||
>
|
||||
Launch UI
|
||||
</a>
|
||||
<button tuiOption iconStart="@tui.qr-code" (click)="showQR()">
|
||||
Show QR
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(actions()); close()"
|
||||
>
|
||||
Copy URL
|
||||
</button>
|
||||
}
|
||||
</tui-opt-group>
|
||||
<tui-opt-group><ng-content select="[tuiOption]" /></tui-opt-group>
|
||||
</tui-data-list>
|
||||
</ng-template>
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
text-align: right;
|
||||
grid-area: 1 / 2 / 3 / 3;
|
||||
}
|
||||
|
||||
.desktop {
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [TuiButton, TuiDropdown, TuiDataList],
|
||||
providers: [tuiButtonOptionsProvider({ appearance: 'icon' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceActionsComponent {
|
||||
readonly dialogs = inject(TuiResponsiveDialogService)
|
||||
readonly copyService = inject(CopyService)
|
||||
readonly interface = inject(InterfaceComponent)
|
||||
|
||||
readonly actions = input.required<string>()
|
||||
|
||||
showQR() {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(QRModal), {
|
||||
size: 'auto',
|
||||
label: 'Interface URL',
|
||||
data: this.actions(),
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { AddressItemComponent } from './address-item.component'
|
||||
import { AddressDetails, AddressesService } from './interface.utils'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-address-group',
|
||||
template: `
|
||||
<div>
|
||||
@if (addresses.length && !service.static) {
|
||||
<button
|
||||
class="icon-add-btn"
|
||||
tuiIconButton
|
||||
iconStart="@tui.plus"
|
||||
(click)="service.add()"
|
||||
></button>
|
||||
}
|
||||
<ng-content />
|
||||
</div>
|
||||
@for (address of addresses; track $index) {
|
||||
<app-address-item [label]="address.label" [address]="address.url" />
|
||||
} @empty {
|
||||
@if (!service.static) {
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.align-self]="'flex-start'"
|
||||
(click)="service.add()"
|
||||
>
|
||||
Add Address
|
||||
</button>
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [AddressItemComponent, TuiButton],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styles: `
|
||||
.icon-add-btn {
|
||||
float: right;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class AddressGroupComponent {
|
||||
readonly service = inject(AddressesService)
|
||||
|
||||
@Input({ required: true }) addresses!: AddressDetails[]
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
import { NgIf } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||
import { CopyService } from '@start9labs/shared'
|
||||
import { TuiDialogService, TuiTitle, TuiButton } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
|
||||
import { mask } from 'src/app/utils/mask'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
import { AddressesService } from './interface.utils'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-address-item',
|
||||
template: `
|
||||
<div tuiCell>
|
||||
<tui-badge appearance="success">
|
||||
{{ label }}
|
||||
</tui-badge>
|
||||
<h3 tuiTitle>
|
||||
<span tuiSubtitle>
|
||||
{{ interface.serviceInterface.masked ? mask : address }}
|
||||
</span>
|
||||
</h3>
|
||||
<button
|
||||
*ngIf="interface.serviceInterface.type === 'ui'"
|
||||
tuiIconButton
|
||||
iconStart="@tui.external-link"
|
||||
appearance="icon"
|
||||
(click)="launch(address)"
|
||||
>
|
||||
Launch
|
||||
</button>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.qr-code"
|
||||
appearance="icon"
|
||||
(click)="showQR(address)"
|
||||
>
|
||||
Show QR code
|
||||
</button>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.copy"
|
||||
appearance="icon"
|
||||
(click)="copyService.copy(address)"
|
||||
>
|
||||
Copy URL
|
||||
</button>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="icon"
|
||||
(click)="service.remove()"
|
||||
>
|
||||
Destroy
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
imports: [NgIf, TuiCell, TuiTitle, TuiButton, TuiBadge],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddressItemComponent {
|
||||
private readonly window = inject(WA_WINDOW)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
|
||||
readonly service = inject(AddressesService)
|
||||
readonly copyService = inject(CopyService)
|
||||
readonly interface = inject(InterfaceComponent)
|
||||
|
||||
@Input() label?: string
|
||||
@Input({ required: true }) address!: string
|
||||
|
||||
get mask(): string {
|
||||
return mask(this.address, 64)
|
||||
}
|
||||
|
||||
launch(url: string): void {
|
||||
this.window.open(url, '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
showQR(data: string) {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(QRModal), {
|
||||
size: 'auto',
|
||||
data,
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDialogOptions,
|
||||
TuiIcon,
|
||||
TuiLink,
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_CONFIRM, TuiTooltip } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { defaultIfEmpty, firstValueFrom, map } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { AcmePipe } from 'src/app/routes/portal/components/interfaces/acme.pipe'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { toAcmeName } from 'src/app/utils/acme'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { InterfaceActionsComponent } from './actions.component'
|
||||
import { AddressDetails } from './interface.utils'
|
||||
import { MaskPipe } from './mask.pipe'
|
||||
|
||||
type ClearnetForm = {
|
||||
domain: string
|
||||
acme: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'section[clearnet]',
|
||||
template: `
|
||||
<header>
|
||||
Clearnet
|
||||
<tui-icon [tuiTooltip]="tooltip" />
|
||||
<ng-template #tooltip>
|
||||
Add a clearnet address to expose this interface on the Internet.
|
||||
Clearnet addresses are fully public and not anonymous.
|
||||
<a
|
||||
tuiLink
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#clearnet"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</ng-template>
|
||||
@if (clearnet().length) {
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="accent"
|
||||
[iconStart]="isPublic() ? '@tui.globe-lock' : '@tui.globe'"
|
||||
(click)="toggle()"
|
||||
>
|
||||
Make {{ isPublic() ? 'private' : 'public' }}
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
@if (clearnet().length) {
|
||||
<table [appTable]="['Domain', 'ACME', 'URL', '']">
|
||||
@for (address of clearnet(); track $index) {
|
||||
<tr>
|
||||
<td [style.width.rem]="15">{{ address.label }}</td>
|
||||
<td>{{ address.acme | acme }}</td>
|
||||
<td>{{ address.url | mask }}</td>
|
||||
<td [actions]="address.url">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="primary-destructive"
|
||||
[style.margin-inline-end.rem]="0.5"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
tuiAppearance="action-destructive"
|
||||
iconStart="@tui.trash"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
} @else {
|
||||
<app-placeholder icon="@tui.app-window">
|
||||
No interfaces available
|
||||
<button tuiButton iconStart="@tui.plus" (click)="add()">Add</button>
|
||||
</app-placeholder>
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiLink,
|
||||
TuiDataList,
|
||||
TuiAppearance,
|
||||
PlaceholderComponent,
|
||||
TableComponent,
|
||||
MaskPipe,
|
||||
AcmePipe,
|
||||
InterfaceActionsComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceClearnetComponent {
|
||||
private readonly dialogs = inject(TuiResponsiveDialogService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly interface = inject(InterfaceComponent)
|
||||
readonly isPublic = computed(() => this.interface.serviceInterface().public)
|
||||
|
||||
readonly clearnet = input.required<readonly AddressDetails[]>()
|
||||
readonly acme = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('serverInfo', 'network', 'acme')
|
||||
.pipe(map(acme => Object.keys(acme))),
|
||||
{ initialValue: [] },
|
||||
)
|
||||
|
||||
async remove({ url }: AddressDetails) {
|
||||
const confirm = await firstValueFrom(
|
||||
this.dialogs
|
||||
.open(TUI_CONFIRM, { label: 'Are you sure?', size: 's' })
|
||||
.pipe(defaultIfEmpty(false)),
|
||||
)
|
||||
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
const params = { domain: new URL(url).hostname }
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgRemoveDomain({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverRemoveDomain(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async toggle() {
|
||||
const loader = this.loader
|
||||
.open(`Making ${this.isPublic() ? 'private' : 'public'}`)
|
||||
.subscribe()
|
||||
|
||||
const params = {
|
||||
internalPort: this.interface.serviceInterface().addressInfo.internalPort,
|
||||
public: !this.isPublic(),
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgBindingSetPubic({
|
||||
...params,
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
package: this.interface.packageId(),
|
||||
})
|
||||
} else {
|
||||
await this.api.serverBindingSetPubic(params)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async add() {
|
||||
const options: Partial<TuiDialogOptions<FormContext<ClearnetForm>>> = {
|
||||
label: 'Select Domain/Subdomain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
domain: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description: 'The domain or subdomain you want to use',
|
||||
placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`,
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
acme: ISB.Value.select({
|
||||
name: 'ACME Provider',
|
||||
description:
|
||||
'Select which ACME provider to use for obtaining your SSL certificate. Add new ACME providers in the System tab. Optionally use your system Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
|
||||
values: this.acme().reduce(
|
||||
(obj, url) => ({
|
||||
...obj,
|
||||
[url]: toAcmeName(url),
|
||||
}),
|
||||
{ none: 'None (use system Root CA)' } as Record<string, string>,
|
||||
),
|
||||
default: '',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => this.save(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
this.formDialog.open(FormComponent, options)
|
||||
}
|
||||
|
||||
private async save(domainInfo: ClearnetForm): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
const { domain, acme } = domainInfo
|
||||
|
||||
const params = {
|
||||
domain,
|
||||
acme: acme === 'none' ? null : acme,
|
||||
private: false,
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgAddDomain({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddDomain(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { Directive, Input } from '@angular/core'
|
||||
import { AddressesService } from '../interface.utils'
|
||||
import { inject } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { toAcmeName } from 'src/app/utils/acme'
|
||||
|
||||
type ClearnetForm = {
|
||||
domain: string
|
||||
acme: string
|
||||
}
|
||||
|
||||
@Directive({
|
||||
standalone: true,
|
||||
selector: '[clearnetAddresses]',
|
||||
providers: [
|
||||
{ provide: AddressesService, useExisting: ClearnetAddressesDirective },
|
||||
],
|
||||
})
|
||||
export class ClearnetAddressesDirective implements AddressesService {
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
@Input({ required: true }) acme!: string[]
|
||||
|
||||
static = false
|
||||
|
||||
async add() {
|
||||
const options: Partial<TuiDialogOptions<FormContext<ClearnetForm>>> = {
|
||||
label: 'Select Domain/Subdomain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
domain: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description: 'The domain or subdomain you want to use',
|
||||
placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`,
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
acme: ISB.Value.select({
|
||||
name: 'ACME Provider',
|
||||
description:
|
||||
'Select which ACME provider to use for obtaining your SSL certificate. Add new ACME providers in the System tab. Optionally use your system Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
|
||||
values: this.acme.reduce(
|
||||
(obj, url) => ({
|
||||
...obj,
|
||||
[url]: toAcmeName(url),
|
||||
}),
|
||||
{ none: 'None (use system Root CA)' } as Record<string, string>,
|
||||
),
|
||||
default: '',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => this.save(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
this.formDialog.open(FormComponent, options)
|
||||
}
|
||||
|
||||
async remove() {}
|
||||
|
||||
private async save(domainInfo: ClearnetForm): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
const { domain, acme } = domainInfo
|
||||
|
||||
const params = {
|
||||
domain,
|
||||
acme: acme === 'none' ? null : acme,
|
||||
private: false,
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.interface.packageId) {
|
||||
await this.api.pkgAddDomain({
|
||||
...params,
|
||||
package: this.interface.packageId,
|
||||
host: this.interface.serviceInterface.addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddDomain(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Directive } from '@angular/core'
|
||||
import { AddressesService } from '../interface.utils'
|
||||
|
||||
@Directive({
|
||||
standalone: true,
|
||||
selector: '[localAddresses]',
|
||||
providers: [
|
||||
{ provide: AddressesService, useExisting: LocalAddressesDirective },
|
||||
],
|
||||
})
|
||||
export class LocalAddressesDirective implements AddressesService {
|
||||
static = true
|
||||
async add() {}
|
||||
async remove() {}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Directive, inject } from '@angular/core'
|
||||
import { AddressesService } from '../interface.utils'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
import { FormComponent, FormContext } from '../../form.component'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
|
||||
type OnionForm = {
|
||||
key: string
|
||||
}
|
||||
|
||||
@Directive({
|
||||
standalone: true,
|
||||
selector: '[torAddresses]',
|
||||
providers: [
|
||||
{ provide: AddressesService, useExisting: TorAddressesDirective },
|
||||
],
|
||||
})
|
||||
export class TorAddressesDirective implements AddressesService {
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
static = false
|
||||
|
||||
async add() {
|
||||
const options: Partial<TuiDialogOptions<FormContext<OnionForm>>> = {
|
||||
label: 'Select Domain/Subdomain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: 'Private Key (optional)',
|
||||
description:
|
||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.',
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.base64],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => this.save(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
this.formDialog.open(FormComponent, options)
|
||||
}
|
||||
|
||||
async remove() {}
|
||||
|
||||
private async save(form: OnionForm): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
try {
|
||||
let onion = form.key
|
||||
? await this.api.addTorKey({ key: form.key })
|
||||
: await this.api.generateTorKey({})
|
||||
onion = `${onion}.onion`
|
||||
|
||||
if (this.interface.packageId) {
|
||||
await this.api.pkgAddOnion({
|
||||
onion,
|
||||
package: this.interface.packageId,
|
||||
host: this.interface.serviceInterface.addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddOnion({ onion })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +1,42 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiSurface } from '@taiga-ui/core'
|
||||
import { TuiCardLarge } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { AddressGroupComponent } from 'src/app/routes/portal/components/interfaces/address-group.component'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { ClearnetAddressesDirective } from './directives/clearnet.directive'
|
||||
import { TorAddressesDirective } from './directives/tor.directive'
|
||||
import { LocalAddressesDirective } from './directives/local.directive'
|
||||
import { AddressDetails } from './interface.utils'
|
||||
import { map } from 'rxjs'
|
||||
import { ChangeDetectionStrategy, Component, input, Input } from '@angular/core'
|
||||
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { InterfaceClearnetComponent } from 'src/app/routes/portal/components/interfaces/clearnet.component'
|
||||
import { InterfaceLocalComponent } from 'src/app/routes/portal/components/interfaces/local.component'
|
||||
import { InterfaceTorComponent } from 'src/app/routes/portal/components/interfaces/tor.component'
|
||||
import { MappedServiceInterface } from './interface.utils'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-interface',
|
||||
template: `
|
||||
<h3 class="g-title">Clearnet</h3>
|
||||
<app-address-group
|
||||
*ngIf="acme$ | async as acme"
|
||||
clearnetAddresses
|
||||
tuiCardLarge="compact"
|
||||
tuiSurface="floating"
|
||||
[acme]="acme"
|
||||
[addresses]="serviceInterface.addresses.clearnet"
|
||||
>
|
||||
<em>
|
||||
Add a clearnet address to expose this interface on the Internet.
|
||||
Clearnet addresses are fully public and not anonymous.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#clearnet"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<strong>Learn More</strong>
|
||||
</a>
|
||||
</em>
|
||||
</app-address-group>
|
||||
|
||||
<h3 class="g-title">Tor</h3>
|
||||
<app-address-group
|
||||
torAddresses
|
||||
tuiCardLarge="compact"
|
||||
tuiSurface="floating"
|
||||
[addresses]="serviceInterface.addresses.tor"
|
||||
>
|
||||
<em>
|
||||
Add an onion address to anonymously expose this interface on the
|
||||
darknet. Onion addresses can only be reached over the Tor network.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#tor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<strong>Learn More</strong>
|
||||
</a>
|
||||
</em>
|
||||
</app-address-group>
|
||||
|
||||
<h3 class="g-title">Local</h3>
|
||||
<app-address-group
|
||||
localAddresses
|
||||
tuiCardLarge="compact"
|
||||
tuiSurface="floating"
|
||||
[addresses]="serviceInterface.addresses.local"
|
||||
>
|
||||
<em>
|
||||
Local addresses can only be accessed by devices connected to the same
|
||||
LAN as your server, either directly or using a VPN.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#local"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<strong>Learn More</strong>
|
||||
</a>
|
||||
</em>
|
||||
</app-address-group>
|
||||
<section [clearnet]="serviceInterface().addresses.clearnet"></section>
|
||||
<section [tor]="serviceInterface().addresses.tor"></section>
|
||||
<section [local]="serviceInterface().addresses.local"></section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
section {
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
:host-context(tui-root:not(._mobile)) section ::ng-deep > header {
|
||||
background: none;
|
||||
}
|
||||
`,
|
||||
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
AddressGroupComponent,
|
||||
TuiCardLarge,
|
||||
TuiSurface,
|
||||
ClearnetAddressesDirective,
|
||||
TorAddressesDirective,
|
||||
LocalAddressesDirective,
|
||||
InterfaceClearnetComponent,
|
||||
InterfaceTorComponent,
|
||||
InterfaceLocalComponent,
|
||||
],
|
||||
})
|
||||
export class InterfaceComponent {
|
||||
readonly acme$ = inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('serverInfo', 'network', 'acme')
|
||||
.pipe(map(acme => Object.keys(acme)))
|
||||
|
||||
@Input() packageId?: string
|
||||
@Input({ required: true }) serviceInterface!: MappedServiceInterface
|
||||
}
|
||||
|
||||
export type MappedServiceInterface = T.ServiceInterface & {
|
||||
addresses: {
|
||||
clearnet: AddressDetails[]
|
||||
local: AddressDetails[]
|
||||
tor: AddressDetails[]
|
||||
}
|
||||
readonly packageId = input('')
|
||||
readonly serviceInterface = input.required<MappedServiceInterface>()
|
||||
}
|
||||
|
||||
@@ -29,10 +29,7 @@ export function getAddresses(
|
||||
tor: AddressDetails[]
|
||||
} {
|
||||
const addressInfo = serviceInterface.addressInfo
|
||||
|
||||
let hostnames = host.hostnameInfo[addressInfo.internalPort]
|
||||
|
||||
hostnames = hostnames.filter(
|
||||
const hostnames = host.hostnameInfo[addressInfo.internalPort].filter(
|
||||
h =>
|
||||
config.isLocalhost() ||
|
||||
h.kind !== 'ip' ||
|
||||
@@ -121,7 +118,17 @@ export function getAddresses(
|
||||
}
|
||||
}
|
||||
|
||||
export type MappedServiceInterface = T.ServiceInterface & {
|
||||
public: boolean
|
||||
addresses: {
|
||||
clearnet: AddressDetails[]
|
||||
local: AddressDetails[]
|
||||
tor: AddressDetails[]
|
||||
}
|
||||
}
|
||||
|
||||
export type AddressDetails = {
|
||||
label: string
|
||||
url: string
|
||||
acme?: string | null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { TuiIcon, TuiLink } from '@taiga-ui/core'
|
||||
import { TuiTooltip } from '@taiga-ui/kit'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { InterfaceActionsComponent } from './actions.component'
|
||||
import { AddressDetails } from './interface.utils'
|
||||
import { MaskPipe } from './mask.pipe'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'section[local]',
|
||||
template: `
|
||||
<header>
|
||||
Local
|
||||
<tui-icon [tuiTooltip]="tooltip" />
|
||||
<ng-template #tooltip>
|
||||
Local addresses can only be accessed by devices connected to the same
|
||||
LAN as your server, either directly or using a VPN.
|
||||
<a
|
||||
tuiLink
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#local"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</ng-template>
|
||||
</header>
|
||||
<table [appTable]="['Network Interface', 'URL', '']">
|
||||
@for (address of local(); track $index) {
|
||||
<tr>
|
||||
<td [style.width.rem]="15">{{ address.label }}</td>
|
||||
<td>{{ address.url | mask }}</td>
|
||||
<td [actions]="address.url"></td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiLink,
|
||||
TableComponent,
|
||||
InterfaceActionsComponent,
|
||||
MaskPipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceLocalComponent {
|
||||
readonly local = input.required<readonly AddressDetails[]>()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'mask',
|
||||
})
|
||||
export class MaskPipe implements PipeTransform {
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
transform(value: string): string {
|
||||
return this.interface.serviceInterface().masked
|
||||
? '●'.repeat(Math.min(64, value.length))
|
||||
: value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
TuiDialogOptions,
|
||||
TuiIcon,
|
||||
TuiLink,
|
||||
TuiOption,
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_CONFIRM, TuiTooltip } from '@taiga-ui/kit'
|
||||
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { InterfaceActionsComponent } from './actions.component'
|
||||
import { AddressDetails } from './interface.utils'
|
||||
import { MaskPipe } from './mask.pipe'
|
||||
|
||||
type OnionForm = {
|
||||
key: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'section[tor]',
|
||||
template: `
|
||||
<header>
|
||||
Tor
|
||||
<tui-icon [tuiTooltip]="tooltip" />
|
||||
<ng-template #tooltip>
|
||||
Add an onion address to anonymously expose this interface on the
|
||||
darknet. Onion addresses can only be reached over the Tor network.
|
||||
<a
|
||||
tuiLink
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#tor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</ng-template>
|
||||
</header>
|
||||
@if (tor().length) {
|
||||
<table [appTable]="['Protocol', 'URL', '']">
|
||||
@for (address of tor(); track $index) {
|
||||
<tr>
|
||||
<td [style.width.rem]="15">{{ address.label }}</td>
|
||||
<td>{{ address.url | mask }}</td>
|
||||
<td [actions]="address.url">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="primary-destructive"
|
||||
[style.margin-inline-end.rem]="0.5"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
tuiAppearance="action-destructive"
|
||||
iconStart="@tui.trash"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
} @else {
|
||||
<app-placeholder icon="@tui.app-window">
|
||||
No Tor addresses available
|
||||
<button tuiButton iconStart="@tui.plus">Add</button>
|
||||
</app-placeholder>
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiLink,
|
||||
TuiAppearance,
|
||||
TuiOption,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
MaskPipe,
|
||||
InterfaceActionsComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceTorComponent {
|
||||
private readonly dialogs = inject(TuiResponsiveDialogService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
readonly tor = input.required<readonly AddressDetails[]>()
|
||||
|
||||
async remove({ url }: AddressDetails) {
|
||||
const confirm = await firstValueFrom(
|
||||
this.dialogs
|
||||
.open(TUI_CONFIRM, { label: 'Are you sure?', size: 's' })
|
||||
.pipe(defaultIfEmpty(false)),
|
||||
)
|
||||
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
const params = { onion: new URL(url).hostname }
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgRemoveOnion({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverRemoveOnion(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async add() {
|
||||
const options: Partial<TuiDialogOptions<FormContext<OnionForm>>> = {
|
||||
label: 'Select Domain/Subdomain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: 'Private Key (optional)',
|
||||
description:
|
||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.',
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.base64],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => this.save(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
this.formDialog.open(FormComponent, options)
|
||||
}
|
||||
|
||||
private async save(form: OnionForm): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
try {
|
||||
let onion = form.key
|
||||
? await this.api.addTorKey({ key: form.key })
|
||||
: await this.api.generateTorKey({})
|
||||
onion = `${onion}.onion`
|
||||
|
||||
if (this.interface.packageId) {
|
||||
await this.api.pkgAddOnion({
|
||||
onion,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.serviceInterface().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddOnion({ onion })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'table[appTable]',
|
||||
template: `
|
||||
<thead>
|
||||
<tr>
|
||||
@for (header of appTable(); track $index) {
|
||||
<th>{{ header }}</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><ng-content /></tbody>
|
||||
`,
|
||||
host: { class: 'g-table' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TableComponent {
|
||||
readonly appTable = input.required<readonly string[]>()
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { QrCodeModule } from 'ng-qrcode'
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'qr',
|
||||
template: '<qr-code [value]="context.data" size="400"></qr-code>',
|
||||
template: '<qr-code [value]="context.data" size="350"></qr-code>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [QrCodeModule],
|
||||
})
|
||||
|
||||
@@ -73,7 +73,7 @@ export default class DashboardComponent {
|
||||
getInstalledPrimaryStatus(b) > getInstalledPrimaryStatus(a) ? -1 : 1
|
||||
|
||||
readonly uptime: TuiComparator<any> = (a, b) =>
|
||||
a.startedAt || '' > b.startedAt || '' ? -1 : 1
|
||||
a.status.started || '' > a.status.started || '' ? -1 : 1
|
||||
|
||||
sorter = this.name
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import { StatusComponent } from './status.component'
|
||||
<a [routerLink]="routerLink">{{ manifest.title }}</a>
|
||||
</td>
|
||||
<td [style.grid-area]="'2 / 2'">{{ manifest.version }}</td>
|
||||
<td [appUptime]="$any(pkg).startedAt"></td>
|
||||
<td [appUptime]="$any(pkg.status).started"></td>
|
||||
<td
|
||||
[style.grid-area]="'3 / 2'"
|
||||
appStatus
|
||||
|
||||
@@ -11,10 +11,10 @@ import { getPkgId } from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { getAddresses } from '../../../components/interfaces/interface.utils'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
|
||||
@@ -99,6 +99,10 @@ const ICONS = {
|
||||
|
||||
.active {
|
||||
color: var(--tui-text-primary);
|
||||
|
||||
[tuiTitle] {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
|
||||
@@ -5,11 +5,11 @@ import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Observable, map } from 'rxjs'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import {
|
||||
InterfaceComponent,
|
||||
getAddresses,
|
||||
MappedServiceInterface,
|
||||
} from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||
} from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function mask(val: string, max: number = Infinity): string {
|
||||
return '●'.repeat(Math.min(max, val.length))
|
||||
}
|
||||
@@ -120,7 +120,7 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
> table {
|
||||
> table[tuiTable] {
|
||||
margin: 0 -0.5rem;
|
||||
|
||||
td:empty,
|
||||
@@ -131,15 +131,15 @@ hr {
|
||||
|
||||
> header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0 0 auto 0;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--tui-background-neutral-1);
|
||||
gap: 0.25rem;
|
||||
padding: 0 1rem;
|
||||
font: var(--tui-font-text-l);
|
||||
font-weight: bold;
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
|
||||
> footer {
|
||||
@@ -152,45 +152,51 @@ hr {
|
||||
|
||||
.g-table:not([tuiTable]) {
|
||||
width: stretch;
|
||||
min-width: 40rem;
|
||||
border: 1px solid var(--tui-background-neutral-1);
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
border-radius: var(--tui-radius-s);
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 0 1px var(--tui-background-neutral-1);
|
||||
|
||||
td,
|
||||
th {
|
||||
position: relative;
|
||||
font: var(--tui-font-text-s);
|
||||
text-align: left;
|
||||
height: 2rem;
|
||||
padding: 0 0.25rem;
|
||||
box-shadow: inset 0 -1px var(--tui-background-neutral-1);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--tui-background-neutral-1);
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--tui-background-neutral-1);
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tui-root._mobile & {
|
||||
min-width: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
color: var(--tui-text-secondary);
|
||||
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
tr {
|
||||
position: relative;
|
||||
display: grid;
|
||||
border-radius: var(--tui-radius-l);
|
||||
padding: 0.375rem 0.5rem;
|
||||
// TODO: Theme
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.5rem 0;
|
||||
box-shadow: inset 0 -1px var(--tui-background-neutral-1);
|
||||
|
||||
&:last-child {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
tr:has(:checked) {
|
||||
@@ -200,10 +206,13 @@ hr {
|
||||
td,
|
||||
th {
|
||||
position: static;
|
||||
height: auto;
|
||||
min-height: 1.5rem;
|
||||
align-content: center;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
color: var(--tui-text-primary);
|
||||
}
|
||||
|
||||
&:not([tuiFade]) {
|
||||
overflow: hidden;
|
||||
|
||||
Reference in New Issue
Block a user