mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-09-26 21:01:12 +08:00
Refactor Web UI
Hide errors immediately on submit Clear secure info immediately on submit Clear password immediately on successful auth
This commit is contained in:
13
lib/main.js
13
lib/main.js
@@ -1,9 +1,9 @@
|
||||
import './processhandlers.js'
|
||||
import './process-handlers.js'
|
||||
import './mqtt.js'
|
||||
import state from './state.js'
|
||||
import ring from './ring.js'
|
||||
import utils from './utils.js'
|
||||
import webui from './webui.js'
|
||||
import webservice from './web-service.js'
|
||||
import chalk from 'chalk'
|
||||
import isOnline from 'is-online'
|
||||
import debugModule from 'debug'
|
||||
@@ -36,6 +36,11 @@ export default new class Main {
|
||||
await state.init()
|
||||
}
|
||||
|
||||
// For the HA addon, Web UI is always started
|
||||
if (process.env.RUNMODE === 'addon') {
|
||||
webservice.start(state.data.systemId)
|
||||
}
|
||||
|
||||
const hasToken = state.data.ring_token || generatedToken
|
||||
if (!hasToken) {
|
||||
this.handleNoToken()
|
||||
@@ -57,7 +62,7 @@ export default new class Main {
|
||||
if (!await ring.init(state, generatedToken)) {
|
||||
debug(chalk.red('Failed to connect to Ring API using saved token, generate a new token using the Web UI'))
|
||||
debug(chalk.red('or wait 60 seconds to automatically retry authentication using the existing token'))
|
||||
webui.start(state.data.systemId)
|
||||
webservice.start(state.data.systemId)
|
||||
await utils.sleep(60)
|
||||
|
||||
if (!ring.client) {
|
||||
@@ -71,7 +76,7 @@ export default new class Main {
|
||||
if (process.env.RUNMODE === 'addon') {
|
||||
debug(chalk.red('No refresh token was found in state file, generate a token using the addon Web UI'))
|
||||
} else {
|
||||
webui.start(state.data.systemId)
|
||||
webservice.start(state.data.systemId)
|
||||
debug(chalk.red('No refresh token was found in the state file, use the Web UI at http://<host_ip_address>:55123/ to generate a token.'))
|
||||
}
|
||||
}
|
||||
|
@@ -1,27 +1,18 @@
|
||||
import { RingRestClient } from 'ring-client-api/rest-client'
|
||||
import utils from './utils.js'
|
||||
import { dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import express from 'express'
|
||||
import bodyParser from 'body-parser'
|
||||
import chalk from 'chalk'
|
||||
import debugModule from 'debug'
|
||||
import { webTemplate } from './web-template.js'
|
||||
|
||||
const PORT = 55123
|
||||
const COOKIE_MAX_AGE = 3600000
|
||||
const debug = debugModule('ring-mqtt')
|
||||
|
||||
class WebUI {
|
||||
class WebService {
|
||||
constructor() {
|
||||
this.app = express()
|
||||
this.listener = null
|
||||
this.ringConnected = false
|
||||
this.webdir = dirname(fileURLToPath(new URL('.', import.meta.url))) + '/web'
|
||||
|
||||
if (process.env.RUNMODE === 'addon') {
|
||||
this.start()
|
||||
}
|
||||
|
||||
this.initializeEventListeners()
|
||||
}
|
||||
|
||||
@@ -38,15 +29,16 @@ class WebUI {
|
||||
async handleAccountSubmission(req, res, restClient) {
|
||||
try {
|
||||
await restClient.getCurrentAuth()
|
||||
res.json({ success: true })
|
||||
} catch (error) {
|
||||
if (restClient.using2fa) {
|
||||
debug('Username/Password was accepted, waiting for 2FA code to be entered.')
|
||||
return res.sendFile('code.html', { root: this.webdir })
|
||||
res.json({ requires2fa: true })
|
||||
} else {
|
||||
const errorMessage = error.message || 'Null response, you may be temporarily throttled/blocked. Please shut down ring-mqtt and try again in a few hours.'
|
||||
debug(chalk.red(errorMessage))
|
||||
res.status(400).json({ error: errorMessage })
|
||||
}
|
||||
const errorMessage = error.message || 'Null response, you may be temporarily throttled/blocked. Please shut down ring-mqtt and try again in a few hours.'
|
||||
debug(chalk.red(errorMessage))
|
||||
res.cookie('error', errorMessage, { maxAge: 1000 })
|
||||
return res.sendFile('account.html', { root: this.webdir })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,28 +47,30 @@ class WebUI {
|
||||
const generatedToken = await restClient.getAuth(req.body.code)
|
||||
if (generatedToken) {
|
||||
utils.event.emit('generated_token', generatedToken.refresh_token)
|
||||
return res.sendFile('restart.html', { root: this.webdir })
|
||||
res.json({ success: true })
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || 'The 2FA code was not accepted, please verify the code and try again.'
|
||||
debug(chalk.red(errorMessage))
|
||||
res.cookie('error', errorMessage, { maxAge: 1000 })
|
||||
return res.sendFile('code.html', { root: this.webdir })
|
||||
res.status(400).json({ error: errorMessage })
|
||||
}
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
let restClient
|
||||
this.app.use(bodyParser.urlencoded({ extended: false }))
|
||||
this.app.use(bodyParser.json())
|
||||
|
||||
this.app.get(['/', /.*force-token-generation$/], (req, res) => {
|
||||
res.cookie('displayName', this.displayName, { maxAge: COOKIE_MAX_AGE })
|
||||
const template = this.ringConnected ? 'connected.html' : 'account.html'
|
||||
res.sendFile(template, { root: this.webdir })
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/get-state', (req, res) => {
|
||||
res.json({
|
||||
connected: this.ringConnected,
|
||||
displayName: this.displayName
|
||||
})
|
||||
})
|
||||
|
||||
let restClient
|
||||
this.app.post(/.*submit-account$/, async (req, res) => {
|
||||
res.cookie('displayName', this.displayName, { maxAge: COOKIE_MAX_AGE })
|
||||
router.post('/submit-account', async (req, res) => {
|
||||
restClient = new RingRestClient({
|
||||
email: req.body.email,
|
||||
password: req.body.password,
|
||||
@@ -86,10 +80,17 @@ class WebUI {
|
||||
await this.handleAccountSubmission(req, res, restClient)
|
||||
})
|
||||
|
||||
this.app.post(/.*submit-code$/, async (req, res) => {
|
||||
res.cookie('displayName', this.displayName, { maxAge: COOKIE_MAX_AGE })
|
||||
router.post('/submit-code', async (req, res) => {
|
||||
await this.handleCodeSubmission(req, res, restClient)
|
||||
})
|
||||
|
||||
// Mount router at base URL
|
||||
this.app.use('/', router)
|
||||
|
||||
// Serve the static HTML
|
||||
this.app.get('*', (req, res) => {
|
||||
res.send(webTemplate)
|
||||
})
|
||||
}
|
||||
|
||||
async start(systemId) {
|
||||
@@ -102,7 +103,7 @@ class WebUI {
|
||||
|
||||
this.setupRoutes()
|
||||
|
||||
this.listener = this.app.listen(PORT, () => {
|
||||
this.listener = this.app.listen(55123, () => {
|
||||
debug('Successfully started the ring-mqtt web UI')
|
||||
})
|
||||
}
|
||||
@@ -115,4 +116,4 @@ class WebUI {
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebUI()
|
||||
export default new WebService()
|
565
lib/web-template.js
Normal file
565
lib/web-template.js
Normal file
@@ -0,0 +1,565 @@
|
||||
export const webTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Ring-MQTT Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
/* Light theme variables */
|
||||
--bg-color: #f5f5f5;
|
||||
--container-bg: #fff;
|
||||
--text-color: #212529;
|
||||
--header-bg: #2f95c8;
|
||||
--header-color: #ffffff;
|
||||
--input-bg: #fff;
|
||||
--input-color: #495057;
|
||||
--input-border: #ced4da;
|
||||
--instruction-color: #6c757d;
|
||||
--error-bg: #f8d7da;
|
||||
--error-color: #721c24;
|
||||
--error-border: #f5c6cb;
|
||||
--success-bg: #e8f5ee;
|
||||
--success-color: #006400;
|
||||
--success-border: #c6e6d5;
|
||||
--button-primary-bg: #2f95c8;
|
||||
--button-primary-border: #2784b3;
|
||||
--button-secondary-bg: #6c757d;
|
||||
--button-secondary-border: #6c757d;
|
||||
--device-name-color: #00cc00;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: #1a1a1a;
|
||||
--container-bg: #2d2d2d;
|
||||
--text-color: #e0e0e0;
|
||||
--header-bg: #1e5c7c;
|
||||
--header-color: #ffffff;
|
||||
--input-bg: #3d3d3d;
|
||||
--input-color: #e0e0e0;
|
||||
--input-border: #4d4d4d;
|
||||
--instruction-color: #a0a0a0;
|
||||
--error-bg: #442326;
|
||||
--error-color: #ff9999;
|
||||
--error-border: #662629;
|
||||
--success-bg: #1e3323;
|
||||
--success-color: #90ee90;
|
||||
--success-border: #2d4d33;
|
||||
--button-primary-bg: #1e5c7c;
|
||||
--button-primary-border: #164459;
|
||||
--button-secondary-bg: #4d4d4d;
|
||||
--button-secondary-border: #404040;
|
||||
--device-name-color: #66ff66;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--header-color);
|
||||
background-color: var(--header-bg);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
background-color: var(--error-bg);
|
||||
border: 1px solid var(--error-border);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin-top: 1rem;
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: 1rem auto;
|
||||
padding: 1.5rem;
|
||||
background-color: var(--container-bg);
|
||||
border: 1px solid rgba(0,0,0,.125);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,.075);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.instruction {
|
||||
text-align: center;
|
||||
color: var(--instruction-color);
|
||||
margin-bottom: .75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
color: var(--input-color);
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
margin-top: 0.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
color: var(--input-color);
|
||||
background-color: var(--input-bg);
|
||||
border-color: #80bdff;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--header-color);
|
||||
background-color: var(--button-primary-bg);
|
||||
border: 1px solid var(--button-primary-border);
|
||||
cursor: pointer;
|
||||
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: var(--header-color);
|
||||
background-color: #2680b0;
|
||||
border-color: #206d94;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(47,149,200,.5);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--header-color);
|
||||
cursor: pointer;
|
||||
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
.button-group .back-button {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
background-color: var(--button-secondary-bg);
|
||||
border-color: var(--button-secondary-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
display: inline-block;
|
||||
width: 0.6em;
|
||||
height: 0.6em;
|
||||
border-left: 0.2em solid currentColor;
|
||||
border-bottom: 0.2em solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
margin-right: 0.2em;
|
||||
position: relative;
|
||||
top: -0.1em;
|
||||
}
|
||||
|
||||
.button-group .submit-button {
|
||||
flex: 1;
|
||||
background-color: var(--button-primary-bg);
|
||||
border-color: var(--button-primary-border);
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #5a6268;
|
||||
border-color: #545b62;
|
||||
}
|
||||
|
||||
.back-button:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(108,117,125,.5);
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #2680b0;
|
||||
border-color: #206d94;
|
||||
}
|
||||
|
||||
.submit-button:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(47,149,200,.5);
|
||||
}
|
||||
|
||||
#togglePassword {
|
||||
width: auto;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-44%);
|
||||
padding: 2px 6px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--header-color);
|
||||
background-color: var(--button-primary-bg);
|
||||
border: 1px solid var(--button-primary-border);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
#togglePassword:hover {
|
||||
background-color: #2680b0;
|
||||
border-color: #206d94;
|
||||
}
|
||||
|
||||
#togglePassword:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.5);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.password-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-container input {
|
||||
padding-right: 85px;
|
||||
}
|
||||
|
||||
#displayName {
|
||||
color: var(--instruction-color);
|
||||
text-align: center;
|
||||
margin-bottom: .75rem;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
color: var(--device-name-color);
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: var(--success-color);
|
||||
background-color: var(--success-bg);
|
||||
border: 1px solid var(--success-border);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin-top: 1rem;
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
#connectedMessage .message:last-child {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#reauth {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.container {
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 id="title">Ring-MQTT Authenticator</h1>
|
||||
<p class="instruction">Authenticate ring-mqtt to your Ring account</p>
|
||||
<div id="displayName"></div>
|
||||
|
||||
<div id="successMessage" class="hidden">
|
||||
<p class="message">Authentication with Ring was successful and <strong>ring-mqtt</strong> will now attempt to connect to Ring servers. No additional steps are required, please review the ring-mqtt logs to monitor progress.</p>
|
||||
</div>
|
||||
|
||||
<div id="connectedMessage" class="hidden">
|
||||
<p class="message">It appears that <strong>ring-mqtt</strong> is already connected to a Ring account.</p>
|
||||
</div>
|
||||
|
||||
<div id="reauthMessage" class="hidden">
|
||||
<p class="message">If you wish to force reauthentication, for example, to change the account used by this addon, click the button below to restart the authentication process.</p>
|
||||
<button id="reauth">Force Reauthentication</button>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="hidden">
|
||||
<div class="form-group">
|
||||
<label>Email Address</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<div class="password-container">
|
||||
<input type="password" id="password" name="password" required>
|
||||
<button type="button" id="togglePassword">Show</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<form id="twoFactorForm" class="hidden">
|
||||
<div class="form-group">
|
||||
<label>Enter 2FA Code</label>
|
||||
<input type="text" id="code" name="code" required>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button type="button" class="back-button" id="backToLogin">
|
||||
<span class="back-arrow"></span>
|
||||
Back
|
||||
</button>
|
||||
<button type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="error" class="error hidden"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
class UIState {
|
||||
static showElement(selector) {
|
||||
document.querySelector(selector).classList.remove('hidden');
|
||||
}
|
||||
|
||||
static hideElement(selector) {
|
||||
document.querySelector(selector).classList.add('hidden');
|
||||
}
|
||||
|
||||
static setDisplayName(name) {
|
||||
const element = document.querySelector('#displayName');
|
||||
element.textContent = 'Device Name: ';
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'device-name';
|
||||
nameSpan.textContent = name;
|
||||
element.appendChild(nameSpan);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorHandler {
|
||||
static #fadeTimeout;
|
||||
|
||||
static show(message) {
|
||||
const errorDiv = document.querySelector('#error');
|
||||
errorDiv.textContent = message;
|
||||
UIState.showElement('#error');
|
||||
|
||||
if (this.#fadeTimeout) {
|
||||
clearTimeout(this.#fadeTimeout);
|
||||
}
|
||||
|
||||
this.#fadeTimeout = setTimeout(() => {
|
||||
errorDiv.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
errorDiv.classList.add('hidden');
|
||||
errorDiv.classList.remove('fade-out');
|
||||
}, 500);
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
static hide() {
|
||||
const errorDiv = document.querySelector('#error');
|
||||
errorDiv.textContent = '';
|
||||
UIState.hideElement('#error');
|
||||
}
|
||||
}
|
||||
|
||||
class AuthService {
|
||||
static async getInitialState() {
|
||||
const response = await fetch('get-state');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
static async submitAccount(formData) {
|
||||
const response = await fetch('submit-account', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static async submitCode(formData) {
|
||||
const response = await fetch('submit-code', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class AuthForm {
|
||||
static async handleLoginSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const passwordInput = document.querySelector('#password');
|
||||
try {
|
||||
const data = await AuthService.submitAccount(new FormData(event.target));
|
||||
ErrorHandler.hide();
|
||||
|
||||
if (data.requires2fa) {
|
||||
UIState.hideElement('#loginForm');
|
||||
UIState.showElement('#twoFactorForm');
|
||||
} else if (data.success) {
|
||||
UIState.hideElement('#loginForm');
|
||||
UIState.showElement('#successMessage');
|
||||
}
|
||||
} catch (err) {
|
||||
ErrorHandler.show(err.message);
|
||||
passwordInput.focus();
|
||||
} finally {
|
||||
passwordInput.value = '';
|
||||
passwordInput.type = 'password';
|
||||
const toggleButton = document.querySelector('#togglePassword');
|
||||
toggleButton.textContent = 'Show';
|
||||
}
|
||||
}
|
||||
|
||||
static async handleTwoFactorSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const codeInput = document.querySelector('#code');
|
||||
try {
|
||||
const data = await AuthService.submitCode(new FormData(event.target));
|
||||
ErrorHandler.hide();
|
||||
|
||||
if (data.success) {
|
||||
UIState.hideElement('#twoFactorForm');
|
||||
UIState.hideElement('#connectedMessage');
|
||||
UIState.showElement('#successMessage');
|
||||
}
|
||||
} catch (err) {
|
||||
ErrorHandler.show(err.message);
|
||||
codeInput.focus();
|
||||
} finally {
|
||||
codeInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
static togglePasswordVisibility(event) {
|
||||
const passwordInput = document.querySelector('#password');
|
||||
const type = passwordInput.type === 'password' ? 'text' : 'password';
|
||||
passwordInput.type = type;
|
||||
event.target.textContent = type === 'password' ? 'Show' : 'Hide';
|
||||
}
|
||||
|
||||
static handleBackToLogin() {
|
||||
ErrorHandler.hide();
|
||||
UIState.hideElement('#twoFactorForm');
|
||||
UIState.showElement('#loginForm');
|
||||
const passwordInput = document.querySelector('#password');
|
||||
|
||||
setTimeout(() => {
|
||||
passwordInput.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
static handleReauth() {
|
||||
try {
|
||||
sessionStorage.setItem('forceReauth', 'true');
|
||||
UIState.hideElement('#connectedMessage');
|
||||
UIState.hideElement('#reauthMessage');
|
||||
UIState.showElement('#loginForm');
|
||||
} catch (err) {
|
||||
ErrorHandler.show('Failed to initiate reauthentication');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AuthApp {
|
||||
static async initialize() {
|
||||
try {
|
||||
const data = await AuthService.getInitialState();
|
||||
|
||||
if (data.displayName) {
|
||||
UIState.setDisplayName(data.displayName);
|
||||
}
|
||||
|
||||
if (data.connected) {
|
||||
UIState.showElement('#connectedMessage');
|
||||
if (sessionStorage.getItem('forceReauth')) {
|
||||
UIState.showElement('#loginForm');
|
||||
} else {
|
||||
UIState.showElement('#reauthMessage');
|
||||
}
|
||||
} else {
|
||||
UIState.showElement('#loginForm');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to get initial state:', err);
|
||||
UIState.showElement('#loginForm');
|
||||
}
|
||||
}
|
||||
|
||||
static setupEventListeners() {
|
||||
document.querySelector('#loginForm')
|
||||
.addEventListener('submit', AuthForm.handleLoginSubmit);
|
||||
|
||||
document.querySelector('#twoFactorForm')
|
||||
.addEventListener('submit', AuthForm.handleTwoFactorSubmit);
|
||||
|
||||
document.querySelector('#togglePassword')
|
||||
.addEventListener('click', AuthForm.togglePasswordVisibility);
|
||||
|
||||
document.querySelector('#backToLogin')
|
||||
.addEventListener('click', AuthForm.handleBackToLogin);
|
||||
|
||||
document.querySelector('#reauth')
|
||||
.addEventListener('click', AuthForm.handleReauth);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
sessionStorage.clear();
|
||||
AuthApp.initialize();
|
||||
AuthApp.setupEventListeners();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
200
web/account.html
200
web/account.html
@@ -1,200 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ring-MQTT Web Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #47a9e6;
|
||||
--primary-hover: #315b82;
|
||||
--bg-color: #f2f2f2;
|
||||
--error-color: #dc3545;
|
||||
--success-color: #39ff14;
|
||||
--text-color: #333;
|
||||
--spacing: 1rem;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
color: white;
|
||||
background-color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: var(--bg-color);
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: var(--text-color);
|
||||
margin-top: var(--spacing);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
.container h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-name span {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2);
|
||||
border-radius: 4px;
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: calc(var(--spacing) / 2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: calc(var(--spacing) / 2);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.2);
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(var(--spacing) / 2);
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2) var(--spacing);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: calc(var(--spacing) / 2);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--spacing);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Ring-MQTT Authentication</h2>
|
||||
<div id="displayName" class="display-name">Device Name: <span></span></div>
|
||||
<p id="errormsg" class="error-message" style="display: none;"></p>
|
||||
|
||||
<div class="container">
|
||||
<h3>Ring Account Info</h3>
|
||||
<form action="./submit-account" method="post">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="form-control"
|
||||
autocomplete="email"
|
||||
required
|
||||
aria-required="true">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="form-control"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
aria-required="true">
|
||||
<div class="password-toggle">
|
||||
<input type="checkbox" id="showPassword">
|
||||
<label for="showPassword">Show Password</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Password visibility toggle
|
||||
document.getElementById('showPassword').addEventListener('change', function() {
|
||||
const password = document.getElementById('password');
|
||||
password.type = this.checked ? 'text' : 'password';
|
||||
});
|
||||
|
||||
// Cookie handling
|
||||
function getCookie(key) {
|
||||
const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return value ? decodeURIComponent(value[2]) : null;
|
||||
}
|
||||
|
||||
// Error message handling
|
||||
const errorMessage = getCookie('error');
|
||||
const errorElement = document.getElementById('errormsg');
|
||||
if (errorMessage) {
|
||||
errorElement.style.display = 'block';
|
||||
errorElement.textContent = errorMessage;
|
||||
}
|
||||
|
||||
// System ID display
|
||||
const displayName = getCookie('displayName');
|
||||
if (displayName) {
|
||||
document.querySelector('#displayName span').textContent = displayName;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
168
web/code.html
168
web/code.html
@@ -1,168 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ring-MQTT Web Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #47a9e6;
|
||||
--primary-hover: #315b82;
|
||||
--bg-color: #f2f2f2;
|
||||
--error-color: #dc3545;
|
||||
--success-color: #39ff14;
|
||||
--text-color: #333;
|
||||
--spacing: 1rem;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
color: white;
|
||||
background-color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: var(--bg-color);
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: var(--text-color);
|
||||
margin-top: var(--spacing);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-name span {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2);
|
||||
border-radius: 4px;
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: calc(var(--spacing) / 2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: calc(var(--spacing) / 2);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2) var(--spacing);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: calc(var(--spacing) / 2);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--spacing);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Ring-MQTT Two-Factor Authentication</h2>
|
||||
<div id="displayName" class="display-name">Device Name: <span></span></div>
|
||||
<p id="errormsg" class="error-message" style="display: none;"></p>
|
||||
|
||||
<div class="container">
|
||||
<form action="./submit-code" method="post">
|
||||
<div class="form-group">
|
||||
<label for="code">Enter 2FA Code</label>
|
||||
<input type="text"
|
||||
id="code"
|
||||
name="code"
|
||||
class="form-control"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
required
|
||||
aria-required="true">
|
||||
</div>
|
||||
<button type="submit" class="btn">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cookie handling
|
||||
function getCookie(key) {
|
||||
const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return value ? decodeURIComponent(value[2]) : null;
|
||||
}
|
||||
|
||||
// Error message handling
|
||||
const errorMessage = getCookie('error');
|
||||
const errorElement = document.getElementById('errormsg');
|
||||
if (errorMessage) {
|
||||
errorElement.style.display = 'block';
|
||||
errorElement.textContent = errorMessage;
|
||||
}
|
||||
|
||||
// System ID display
|
||||
const displayName = getCookie('displayName');
|
||||
if (displayName) {
|
||||
document.querySelector('#displayName span').textContent = displayName;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,114 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ring-MQTT Web Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #47a9e6;
|
||||
--primary-hover: #315b82;
|
||||
--bg-color: #f2f2f2;
|
||||
--error-color: #dc3545;
|
||||
--success-color: #39ff14;
|
||||
--text-color: #333;
|
||||
--spacing: 1rem;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
color: white;
|
||||
background-color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-name span {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--spacing);
|
||||
padding: var(--spacing) 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2) var(--spacing);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: calc(var(--spacing) / 2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Ring-MQTT Addon Connected</h2>
|
||||
<div id="displayName" class="display-name">Device Name: <span></span></div>
|
||||
<div class="message">
|
||||
It appears that <strong>ring-mqtt</strong> is already connected to the Ring API, no additional action is required.
|
||||
</div>
|
||||
<div class="message">
|
||||
If you wish to force reauthentication, for example, to change the account used by this addon, clicking the button below will restart the authentication process.
|
||||
</div>
|
||||
<div class="container">
|
||||
<form action="./force-token-generation" method="get">
|
||||
<input type="submit" value="Force Reauthentication" class="btn">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getCookie(key) {
|
||||
const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return value ? decodeURIComponent(value[2]) : null;
|
||||
}
|
||||
|
||||
const displayName = getCookie('displayName');
|
||||
if (displayName) {
|
||||
document.querySelector('#displayName span').textContent = displayName;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,97 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ring-MQTT Web Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #47a9e6;
|
||||
--primary-hover: #315b82;
|
||||
--bg-color: #f2f2f2;
|
||||
--error-color: #dc3545;
|
||||
--success-color: #39ff14;
|
||||
--text-color: #333;
|
||||
--spacing: 1rem;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
color: white;
|
||||
background-color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: var(--bg-color);
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: var(--text-color);
|
||||
margin-top: var(--spacing);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-name span {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: var(--spacing);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: calc(var(--spacing) / 2);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--spacing);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Ring-MQTT Authentication Complete</h2>
|
||||
<div id="displayName" class="display-name">Device Name: <span></span></div>
|
||||
<div class="message">
|
||||
Authentication with Ring was successful and <strong>ring-mqtt</strong> will now attempt to connect to Ring servers. No additional steps are required, please review the ring-mqtt logs to monitor progress.
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cookie handling
|
||||
function getCookie(key) {
|
||||
const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return value ? decodeURIComponent(value[2]) : null;
|
||||
}
|
||||
|
||||
// System ID display
|
||||
const displayName = getCookie('displayName');
|
||||
if (displayName) {
|
||||
document.querySelector('#displayName span').textContent = displayName;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user