mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-09-26 21:01:12 +08:00
86
lib/main.js
86
lib/main.js
@@ -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.'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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))
|
123
lib/tokenapp.js
123
lib/tokenapp.js
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
101
lib/utils.js
101
lib/utils.js
@@ -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
118
lib/webui.js
Normal 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()
|
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
export * from './lib/main.js'
|
import './lib/main.js'
|
||||||
|
270
web/account.html
270
web/account.html
@@ -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>
|
215
web/code.html
215
web/code.html
@@ -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>
|
@@ -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>
|
110
web/restart.html
110
web/restart.html
@@ -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>
|
Reference in New Issue
Block a user