homebridge-roborock-vacuum 1.2.4 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Plan.md +54 -0
- package/config.schema.json +15 -6
- package/dist/crypto.js +62 -0
- package/dist/crypto.js.map +1 -0
- package/dist/platform.js +16 -3
- package/dist/platform.js.map +1 -1
- package/dist/ui/index.js +161 -0
- package/dist/ui/index.js.map +1 -0
- package/homebridge-ui/public/index.html +59 -0
- package/homebridge-ui/public/index.js +250 -0
- package/homebridge-ui/public/styles.css +184 -0
- package/homebridge-ui/server.js +3 -0
- package/package.json +4 -2
- package/roborockLib/lib/deviceFeatures.js +40 -0
- package/roborockLib/lib/roborockAuth.js +147 -0
- package/roborockLib/roborockAPI.js +186 -29
- package/roborockLib/test.js +2 -1
- package/roborockLib/data/UserData +0 -4
- package/roborockLib/data/clientID +0 -4
- package/roborockLib/userdata.json +0 -24
package/Plan.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Plan
|
|
2
|
+
|
|
3
|
+
This plan adds a 2FA Email authentication flow, a Homebridge Config UI settings page (with a “Send 2FA Email” button and toast-based status messages), and encrypted token persistence in `config.json` so Docker restarts do not force re-login. The flow follows ioBroker’s 2FA pattern and integrates with the existing login/state logic.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
- UI provides a “Send 2FA Email” button and shows login results via toast.
|
|
7
|
+
- UI calls the plugin UI backend directly for login and 2FA.
|
|
8
|
+
- Token is encrypted before storing in `config.json`; if present and valid, skip login.
|
|
9
|
+
- Code comments and UI copy are in English.
|
|
10
|
+
|
|
11
|
+
## Scope
|
|
12
|
+
- In: 2FA Email flow, UI backend endpoints, UI form, encrypted token persistence, login short-circuit, toast UI.
|
|
13
|
+
- Out: Device control, MQTT/data handling changes.
|
|
14
|
+
|
|
15
|
+
## Files and entry points
|
|
16
|
+
- roborockLib/roborockAPI.js
|
|
17
|
+
- src/platform.ts
|
|
18
|
+
- config.schema.json
|
|
19
|
+
- package.json (Homebridge UI integration)
|
|
20
|
+
- UI assets/backend (e.g. src/ui/ or ui/)
|
|
21
|
+
|
|
22
|
+
## Data model / API changes
|
|
23
|
+
- Add `encryptedToken` (and any needed expiry metadata) to config.
|
|
24
|
+
- Add encryption/decryption logic for the token (key derived from Homebridge context or user-provided secret).
|
|
25
|
+
- UI backend endpoints:
|
|
26
|
+
- POST /auth/send-2fa-email
|
|
27
|
+
- POST /auth/verify-2fa-code
|
|
28
|
+
- POST /auth/login
|
|
29
|
+
- GET /auth/status
|
|
30
|
+
- UI uses toast notifications for success/failure/status.
|
|
31
|
+
|
|
32
|
+
## Action items
|
|
33
|
+
[ ] Review ioBroker `httpApi.ts` 2FA Email flow and map required endpoints/params to current login flow.
|
|
34
|
+
[ ] Implement 2FA flow in `roborockLib/roborockAPI.js` (send email, verify code, clear status/error handling).
|
|
35
|
+
[ ] Add encrypted token persistence and recovery flow (decrypt on startup; fallback to login on failure/expiry).
|
|
36
|
+
[ ] Update `src/platform.ts` to short-circuit login when a valid token is present.
|
|
37
|
+
[ ] Build a dedicated Homebridge Config UI page with English copy, “Send 2FA Email” button, 2FA input, and toast status display.
|
|
38
|
+
[ ] Add UI backend endpoints for send/verify/login and update config with encrypted token.
|
|
39
|
+
[ ] Update `config.schema.json` and types to align UI and config fields.
|
|
40
|
+
|
|
41
|
+
## Testing and validation
|
|
42
|
+
- Clicking “Send 2FA Email” triggers email delivery and shows toast feedback.
|
|
43
|
+
- 2FA code verification returns token and displays success toast.
|
|
44
|
+
- Encrypted token in config allows login skip after restart.
|
|
45
|
+
- Decryption failure or token expiry falls back to full login and refreshes token.
|
|
46
|
+
|
|
47
|
+
## Risks and edge cases
|
|
48
|
+
- 2FA endpoints may differ across regions/accounts.
|
|
49
|
+
- Key management errors can make tokens undecryptable.
|
|
50
|
+
- Token expiry must be detected and trigger re-login.
|
|
51
|
+
|
|
52
|
+
## Open questions
|
|
53
|
+
- Should the encryption key be derived from Homebridge machine identity or a user-provided secret?
|
|
54
|
+
- Should toast notifications use a lightweight custom component or an existing UI library?
|
package/config.schema.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pluginAlias": "RoborockVacuumPlatform",
|
|
3
3
|
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"customUi": true,
|
|
6
|
+
"customUiPath": "./homebridge-ui",
|
|
4
7
|
"schema": {
|
|
5
8
|
"type": "object",
|
|
6
9
|
"properties": {
|
|
@@ -16,15 +19,21 @@
|
|
|
16
19
|
"placeholder": "Password",
|
|
17
20
|
"description": "Roborock account password"
|
|
18
21
|
},
|
|
22
|
+
"encryptedToken": {
|
|
23
|
+
"title": "Encrypted Token",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Encrypted Roborock session data (managed by the UI)",
|
|
26
|
+
"readOnly": true
|
|
27
|
+
},
|
|
19
28
|
"skipDevices": {
|
|
20
|
-
"title": "Skip
|
|
29
|
+
"title": "Skip Devices",
|
|
21
30
|
"type": "string",
|
|
22
|
-
"description": "
|
|
31
|
+
"description": "Comma-separated device IDs to skip"
|
|
23
32
|
},
|
|
24
33
|
"baseURL": {
|
|
25
34
|
"type": "string",
|
|
26
|
-
"default": "usiot.roborock.com",
|
|
27
|
-
"description": "Roborock API endpoint
|
|
35
|
+
"default": "https://usiot.roborock.com",
|
|
36
|
+
"description": "Roborock API endpoint: https://usiot.roborock.com (US), https://euiot.roborock.com (EU), https://cniot.roborock.com (CN), https://api.roborock.com (Asia)"
|
|
28
37
|
}
|
|
29
38
|
,
|
|
30
39
|
"debugMode": {
|
|
@@ -34,6 +43,6 @@
|
|
|
34
43
|
"default": false
|
|
35
44
|
}
|
|
36
45
|
},
|
|
37
|
-
"required": ["email"
|
|
46
|
+
"required": ["email"]
|
|
38
47
|
}
|
|
39
|
-
}
|
|
48
|
+
}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.encryptSession = encryptSession;
|
|
7
|
+
exports.decryptSession = decryptSession;
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const KEY_FILENAME = 'roborock.token.key';
|
|
12
|
+
function loadOrCreateKey(storagePath) {
|
|
13
|
+
const keyPath = path_1.default.join(storagePath, KEY_FILENAME);
|
|
14
|
+
try {
|
|
15
|
+
const existing = fs_1.default.readFileSync(keyPath);
|
|
16
|
+
if (existing.length === 32) {
|
|
17
|
+
return existing;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// Ignore and create a new key.
|
|
22
|
+
}
|
|
23
|
+
const key = crypto_1.default.randomBytes(32);
|
|
24
|
+
fs_1.default.mkdirSync(storagePath, { recursive: true });
|
|
25
|
+
fs_1.default.writeFileSync(keyPath, key, { mode: 0o600 });
|
|
26
|
+
return key;
|
|
27
|
+
}
|
|
28
|
+
function encryptSession(session, storagePath) {
|
|
29
|
+
const key = loadOrCreateKey(storagePath);
|
|
30
|
+
const iv = crypto_1.default.randomBytes(12);
|
|
31
|
+
const cipher = crypto_1.default.createCipheriv('aes-256-gcm', key, iv);
|
|
32
|
+
const plaintext = Buffer.from(JSON.stringify(session), 'utf8');
|
|
33
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
34
|
+
const tag = cipher.getAuthTag();
|
|
35
|
+
const payload = {
|
|
36
|
+
iv: iv.toString('base64'),
|
|
37
|
+
tag: tag.toString('base64'),
|
|
38
|
+
data: ciphertext.toString('base64'),
|
|
39
|
+
};
|
|
40
|
+
return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
|
|
41
|
+
}
|
|
42
|
+
function decryptSession(encrypted, storagePath) {
|
|
43
|
+
if (!encrypted) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const key = loadOrCreateKey(storagePath);
|
|
48
|
+
const raw = Buffer.from(encrypted, 'base64').toString('utf8');
|
|
49
|
+
const payload = JSON.parse(raw);
|
|
50
|
+
const iv = Buffer.from(payload.iv, 'base64');
|
|
51
|
+
const tag = Buffer.from(payload.tag, 'base64');
|
|
52
|
+
const ciphertext = Buffer.from(payload.data, 'base64');
|
|
53
|
+
const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', key, iv);
|
|
54
|
+
decipher.setAuthTag(tag);
|
|
55
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
56
|
+
return JSON.parse(plaintext);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
package/dist/platform.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const vacuum_accessory_1 = __importDefault(require("./vacuum_accessory"));
|
|
7
7
|
const logger_1 = __importDefault(require("./logger"));
|
|
8
8
|
const settings_1 = require("./settings");
|
|
9
|
+
const crypto_1 = require("./crypto");
|
|
9
10
|
const Roborock = require("../roborockLib/roborockAPI").Roborock;
|
|
10
11
|
/**
|
|
11
12
|
* Roborock App Platform Plugin for Homebridge
|
|
@@ -35,7 +36,19 @@ class RoborockPlatform {
|
|
|
35
36
|
const password = this.platformConfig.password;
|
|
36
37
|
const baseURL = this.platformConfig.baseURL;
|
|
37
38
|
const debugMode = this.platformConfig.debugMode;
|
|
38
|
-
|
|
39
|
+
const storagePath = this.api.user.storagePath();
|
|
40
|
+
const decryptedSession = this.platformConfig.encryptedToken
|
|
41
|
+
? (0, crypto_1.decryptSession)(this.platformConfig.encryptedToken, storagePath)
|
|
42
|
+
: null;
|
|
43
|
+
this.roborockAPI = new Roborock({
|
|
44
|
+
username: username,
|
|
45
|
+
password: password,
|
|
46
|
+
debug: debugMode,
|
|
47
|
+
baseURL: baseURL,
|
|
48
|
+
log: this.log,
|
|
49
|
+
userData: decryptedSession,
|
|
50
|
+
storagePath: storagePath,
|
|
51
|
+
});
|
|
39
52
|
/**
|
|
40
53
|
* When this event is fired it means Homebridge has restored all cached accessories from disk.
|
|
41
54
|
* Dynamic Platform plugins should only register new accessories after this event was fired,
|
|
@@ -62,9 +75,9 @@ class RoborockPlatform {
|
|
|
62
75
|
+ 'Please set the field `email` in your config and restart Homebridge.');
|
|
63
76
|
return;
|
|
64
77
|
}
|
|
65
|
-
if (!this.platformConfig.password) {
|
|
78
|
+
if (!this.platformConfig.password && !this.platformConfig.encryptedToken) {
|
|
66
79
|
this.log.error('Password is not configured - aborting plugin start. '
|
|
67
|
-
+ 'Please set
|
|
80
|
+
+ 'Please set `password` or complete login in the Config UI.');
|
|
68
81
|
return;
|
|
69
82
|
}
|
|
70
83
|
const self = this;
|
package/dist/platform.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":";;;;;AAWA,0EAAyD;AAEzD,sDAA8C;AAE9C,yCAGoB;
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const plugin_ui_utils_1 = require("@homebridge/plugin-ui-utils");
|
|
10
|
+
const crypto_2 = require("../crypto");
|
|
11
|
+
const roborockAuth = require('../../roborockLib/lib/roborockAuth');
|
|
12
|
+
class RoborockUiServer extends plugin_ui_utils_1.HomebridgePluginUiServer {
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.onRequest('/auth/send-2fa-email', this.sendTwoFactorEmail.bind(this));
|
|
16
|
+
this.onRequest('/auth/verify-2fa-code', this.verifyTwoFactorCode.bind(this));
|
|
17
|
+
this.onRequest('/auth/login', this.loginWithPassword.bind(this));
|
|
18
|
+
this.onRequest('/auth/logout', this.logout.bind(this));
|
|
19
|
+
this.ready();
|
|
20
|
+
}
|
|
21
|
+
getStoragePath() {
|
|
22
|
+
return this.homebridgeStoragePath || process.cwd();
|
|
23
|
+
}
|
|
24
|
+
async getClientId() {
|
|
25
|
+
const storagePath = this.getStoragePath();
|
|
26
|
+
if (storagePath) {
|
|
27
|
+
const clientIdPath = path_1.default.join(storagePath, 'roborock.clientID');
|
|
28
|
+
try {
|
|
29
|
+
const stored = JSON.parse(fs_1.default.readFileSync(clientIdPath, 'utf8'));
|
|
30
|
+
if (stored && stored.val) {
|
|
31
|
+
return stored.val;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
// Ignore and generate a new client ID.
|
|
36
|
+
}
|
|
37
|
+
const clientId = crypto_1.default.randomUUID();
|
|
38
|
+
fs_1.default.mkdirSync(storagePath, { recursive: true });
|
|
39
|
+
fs_1.default.writeFileSync(clientIdPath, JSON.stringify({ val: clientId, ack: true }, null, 2), 'utf8');
|
|
40
|
+
return clientId;
|
|
41
|
+
}
|
|
42
|
+
return crypto_1.default.randomUUID();
|
|
43
|
+
}
|
|
44
|
+
async buildLoginApi(config) {
|
|
45
|
+
const clientID = await this.getClientId();
|
|
46
|
+
return roborockAuth.createLoginApi({
|
|
47
|
+
baseURL: config.baseURL || 'usiot.roborock.com',
|
|
48
|
+
username: config.email,
|
|
49
|
+
clientID,
|
|
50
|
+
language: 'en',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async sendTwoFactorEmail(payload) {
|
|
54
|
+
const email = payload.email;
|
|
55
|
+
if (!email) {
|
|
56
|
+
return { ok: false, message: 'Email is required.' };
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const loginApi = await this.buildLoginApi({ email, baseURL: payload.baseURL });
|
|
60
|
+
await roborockAuth.requestEmailCode(loginApi, email);
|
|
61
|
+
return { ok: true, message: 'Verification email sent.' };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('2FA email request failed:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
65
|
+
return { ok: false, message: (error === null || error === void 0 ? void 0 : error.message) || 'Failed to send verification email.' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async verifyTwoFactorCode(payload) {
|
|
69
|
+
const email = payload.email;
|
|
70
|
+
if (!email) {
|
|
71
|
+
return { ok: false, message: 'Email is required.' };
|
|
72
|
+
}
|
|
73
|
+
if (!payload.code) {
|
|
74
|
+
return { ok: false, message: 'Verification code is required.' };
|
|
75
|
+
}
|
|
76
|
+
let loginResult;
|
|
77
|
+
try {
|
|
78
|
+
const loginApi = await this.buildLoginApi({ email, baseURL: payload.baseURL });
|
|
79
|
+
const nonce = this.buildNonce();
|
|
80
|
+
const signData = await roborockAuth.signRequest(loginApi, nonce);
|
|
81
|
+
if (!signData || !signData.k) {
|
|
82
|
+
return { ok: false, message: 'Failed to create login signature.' };
|
|
83
|
+
}
|
|
84
|
+
const region = roborockAuth.getRegionConfig(payload.baseURL || 'usiot.roborock.com');
|
|
85
|
+
loginResult = await roborockAuth.loginWithCode(loginApi, {
|
|
86
|
+
email,
|
|
87
|
+
code: payload.code,
|
|
88
|
+
country: region.country,
|
|
89
|
+
countryCode: region.countryCode,
|
|
90
|
+
k: signData.k,
|
|
91
|
+
s: nonce,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error('2FA verification request failed:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
96
|
+
return { ok: false, message: (error === null || error === void 0 ? void 0 : error.message) || 'Verification failed.' };
|
|
97
|
+
}
|
|
98
|
+
if (loginResult && loginResult.code === 200 && loginResult.data) {
|
|
99
|
+
const encrypted = (0, crypto_2.encryptSession)(loginResult.data, this.getStoragePath());
|
|
100
|
+
return { ok: true, message: 'Login completed and token saved.', encryptedToken: encrypted };
|
|
101
|
+
}
|
|
102
|
+
console.error('2FA verification failed:', loginResult);
|
|
103
|
+
return { ok: false, message: (loginResult === null || loginResult === void 0 ? void 0 : loginResult.msg) || 'Verification failed.' };
|
|
104
|
+
}
|
|
105
|
+
async loginWithPassword(payload) {
|
|
106
|
+
const email = payload.email;
|
|
107
|
+
const password = payload.password;
|
|
108
|
+
if (!email || !password) {
|
|
109
|
+
return { ok: false, message: 'Email and password are required.' };
|
|
110
|
+
}
|
|
111
|
+
let loginResult;
|
|
112
|
+
try {
|
|
113
|
+
const loginApi = await this.buildLoginApi({ email, baseURL: payload.baseURL });
|
|
114
|
+
const nonce = this.buildNonce();
|
|
115
|
+
const signData = await roborockAuth.signRequest(loginApi, nonce);
|
|
116
|
+
if (!signData || !signData.k) {
|
|
117
|
+
return { ok: false, message: 'Failed to create login signature.' };
|
|
118
|
+
}
|
|
119
|
+
loginResult = await roborockAuth.loginByPassword(loginApi, {
|
|
120
|
+
email,
|
|
121
|
+
password,
|
|
122
|
+
k: signData.k,
|
|
123
|
+
s: nonce,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error('Login request failed:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
128
|
+
return { ok: false, message: (error === null || error === void 0 ? void 0 : error.message) || 'Login failed.' };
|
|
129
|
+
}
|
|
130
|
+
if (loginResult && loginResult.code === 200 && loginResult.data) {
|
|
131
|
+
const encrypted = (0, crypto_2.encryptSession)(loginResult.data, this.getStoragePath());
|
|
132
|
+
return { ok: true, message: 'Login successful. Token saved.', encryptedToken: encrypted };
|
|
133
|
+
}
|
|
134
|
+
if (loginResult && loginResult.code === 2031) {
|
|
135
|
+
return { ok: false, twoFactorRequired: true, message: 'Two-factor authentication required.' };
|
|
136
|
+
}
|
|
137
|
+
console.error('Login failed:', loginResult);
|
|
138
|
+
return { ok: false, message: (loginResult === null || loginResult === void 0 ? void 0 : loginResult.msg) || 'Login failed. Check your credentials.' };
|
|
139
|
+
}
|
|
140
|
+
async logout() {
|
|
141
|
+
const storagePath = this.getStoragePath();
|
|
142
|
+
if (!storagePath) {
|
|
143
|
+
return { ok: true, message: 'Logged out. Token cleared.' };
|
|
144
|
+
}
|
|
145
|
+
const userDataPath = path_1.default.join(storagePath, 'roborock.UserData');
|
|
146
|
+
try {
|
|
147
|
+
if (fs_1.default.existsSync(userDataPath)) {
|
|
148
|
+
fs_1.default.unlinkSync(userDataPath);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
// Ignore file removal errors.
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, message: 'Logged out. Token cleared.' };
|
|
155
|
+
}
|
|
156
|
+
buildNonce() {
|
|
157
|
+
return crypto_1.default.randomBytes(12).toString('base64').substring(0, 16).replace(/\+/g, 'X').replace(/\//g, 'Y');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
(() => new RoborockUiServer())();
|
|
161
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<link rel="stylesheet" href="styles.css" />
|
|
2
|
+
|
|
3
|
+
<main class="container">
|
|
4
|
+
<header>
|
|
5
|
+
<h1>Roborock Vacuum</h1>
|
|
6
|
+
<p class="subtitle">Manage login and two-factor authentication for your Roborock account.</p>
|
|
7
|
+
</header>
|
|
8
|
+
|
|
9
|
+
<section class="card">
|
|
10
|
+
<h2>Account</h2>
|
|
11
|
+
<label>
|
|
12
|
+
Email
|
|
13
|
+
<input id="email" type="email" placeholder="name@example.com" />
|
|
14
|
+
</label>
|
|
15
|
+
<label id="password-row">
|
|
16
|
+
Password
|
|
17
|
+
<input id="password" type="password" placeholder="Password" />
|
|
18
|
+
</label>
|
|
19
|
+
<p class="help">Use password login to test credentials and save a token.</p>
|
|
20
|
+
<label>
|
|
21
|
+
Region
|
|
22
|
+
<select id="base-url">
|
|
23
|
+
<option value="https://usiot.roborock.com">US (usiot.roborock.com)</option>
|
|
24
|
+
<option value="https://euiot.roborock.com">EU (euiot.roborock.com)</option>
|
|
25
|
+
<option value="https://cniot.roborock.com">CN (cniot.roborock.com)</option>
|
|
26
|
+
<option value="https://api.roborock.com">Asia (api.roborock.com)</option>
|
|
27
|
+
</select>
|
|
28
|
+
</label>
|
|
29
|
+
<label>
|
|
30
|
+
Skip Devices
|
|
31
|
+
<input id="skip-devices" type="text" placeholder="Comma-separated device IDs" />
|
|
32
|
+
</label>
|
|
33
|
+
<label class="checkbox">
|
|
34
|
+
<input id="debug-mode" type="checkbox" />
|
|
35
|
+
Enable debug logging
|
|
36
|
+
</label>
|
|
37
|
+
<div class="actions">
|
|
38
|
+
<button id="login" class="secondary">Password Login</button>
|
|
39
|
+
<button id="logout" class="secondary hidden">Logout</button>
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
|
|
43
|
+
<section class="card" id="two-factor-section">
|
|
44
|
+
<h2>Two-Factor Authentication</h2>
|
|
45
|
+
<p class="help">If your account requires 2FA, send a verification email and enter the code below.</p>
|
|
46
|
+
<label id="code-row">
|
|
47
|
+
Verification Code
|
|
48
|
+
<input id="two-factor-code" type="text" placeholder="6-digit code" />
|
|
49
|
+
</label>
|
|
50
|
+
<div class="actions">
|
|
51
|
+
<button id="send-2fa" class="primary">Send 2FA Email</button>
|
|
52
|
+
<button id="verify-2fa" class="secondary">Verify Code</button>
|
|
53
|
+
</div>
|
|
54
|
+
</section>
|
|
55
|
+
</main>
|
|
56
|
+
|
|
57
|
+
<div id="toast-container" class="toast-container"></div>
|
|
58
|
+
|
|
59
|
+
<script src="index.js"></script>
|