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' import './processhandlers.js'
export * from './mqtt.js' import './mqtt.js'
import state from './state.js' import state from './state.js'
import ring from './ring.js' import ring from './ring.js'
import utils from './utils.js' import utils from './utils.js'
import tokenApp from './tokenapp.js' import webui from './webui.js'
import chalk from 'chalk' import chalk from 'chalk'
import isOnline from 'is-online' import isOnline from 'is-online'
import debugModule from 'debug' import debugModule from 'debug'
const debug = debugModule('ring-mqtt') const debug = debugModule('ring-mqtt')
export default new class Main { export default new class Main {
constructor() { constructor() {
// Hack to suppress spurious message from push-receiver during startup console.warn = (message) => {
console.warn = (data) => { const suppressedMessages = [
if (data.includes('PHONE_REGISTRATION_ERROR') || /^Retry\.\.\./,
data.startsWith('Retry...') || /PHONE_REGISTRATION_ERROR/,
data.includes('Message dropped as it could not be decrypted:') /Message dropped as it could not be decrypted:/
) { ]
return
if (!suppressedMessages.some(suppressedMessages => suppressedMessages.test(message))) {
console.error(message)
} }
console.error(data)
} }
// Start event listeners
utils.event.on('generated_token', (generatedToken) => { utils.event.on('generated_token', (generatedToken) => {
this.init(generatedToken) this.init(generatedToken)
}) })
@@ -33,34 +34,45 @@ export default new class Main {
async init(generatedToken) { async init(generatedToken) {
if (!state.valid) { if (!state.valid) {
await state.init() await state.init()
tokenApp.setSystemId(state.data.systemId)
} }
// Is there any usable token? const hasToken = state.data.ring_token || generatedToken
if (state.data.ring_token || generatedToken) { if (!hasToken) {
// Wait for the network to be online and then attempt to connect to the Ring API using the token this.handleNoToken()
while (!(await isOnline())) { return
debug(chalk.yellow('Network is offline, waiting 10 seconds to check again...')) }
await utils.sleep(10)
}
if (!await ring.init(state, generatedToken)) { await this.waitForNetwork()
debug(chalk.red('Failed to connect to Ring API using saved token, generate a new token using the Web UI')) await this.attemptRingConnection(generatedToken)
debug(chalk.red('or wait 60 seconds to automatically retry authentication using the existing token')) }
tokenApp.start()
await utils.sleep(60) async waitForNetwork() {
if (!ring.client) { while (!(await isOnline())) {
debug(chalk.yellow('Retrying authentication with existing saved token...')) debug(chalk.yellow('Network is offline, waiting 10 seconds to check again...'))
this.init() await utils.sleep(10)
} }
} }
} else {
if (process.env.RUNMODE === 'addon') { async attemptRingConnection(generatedToken) {
debug(chalk.red('No refresh token was found in state file, generate a token using the addon Web UI')) if (!await ring.init(state, generatedToken)) {
} else { debug(chalk.red('Failed to connect to Ring API using saved token, generate a new token using the Web UI'))
tokenApp.start() debug(chalk.red('or wait 60 seconds to automatically retry authentication using the existing token'))
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.')) 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' import debugModule from 'debug'
const debug = debugModule('ring-mqtt') const debug = debugModule('ring-mqtt')
export default new class ExitHandler { export default new class ProcessHandlers {
constructor() { constructor() {
this.init() this.init()
} }
init() { init() {
// Setup Exit Handlers
process.on('exit', this.processExit.bind(null, 0)) process.on('exit', this.processExit.bind(null, 0))
process.on('SIGINT', this.processExit.bind(null, 0)) process.on('SIGINT', this.processExit.bind(null, 0))
process.on('SIGTERM', 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 os from 'os'
import { promisify } from 'util' import { promisify } from 'util'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import debugModule from 'debug' import debug from 'debug'
const debug = {
mqtt: debugModule('ring-mqtt'), const debuggers = {
attr: debugModule('ring-attr'), mqtt: debug('ring-mqtt'),
disc: debugModule('ring-disc'), attr: debug('ring-attr'),
rtsp: debugModule('ring-rtsp'), disc: debug('ring-disc'),
wrtc: debugModule('ring-wrtc') 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() { config() {
this.event = new EventEmitter() return config.data
} }
config() { sleep(sec) {
return config.data return this.msleep(sec * 1000)
} }
// Sleep function (seconds) msleep(msec) {
sleep(sec) { return new Promise(res => setTimeout(res, msec))
return this.msleep(sec*1000) }
}
// Sleep function (milliseconds) getISOTime(epoch) {
msleep(msec) { return new Date(epoch).toISOString().slice(0, -5) + 'Z'
return new Promise(res => setTimeout(res, msec)) }
}
// Return ISO time from epoch without milliseconds async getHostFqdn() {
getISOTime(epoch) { try {
return new Date(epoch).toISOString().slice(0,-5)+"Z" 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() { async getHostIp() {
const pLookupService = promisify(dns.lookupService) try {
try { const { address } = await this.dnsLookup(os.hostname())
return (await pLookupService(await this.getHostIp(), 0)).hostname return address
} catch { } catch (error) {
console.log('Failed to resolve FQDN, using os.hostname() instead') console.warn('Failed to resolve hostname IP address, returning localhost instead:', error.message)
return os.hostname() return 'localhost'
}
} }
}
async getHostIp() { isNumeric(num) {
try { return !isNaN(parseFloat(num)) && isFinite(num)
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) { debug(message, debugType = 'mqtt') {
return !isNaN(parseFloat(num)) && isFinite(num); debuggers[debugType]?.(message)
} }
debug(message, debugType) {
debugType = debugType ? debugType : 'mqtt'
debug[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 #!/usr/bin/env node
export * from './lib/main.js' import './lib/main.js'

View File

@@ -1,92 +1,200 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta charset="UTF-8">
<style> <meta name="viewport" content="width=device-width, initial-scale=1.0">
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;} <title>Ring-MQTT Web Authenticator</title>
* {box-sizing: border-box;} <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 { body {
width: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
padding: 12px; max-width: 500px;
border: 1px solid #ccc; margin: 0 auto;
border-radius: 4px; padding: var(--spacing);
box-sizing: border-box; color: white;
margin-top: 6px; background-color: #666;
margin-bottom: 16px; line-height: 1.6;
resize: vertical; }
}
input[type=password], select, textarea { .container {
width: 100%; background-color: var(--bg-color);
padding: 12px; padding: calc(var(--spacing) * 1.5);
border: 1px solid #ccc; border-radius: 8px;
border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-sizing: border-box; color: var(--text-color);
margin-top: 6px; margin-top: var(--spacing);
margin-bottom: 16px; }
resize: vertical;
}
input[type=submit] { h2 {
background-color: #47a9e6; margin: 0 0 var(--spacing) 0;
color: white; }
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
input[type=submit]:hover { h3 {
background-color: #315b82; margin: 0 0 var(--spacing) 0;
} }
.container { .container h3 {
max-width: 500px; margin-top: 0;
border-radius: 5px; }
background-color: #f2f2f2;
padding: 20px; .display-name {
color: black; margin: calc(var(--spacing) / 2) 0;
} padding: 0;
</style> 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> </head>
<body> <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> <div class="container">
<h3 id="systemId"></h3> <h3>Ring Account Info</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> <form action="./submit-account" method="post">
<p style="background-color:red" id="errormsg"></p> <div class="form-group">
<h3>Login</h3> <label for="email">Email Address</label>
<div class="container"> <input type="email"
<form action="./submit-account" method="post"> id="email"
<label for="email">Email</label> name="email"
<input type="text" id="email" name="email"> class="form-control"
<label for="password">Password</label> autocomplete="email"
<input type="password" id="password" name="password"> required
<input type="checkbox" onclick="showPassword()">Show Password aria-required="true">
<br><br> </div>
<input type="submit" value="Submit"> <div class="form-group">
</form> <label for="password">Password</label>
</div> <input type="password"
<script> id="password"
function showPassword() { name="password"
var x = document.getElementById("password"); class="form-control"
if (x.type === "password") { autocomplete="current-password"
x.type = "text"; required
} else { aria-required="true">
x.type = "password"; <div class="password-toggle">
} <input type="checkbox" id="showPassword">
} <label for="showPassword">Show Password</label>
function getCookie(key) { </div>
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)'); </div>
return keyValue ? keyValue[2] : null; <button type="submit" class="btn">Submit</button>
} </form>
if (getCookie('error')) { </div>
document.getElementById("errormsg").innerHTML = getCookie('error')
} <script>
if (getCookie('systemId')) { // Password visibility toggle
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>` document.getElementById('showPassword').addEventListener('change', function() {
} const password = document.getElementById('password');
</script> 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> </body>
</html> </html>

View File

@@ -1,67 +1,168 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta charset="UTF-8">
<style> <meta name="viewport" content="width=device-width, initial-scale=1.0">
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;} <title>Ring-MQTT Web Authenticator</title>
* {box-sizing: border-box;} <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 { body {
width: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
padding: 12px; max-width: 500px;
border: 1px solid #ccc; margin: 0 auto;
border-radius: 4px; padding: var(--spacing);
box-sizing: border-box; color: white;
margin-top: 6px; background-color: #666;
margin-bottom: 16px; line-height: 1.6;
resize: vertical; }
}
input[type=submit] { .container {
background-color: #47a9e6; background-color: var(--bg-color);
color: white; padding: calc(var(--spacing) * 1.5);
padding: 12px 20px; border-radius: 8px;
border: none; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 4px; color: var(--text-color);
cursor: pointer; margin-top: var(--spacing);
} }
input[type=submit]:hover { h2 {
background-color: #315b82; margin: 0 0 var(--spacing) 0;
} }
.container { h3 {
max-width: 500px; margin: 0 0 var(--spacing) 0;
border-radius: 5px; }
background-color: #f2f2f2;
padding: 20px; .display-name {
color: black; margin: calc(var(--spacing) / 2) 0;
} padding: 0;
</style> 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> </head>
<body> <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> <div class="container">
<h3 id="systemId"></h3> <form action="./submit-code" method="post">
<p style="background-color:Red;" id="errormsg"></p></br> <div class="form-group">
<div class="container"> <label for="code">Enter 2FA Code</label>
<form action="./submit-code" method="post"> <input type="text"
<label for="2facode">Code</label> id="code"
<input type="text" id="code" name="code"> name="code"
<input type="submit" value="Submit"> class="form-control"
</form> autocomplete="one-time-code"
</div> inputmode="numeric"
<script> required
function getCookie(key) { aria-required="true">
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)'); </div>
return keyValue ? keyValue[2] : null; <button type="submit" class="btn">Submit</button>
} </form>
if (getCookie('error')) { </div>
document.getElementById("errormsg").innerHTML = getCookie('error')
} <script>
if (getCookie('systemId')) { // Cookie handling
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>` function getCookie(key) {
} const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
</script> 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> </body>
</html> </html>

View File

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

View File

@@ -1,23 +1,97 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta charset="UTF-8">
<style> <meta name="viewport" content="width=device-width, initial-scale=1.0">
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;} <title>Ring-MQTT Web Authenticator</title>
</style> <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> </head>
<body> <body>
<h2>Refresh Token Generation Complete</h2> <h2>Ring-MQTT Authentication Complete</h2>
<h3 id="systemId"></h3> <div id="displayName" class="display-name">Device Name: <span></span></div>
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. <div class="message">
<script> 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.
function getCookie(key) { </div>
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null; <script>
} // Cookie handling
if (getCookie('systemId')) { function getCookie(key) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>` const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
} return value ? decodeURIComponent(value[2]) : null;
</script> }
// System ID display
const displayName = getCookie('displayName');
if (displayName) {
document.querySelector('#displayName span').textContent = displayName;
}
</script>
</body> </body>
</html> </html>