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:
Tom Sightler
2024-11-27 00:56:34 -05:00
parent ada687e5fc
commit 9eb2727eac
8 changed files with 605 additions and 613 deletions

View File

@@ -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.'))
}
}

View File

@@ -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
View 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>`

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>