Code Refactor/Cleanup Web UI (#942)

* Refactor Web UI
This commit is contained in:
tsightler
2024-11-27 00:38:28 -05:00
committed by GitHub
parent c789296798
commit 4dc404edd5
10 changed files with 765 additions and 407 deletions

View File

@@ -1,28 +1,29 @@
export * from './exithandler.js'
export * from './mqtt.js'
import './processhandlers.js'
import './mqtt.js'
import state from './state.js'
import ring from './ring.js'
import utils from './utils.js'
import tokenApp from './tokenapp.js'
import webui from './webui.js'
import chalk from 'chalk'
import isOnline from 'is-online'
import debugModule from 'debug'
const debug = debugModule('ring-mqtt')
export default new class Main {
constructor() {
// Hack to suppress spurious message from push-receiver during startup
console.warn = (data) => {
if (data.includes('PHONE_REGISTRATION_ERROR') ||
data.startsWith('Retry...') ||
data.includes('Message dropped as it could not be decrypted:')
) {
return
console.warn = (message) => {
const suppressedMessages = [
/^Retry\.\.\./,
/PHONE_REGISTRATION_ERROR/,
/Message dropped as it could not be decrypted:/
]
if (!suppressedMessages.some(suppressedMessages => suppressedMessages.test(message))) {
console.error(message)
}
console.error(data)
}
// Start event listeners
utils.event.on('generated_token', (generatedToken) => {
this.init(generatedToken)
})
@@ -33,34 +34,45 @@ export default new class Main {
async init(generatedToken) {
if (!state.valid) {
await state.init()
tokenApp.setSystemId(state.data.systemId)
}
// Is there any usable token?
if (state.data.ring_token || generatedToken) {
// Wait for the network to be online and then attempt to connect to the Ring API using the token
while (!(await isOnline())) {
debug(chalk.yellow('Network is offline, waiting 10 seconds to check again...'))
await utils.sleep(10)
}
const hasToken = state.data.ring_token || generatedToken
if (!hasToken) {
this.handleNoToken()
return
}
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'))
tokenApp.start()
await utils.sleep(60)
if (!ring.client) {
debug(chalk.yellow('Retrying authentication with existing saved token...'))
this.init()
}
}
} else {
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 {
tokenApp.start()
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.'))
await this.waitForNetwork()
await this.attemptRingConnection(generatedToken)
}
async waitForNetwork() {
while (!(await isOnline())) {
debug(chalk.yellow('Network is offline, waiting 10 seconds to check again...'))
await utils.sleep(10)
}
}
async attemptRingConnection(generatedToken) {
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)
await utils.sleep(60)
if (!ring.client) {
debug(chalk.yellow('Retrying authentication with existing saved token...'))
this.init()
}
}
}
}
handleNoToken() {
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)
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

@@ -4,13 +4,12 @@ import ring from './ring.js'
import debugModule from 'debug'
const debug = debugModule('ring-mqtt')
export default new class ExitHandler {
export default new class ProcessHandlers {
constructor() {
this.init()
}
init() {
// Setup Exit Handlers
process.on('exit', this.processExit.bind(null, 0))
process.on('SIGINT', this.processExit.bind(null, 0))
process.on('SIGTERM', this.processExit.bind(null, 0))

View File

@@ -1,123 +0,0 @@
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'
const debug = debugModule('ring-mqtt')
export default new class TokenApp {
constructor() {
this.app = express()
this.listener = false
this.ringConnected = false
this.systemId = ''
if (process.env.RUNMODE === 'addon') {
this.start()
}
utils.event.on('ring_api_state', async (state) => {
if (state === 'connected') {
this.ringConnected = true
// Only the addon leaves the web UI running all the time
if (process.env.RUNMODE !== 'addon') {
this.stop()
}
} else {
this.ringConnected = false
}
})
}
setSystemId(systemId) {
if (systemId) {
this.systemId = systemId
}
}
// Super simple web service to acquire authentication tokens from Ring
async start() {
if (this.listener) {
return
}
const webdir = dirname(fileURLToPath(new URL('.', import.meta.url)))+'/web'
let restClient
this.listener = this.app.listen(55123, () => {
debug('Succesfully started the token generator web UI')
})
this.app.use(bodyParser.urlencoded({ extended: false }))
this.app.get('/', (req, res) => {
res.cookie('systemId', `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${this.systemId.slice(-5)}`, { maxAge: 3600000, encode: String })
if (this.ringConnected) {
res.sendFile('connected.html', {root: webdir})
} else {
res.sendFile('account.html', {root: webdir})
}
})
this.app.get(/.*force-token-generation$/, (req, res) => {
res.cookie('systemId', `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${this.systemId.slice(-5)}`, { maxAge: 3600000, encode: String })
res.sendFile('account.html', {root: webdir})
})
this.app.post(/.*submit-account$/, async (req, res) => {
res.cookie('systemId', `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${this.systemId.slice(-5)}`, { maxAge: 3600000, encode: String })
const email = req.body.email
const password = req.body.password
restClient = await new RingRestClient({
email,
password,
controlCenterDisplayName: `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${this.systemId.slice(-5)}`,
systemId: this.systemId
})
// Check if the user/password was accepted
try {
await restClient.getCurrentAuth()
} catch(error) {
if (restClient.using2fa) {
debug('Username/Password was accepted, waiting for 2FA code to be entered.')
res.sendFile('code.html', {root: webdir})
} else {
const errmsg = error.message ? 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(errmsg))
res.cookie('error', errmsg, { maxAge: 1000, encode: String })
res.sendFile('account.html', {root: webdir})
}
}
})
this.app.post(/.*submit-code$/, async (req, res) => {
res.cookie('systemId', `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${this.systemId.slice(-5)}`, { maxAge: 3600000, encode: String })
let generatedToken
const code = req.body.code
try {
generatedToken = await restClient.getAuth(code)
} catch {
generatedToken = false
const errormsg = 'The 2FA code was not accepted, please verify the code and try again.'
debug(errormsg)
res.cookie('error', errormsg, { maxAge: 1000, encode: String })
res.sendFile('code.html', {root: webdir})
}
if (generatedToken) {
res.sendFile('restart.html', {root: webdir})
utils.event.emit('generated_token', generatedToken.refresh_token)
}
})
}
async stop() {
if (this.listener) {
await this.listener.close()
this.listener = false
}
}
}

View File

@@ -3,66 +3,67 @@ import dns from 'dns'
import os from 'os'
import { promisify } from 'util'
import { EventEmitter } from 'events'
import debugModule from 'debug'
const debug = {
mqtt: debugModule('ring-mqtt'),
attr: debugModule('ring-attr'),
disc: debugModule('ring-disc'),
rtsp: debugModule('ring-rtsp'),
wrtc: debugModule('ring-wrtc')
import debug from 'debug'
const debuggers = {
mqtt: debug('ring-mqtt'),
attr: debug('ring-attr'),
disc: debug('ring-disc'),
rtsp: debug('ring-rtsp'),
wrtc: debug('ring-wrtc')
}
export default new class Utils {
class Utils {
constructor() {
this.event = new EventEmitter()
this.dnsLookup = promisify(dns.lookup)
this.dnsLookupService = promisify(dns.lookupService)
}
constructor() {
this.event = new EventEmitter()
}
config() {
return config.data
}
config() {
return config.data
}
sleep(sec) {
return this.msleep(sec * 1000)
}
// Sleep function (seconds)
sleep(sec) {
return this.msleep(sec*1000)
}
msleep(msec) {
return new Promise(res => setTimeout(res, msec))
}
// Sleep function (milliseconds)
msleep(msec) {
return new Promise(res => setTimeout(res, msec))
}
getISOTime(epoch) {
return new Date(epoch).toISOString().slice(0, -5) + 'Z'
}
// Return ISO time from epoch without milliseconds
getISOTime(epoch) {
return new Date(epoch).toISOString().slice(0,-5)+"Z"
async getHostFqdn() {
try {
const ip = await this.getHostIp()
const { hostname } = await this.dnsLookupService(ip, 0)
return hostname
} catch (error) {
console.warn('Failed to resolve FQDN, using os.hostname() instead:', error.message)
return os.hostname()
}
}
async getHostFqdn() {
const pLookupService = promisify(dns.lookupService)
try {
return (await pLookupService(await this.getHostIp(), 0)).hostname
} catch {
console.log('Failed to resolve FQDN, using os.hostname() instead')
return os.hostname()
}
async getHostIp() {
try {
const { address } = await this.dnsLookup(os.hostname())
return address
} catch (error) {
console.warn('Failed to resolve hostname IP address, returning localhost instead:', error.message)
return 'localhost'
}
}
async getHostIp() {
try {
const pLookup = promisify(dns.lookup)
return (await pLookup(os.hostname())).address
} catch {
console.log('Failed to resolve hostname IP address, returning localhost instead')
return 'localhost'
}
}
isNumeric(num) {
return !isNaN(parseFloat(num)) && isFinite(num)
}
isNumeric(num) {
return !isNaN(parseFloat(num)) && isFinite(num);
}
debug(message, debugType) {
debugType = debugType ? debugType : 'mqtt'
debug[debugType](message)
}
debug(message, debugType = 'mqtt') {
debuggers[debugType]?.(message)
}
}
export default new Utils()

118
lib/webui.js Normal file
View File

@@ -0,0 +1,118 @@
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'
const PORT = 55123
const COOKIE_MAX_AGE = 3600000
const debug = debugModule('ring-mqtt')
class WebUI {
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()
}
initializeEventListeners() {
utils.event.on('ring_api_state', async (state) => {
this.ringConnected = state === 'connected'
if (this.ringConnected && process.env.RUNMODE !== 'addon') {
await this.stop()
}
})
}
async handleAccountSubmission(req, res, restClient) {
try {
await restClient.getCurrentAuth()
} 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 })
}
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 })
}
}
async handleCodeSubmission(req, res, restClient) {
try {
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 })
}
} 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 })
}
}
setupRoutes() {
this.app.use(bodyParser.urlencoded({ extended: false }))
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 })
})
let restClient
this.app.post(/.*submit-account$/, async (req, res) => {
res.cookie('displayName', this.displayName, { maxAge: COOKIE_MAX_AGE })
restClient = new RingRestClient({
email: req.body.email,
password: req.body.password,
controlCenterDisplayName: this.displayName,
systemId: this.systemId
})
await this.handleAccountSubmission(req, res, restClient)
})
this.app.post(/.*submit-code$/, async (req, res) => {
res.cookie('displayName', this.displayName, { maxAge: COOKIE_MAX_AGE })
await this.handleCodeSubmission(req, res, restClient)
})
}
async start(systemId) {
if (this.listener) {
return
}
this.systemId = systemId
this.displayName = `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${systemId.slice(-5)}`
this.setupRoutes()
this.listener = this.app.listen(PORT, () => {
debug('Successfully started the ring-mqtt web UI')
})
}
async stop() {
if (this.listener) {
await this.listener.close()
this.listener = null
}
}
}
export default new WebUI()

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env node
export * from './lib/main.js'
import './lib/main.js'

View File

@@ -1,92 +1,200 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;}
* {box-sizing: border-box;}
<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;
}
input[type=text], select, textarea {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-top: 6px;
margin-bottom: 16px;
resize: vertical;
}
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;
}
input[type=password], select, textarea {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-top: 6px;
margin-bottom: 16px;
resize: vertical;
}
.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);
}
input[type=submit] {
background-color: #47a9e6;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
h2 {
margin: 0 0 var(--spacing) 0;
}
input[type=submit]:hover {
background-color: #315b82;
}
h3 {
margin: 0 0 var(--spacing) 0;
}
.container {
max-width: 500px;
border-radius: 5px;
background-color: #f2f2f2;
padding: 20px;
color: black;
}
</style>
.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>
<h2>Acquire Refresh Token</h2>
<h3 id="systemId"></h3>
Please enter your Ring account information below and the 2FA code on the following page to generate the refresh token required for this application to access the Ring API.<br>
<p style="background-color:red" id="errormsg"></p>
<h3>Login</h3>
<div class="container">
<form action="./submit-account" method="post">
<label for="email">Email</label>
<input type="text" id="email" name="email">
<label for="password">Password</label>
<input type="password" id="password" name="password">
<input type="checkbox" onclick="showPassword()">Show Password
<br><br>
<input type="submit" value="Submit">
</form>
</div>
<script>
function showPassword() {
var x = document.getElementById("password");
if (x.type === "password") {
x.type = "text";
} else {
x.type = "password";
}
}
function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
if (getCookie('error')) {
document.getElementById("errormsg").innerHTML = getCookie('error')
}
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
}
</script>
<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>
</html>

View File

@@ -1,67 +1,168 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;}
* {box-sizing: border-box;}
<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;
}
input[type=text], select, textarea {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-top: 6px;
margin-bottom: 16px;
resize: vertical;
}
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;
}
input[type=submit] {
background-color: #47a9e6;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.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);
}
input[type=submit]:hover {
background-color: #315b82;
}
h2 {
margin: 0 0 var(--spacing) 0;
}
.container {
max-width: 500px;
border-radius: 5px;
background-color: #f2f2f2;
padding: 20px;
color: black;
}
</style>
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>
<h2>Enter 2FA Code</h2>
<h3 id="systemId"></h3>
<p style="background-color:Red;" id="errormsg"></p></br>
<div class="container">
<form action="./submit-code" method="post">
<label for="2facode">Code</label>
<input type="text" id="code" name="code">
<input type="submit" value="Submit">
</form>
</div>
<script>
function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
if (getCookie('error')) {
document.getElementById("errormsg").innerHTML = getCookie('error')
}
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
}
</script>
<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>
</html>

View File

@@ -1,46 +1,114 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;}
<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;
}
input[type=submit] {
background-color: #47a9e6;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
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;
}
input[type=submit]:hover {
background-color: #315b82;
}
h2 {
margin: 0 0 var(--spacing) 0;
}
.container {
padding: 20px 5px;
}
</style>
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 Device Addon Connected</h2>
<h3 id="systemId"></h3>
It appears that this addon is already authenticated and connected to the Ring API, no additional action is required.
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 class="container">
<form action="./force-token-generation" method="get">
<input type="submit" value="Force Reauthentication">
</form>
</div>
<script>
function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
}
</script>
<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>
</html>

View File

@@ -1,23 +1,97 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;}
</style>
<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>Refresh Token Generation Complete</h2>
<h3 id="systemId"></h3>
The <b>Home Assistant ring-mqtt Add-on</b> will now automatically connect using the generated token. No additional configuration steps are required, please check the addon logs to monitor progress.
<script>
function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
}
</script>
<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>
</html>