This commit is contained in:
Tom Sightler
2024-11-27 00:45:40 -05:00
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()
return
}
await this.waitForNetwork()
await this.attemptRingConnection(generatedToken)
}
async waitForNetwork() {
while (!(await isOnline())) { while (!(await isOnline())) {
debug(chalk.yellow('Network is offline, waiting 10 seconds to check again...')) debug(chalk.yellow('Network is offline, waiting 10 seconds to check again...'))
await utils.sleep(10) await utils.sleep(10)
} }
}
async attemptRingConnection(generatedToken) {
if (!await ring.init(state, 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('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')) debug(chalk.red('or wait 60 seconds to automatically retry authentication using the existing token'))
tokenApp.start() webui.start(state.data.systemId)
await utils.sleep(60) await utils.sleep(60)
if (!ring.client) { if (!ring.client) {
debug(chalk.yellow('Retrying authentication with existing saved token...')) debug(chalk.yellow('Retrying authentication with existing saved token...'))
this.init() this.init()
} }
} }
} else { }
handleNoToken() {
if (process.env.RUNMODE === 'addon') { if (process.env.RUNMODE === 'addon') {
debug(chalk.red('No refresh token was found in state file, generate a token using the addon Web UI')) debug(chalk.red('No refresh token was found in state file, generate a token using the addon Web UI'))
} else { } else {
tokenApp.start() 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.')) 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() { constructor() {
this.event = new EventEmitter() this.event = new EventEmitter()
this.dnsLookup = promisify(dns.lookup)
this.dnsLookupService = promisify(dns.lookupService)
} }
config() { config() {
return config.data return config.data
} }
// Sleep function (seconds)
sleep(sec) { sleep(sec) {
return this.msleep(sec*1000) return this.msleep(sec * 1000)
} }
// Sleep function (milliseconds)
msleep(msec) { msleep(msec) {
return new Promise(res => setTimeout(res, msec)) return new Promise(res => setTimeout(res, msec))
} }
// Return ISO time from epoch without milliseconds
getISOTime(epoch) { getISOTime(epoch) {
return new Date(epoch).toISOString().slice(0,-5)+"Z" return new Date(epoch).toISOString().slice(0, -5) + 'Z'
} }
async getHostFqdn() { async getHostFqdn() {
const pLookupService = promisify(dns.lookupService)
try { try {
return (await pLookupService(await this.getHostIp(), 0)).hostname const ip = await this.getHostIp()
} catch { const { hostname } = await this.dnsLookupService(ip, 0)
console.log('Failed to resolve FQDN, using os.hostname() instead') return hostname
} catch (error) {
console.warn('Failed to resolve FQDN, using os.hostname() instead:', error.message)
return os.hostname() return os.hostname()
} }
} }
async getHostIp() { async getHostIp() {
try { try {
const pLookup = promisify(dns.lookup) const { address } = await this.dnsLookup(os.hostname())
return (await pLookup(os.hostname())).address return address
} catch { } catch (error) {
console.log('Failed to resolve hostname IP address, returning localhost instead') console.warn('Failed to resolve hostname IP address, returning localhost instead:', error.message)
return 'localhost' return 'localhost'
} }
} }
isNumeric(num) { isNumeric(num) {
return !isNaN(parseFloat(num)) && isFinite(num); return !isNaN(parseFloat(num)) && isFinite(num)
} }
debug(message, debugType) { debug(message, debugType = 'mqtt') {
debugType = debugType ? debugType : 'mqtt' debuggers[debugType]?.(message)
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;
margin-top: 6px;
margin-bottom: 16px;
resize: vertical;
}
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;
}
input[type=submit] {
background-color: #47a9e6;
color: white; color: white;
padding: 12px 20px; 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: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} font-size: 1rem;
transition: background-color 0.2s;
width: 100%;
}
input[type=submit]:hover { .btn:hover {
background-color: #315b82; background-color: var(--primary-hover);
} }
.container { .btn:focus {
max-width: 500px; outline: none;
border-radius: 5px; box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.4);
background-color: #f2f2f2; }
padding: 20px;
color: black; @media (max-width: 600px) {
} body {
</style> 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>
<p style="background-color:red" id="errormsg"></p>
<h3>Login</h3>
<div class="container">
<form action="./submit-account" method="post"> <form action="./submit-account" method="post">
<label for="email">Email</label> <div class="form-group">
<input type="text" id="email" name="email"> <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> <label for="password">Password</label>
<input type="password" id="password" name="password"> <input type="password"
<input type="checkbox" onclick="showPassword()">Show Password id="password"
<br><br> name="password"
<input type="submit" value="Submit"> 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> </form>
</div> </div>
<script>
function showPassword() { <script>
var x = document.getElementById("password"); // Password visibility toggle
if (x.type === "password") { document.getElementById('showPassword').addEventListener('change', function() {
x.type = "text"; const password = document.getElementById('password');
} else { password.type = this.checked ? 'text' : 'password';
x.type = "password"; });
}
} // Cookie handling
function getCookie(key) { function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)'); const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null; return value ? decodeURIComponent(value[2]) : null;
} }
if (getCookie('error')) {
document.getElementById("errormsg").innerHTML = getCookie('error') // Error message handling
const errorMessage = getCookie('error');
const errorElement = document.getElementById('errormsg');
if (errorMessage) {
errorElement.style.display = 'block';
errorElement.textContent = errorMessage;
} }
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>` // System ID display
const displayName = getCookie('displayName');
if (displayName) {
document.querySelector('#displayName span').textContent = displayName;
} }
</script> </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;
margin-top: 6px;
margin-bottom: 16px;
resize: vertical;
}
input[type=submit] {
background-color: #47a9e6;
color: white; color: white;
padding: 12px 20px; 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: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} font-size: 1rem;
transition: background-color 0.2s;
width: 100%;
}
input[type=submit]:hover { .btn:hover {
background-color: #315b82; background-color: var(--primary-hover);
} }
.container { .btn:focus {
max-width: 500px; outline: none;
border-radius: 5px; box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.4);
background-color: #f2f2f2; }
padding: 20px;
color: black; @media (max-width: 600px) {
} body {
</style> 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>
<p style="background-color:Red;" id="errormsg"></p></br>
<div class="container">
<form action="./submit-code" method="post"> <form action="./submit-code" method="post">
<label for="2facode">Code</label> <div class="form-group">
<input type="text" id="code" name="code"> <label for="code">Enter 2FA Code</label>
<input type="submit" value="Submit"> <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> </form>
</div> </div>
<script>
<script>
// Cookie handling
function getCookie(key) { function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)'); const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null; return value ? decodeURIComponent(value[2]) : null;
} }
if (getCookie('error')) {
document.getElementById("errormsg").innerHTML = getCookie('error') // Error message handling
const errorMessage = getCookie('error');
const errorElement = document.getElementById('errormsg');
if (errorMessage) {
errorElement.style.display = 'block';
errorElement.textContent = errorMessage;
} }
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>` // System ID display
const displayName = getCookie('displayName');
if (displayName) {
document.querySelector('#displayName span').textContent = displayName;
} }
</script> </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;
max-width: 500px;
margin: 0 auto;
padding: var(--spacing);
color: white; color: white;
padding: 12px 20px; 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: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} font-size: 1rem;
transition: background-color 0.2s;
min-width: 200px;
}
input[type=submit]:hover { .btn:hover {
background-color: #315b82; background-color: var(--primary-hover);
} }
.container { .btn:focus {
padding: 20px 5px; outline: none;
} box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.4);
</style> }
@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>
<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"> <form action="./force-token-generation" method="get">
<input type="submit" value="Force Reauthentication"> <input type="submit" value="Force Reauthentication" class="btn">
</form> </form>
</div> </div>
<script>
<script>
function getCookie(key) { function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)'); const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null; return value ? decodeURIComponent(value[2]) : null;
} }
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>` const displayName = getCookie('displayName');
if (displayName) {
document.querySelector('#displayName span').textContent = displayName;
} }
</script> </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.
</div>
<script>
// Cookie handling
function getCookie(key) { function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)'); const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null; return value ? decodeURIComponent(value[2]) : null;
} }
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>` // System ID display
const displayName = getCookie('displayName');
if (displayName) {
document.querySelector('#displayName span').textContent = displayName;
} }
</script> </script>
</body> </body>
</html> </html>