mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-09-26 21:01:12 +08:00
Merge branch 'dev' of https://github.com/tsightler/ring-mqtt into dev
This commit is contained in:
86
lib/main.js
86
lib/main.js
@@ -1,28 +1,29 @@
|
||||
export * from './exithandler.js'
|
||||
export * from './mqtt.js'
|
||||
import './processhandlers.js'
|
||||
import './mqtt.js'
|
||||
import state from './state.js'
|
||||
import ring from './ring.js'
|
||||
import utils from './utils.js'
|
||||
import tokenApp from './tokenapp.js'
|
||||
import webui from './webui.js'
|
||||
import chalk from 'chalk'
|
||||
import isOnline from 'is-online'
|
||||
import debugModule from 'debug'
|
||||
|
||||
const debug = debugModule('ring-mqtt')
|
||||
|
||||
export default new class Main {
|
||||
constructor() {
|
||||
// Hack to suppress spurious message from push-receiver during startup
|
||||
console.warn = (data) => {
|
||||
if (data.includes('PHONE_REGISTRATION_ERROR') ||
|
||||
data.startsWith('Retry...') ||
|
||||
data.includes('Message dropped as it could not be decrypted:')
|
||||
) {
|
||||
return
|
||||
console.warn = (message) => {
|
||||
const suppressedMessages = [
|
||||
/^Retry\.\.\./,
|
||||
/PHONE_REGISTRATION_ERROR/,
|
||||
/Message dropped as it could not be decrypted:/
|
||||
]
|
||||
|
||||
if (!suppressedMessages.some(suppressedMessages => suppressedMessages.test(message))) {
|
||||
console.error(message)
|
||||
}
|
||||
console.error(data)
|
||||
}
|
||||
|
||||
// Start event listeners
|
||||
utils.event.on('generated_token', (generatedToken) => {
|
||||
this.init(generatedToken)
|
||||
})
|
||||
@@ -33,34 +34,45 @@ export default new class Main {
|
||||
async init(generatedToken) {
|
||||
if (!state.valid) {
|
||||
await state.init()
|
||||
tokenApp.setSystemId(state.data.systemId)
|
||||
}
|
||||
|
||||
// Is there any usable token?
|
||||
if (state.data.ring_token || generatedToken) {
|
||||
// Wait for the network to be online and then attempt to connect to the Ring API using the token
|
||||
while (!(await isOnline())) {
|
||||
debug(chalk.yellow('Network is offline, waiting 10 seconds to check again...'))
|
||||
await utils.sleep(10)
|
||||
}
|
||||
const hasToken = state.data.ring_token || generatedToken
|
||||
if (!hasToken) {
|
||||
this.handleNoToken()
|
||||
return
|
||||
}
|
||||
|
||||
if (!await ring.init(state, generatedToken)) {
|
||||
debug(chalk.red('Failed to connect to Ring API using saved token, generate a new token using the Web UI'))
|
||||
debug(chalk.red('or wait 60 seconds to automatically retry authentication using the existing token'))
|
||||
tokenApp.start()
|
||||
await utils.sleep(60)
|
||||
if (!ring.client) {
|
||||
debug(chalk.yellow('Retrying authentication with existing saved token...'))
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (process.env.RUNMODE === 'addon') {
|
||||
debug(chalk.red('No refresh token was found in state file, generate a token using the addon Web UI'))
|
||||
} else {
|
||||
tokenApp.start()
|
||||
debug(chalk.red('No refresh token was found in the state file, use the Web UI at http://<host_ip_address>:55123/ to generate a token.'))
|
||||
await this.waitForNetwork()
|
||||
await this.attemptRingConnection(generatedToken)
|
||||
}
|
||||
|
||||
async waitForNetwork() {
|
||||
while (!(await isOnline())) {
|
||||
debug(chalk.yellow('Network is offline, waiting 10 seconds to check again...'))
|
||||
await utils.sleep(10)
|
||||
}
|
||||
}
|
||||
|
||||
async attemptRingConnection(generatedToken) {
|
||||
if (!await ring.init(state, generatedToken)) {
|
||||
debug(chalk.red('Failed to connect to Ring API using saved token, generate a new token using the Web UI'))
|
||||
debug(chalk.red('or wait 60 seconds to automatically retry authentication using the existing token'))
|
||||
webui.start(state.data.systemId)
|
||||
await utils.sleep(60)
|
||||
|
||||
if (!ring.client) {
|
||||
debug(chalk.yellow('Retrying authentication with existing saved token...'))
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNoToken() {
|
||||
if (process.env.RUNMODE === 'addon') {
|
||||
debug(chalk.red('No refresh token was found in state file, generate a token using the addon Web UI'))
|
||||
} else {
|
||||
webui.start(state.data.systemId)
|
||||
debug(chalk.red('No refresh token was found in the state file, use the Web UI at http://<host_ip_address>:55123/ to generate a token.'))
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,13 +4,12 @@ import ring from './ring.js'
|
||||
import debugModule from 'debug'
|
||||
const debug = debugModule('ring-mqtt')
|
||||
|
||||
export default new class ExitHandler {
|
||||
export default new class ProcessHandlers {
|
||||
constructor() {
|
||||
this.init()
|
||||
}
|
||||
|
||||
init() {
|
||||
// Setup Exit Handlers
|
||||
process.on('exit', this.processExit.bind(null, 0))
|
||||
process.on('SIGINT', this.processExit.bind(null, 0))
|
||||
process.on('SIGTERM', this.processExit.bind(null, 0))
|
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 { promisify } from 'util'
|
||||
import { EventEmitter } from 'events'
|
||||
import debugModule from 'debug'
|
||||
const debug = {
|
||||
mqtt: debugModule('ring-mqtt'),
|
||||
attr: debugModule('ring-attr'),
|
||||
disc: debugModule('ring-disc'),
|
||||
rtsp: debugModule('ring-rtsp'),
|
||||
wrtc: debugModule('ring-wrtc')
|
||||
import debug from 'debug'
|
||||
|
||||
const debuggers = {
|
||||
mqtt: debug('ring-mqtt'),
|
||||
attr: debug('ring-attr'),
|
||||
disc: debug('ring-disc'),
|
||||
rtsp: debug('ring-rtsp'),
|
||||
wrtc: debug('ring-wrtc')
|
||||
}
|
||||
|
||||
export default new class Utils {
|
||||
class Utils {
|
||||
constructor() {
|
||||
this.event = new EventEmitter()
|
||||
this.dnsLookup = promisify(dns.lookup)
|
||||
this.dnsLookupService = promisify(dns.lookupService)
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.event = new EventEmitter()
|
||||
}
|
||||
config() {
|
||||
return config.data
|
||||
}
|
||||
|
||||
config() {
|
||||
return config.data
|
||||
}
|
||||
sleep(sec) {
|
||||
return this.msleep(sec * 1000)
|
||||
}
|
||||
|
||||
// Sleep function (seconds)
|
||||
sleep(sec) {
|
||||
return this.msleep(sec*1000)
|
||||
}
|
||||
msleep(msec) {
|
||||
return new Promise(res => setTimeout(res, msec))
|
||||
}
|
||||
|
||||
// Sleep function (milliseconds)
|
||||
msleep(msec) {
|
||||
return new Promise(res => setTimeout(res, msec))
|
||||
}
|
||||
getISOTime(epoch) {
|
||||
return new Date(epoch).toISOString().slice(0, -5) + 'Z'
|
||||
}
|
||||
|
||||
// Return ISO time from epoch without milliseconds
|
||||
getISOTime(epoch) {
|
||||
return new Date(epoch).toISOString().slice(0,-5)+"Z"
|
||||
async getHostFqdn() {
|
||||
try {
|
||||
const ip = await this.getHostIp()
|
||||
const { hostname } = await this.dnsLookupService(ip, 0)
|
||||
return hostname
|
||||
} catch (error) {
|
||||
console.warn('Failed to resolve FQDN, using os.hostname() instead:', error.message)
|
||||
return os.hostname()
|
||||
}
|
||||
}
|
||||
|
||||
async getHostFqdn() {
|
||||
const pLookupService = promisify(dns.lookupService)
|
||||
try {
|
||||
return (await pLookupService(await this.getHostIp(), 0)).hostname
|
||||
} catch {
|
||||
console.log('Failed to resolve FQDN, using os.hostname() instead')
|
||||
return os.hostname()
|
||||
}
|
||||
async getHostIp() {
|
||||
try {
|
||||
const { address } = await this.dnsLookup(os.hostname())
|
||||
return address
|
||||
} catch (error) {
|
||||
console.warn('Failed to resolve hostname IP address, returning localhost instead:', error.message)
|
||||
return 'localhost'
|
||||
}
|
||||
}
|
||||
|
||||
async getHostIp() {
|
||||
try {
|
||||
const pLookup = promisify(dns.lookup)
|
||||
return (await pLookup(os.hostname())).address
|
||||
} catch {
|
||||
console.log('Failed to resolve hostname IP address, returning localhost instead')
|
||||
return 'localhost'
|
||||
}
|
||||
}
|
||||
isNumeric(num) {
|
||||
return !isNaN(parseFloat(num)) && isFinite(num)
|
||||
}
|
||||
|
||||
isNumeric(num) {
|
||||
return !isNaN(parseFloat(num)) && isFinite(num);
|
||||
}
|
||||
|
||||
debug(message, debugType) {
|
||||
debugType = debugType ? debugType : 'mqtt'
|
||||
debug[debugType](message)
|
||||
}
|
||||
debug(message, debugType = 'mqtt') {
|
||||
debuggers[debugType]?.(message)
|
||||
}
|
||||
}
|
||||
|
||||
export default new Utils()
|
118
lib/webui.js
Normal file
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
|
||||
export * from './lib/main.js'
|
||||
import './lib/main.js'
|
||||
|
270
web/account.html
270
web/account.html
@@ -1,92 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;}
|
||||
* {box-sizing: border-box;}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ring-MQTT Web Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #47a9e6;
|
||||
--primary-hover: #315b82;
|
||||
--bg-color: #f2f2f2;
|
||||
--error-color: #dc3545;
|
||||
--success-color: #39ff14;
|
||||
--text-color: #333;
|
||||
--spacing: 1rem;
|
||||
}
|
||||
|
||||
input[type=text], select, textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 16px;
|
||||
resize: vertical;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
color: white;
|
||||
background-color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
input[type=password], select, textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 16px;
|
||||
resize: vertical;
|
||||
}
|
||||
.container {
|
||||
background-color: var(--bg-color);
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: var(--text-color);
|
||||
margin-top: var(--spacing);
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
background-color: #47a9e6;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
h2 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
background-color: #315b82;
|
||||
}
|
||||
h3 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
border-radius: 5px;
|
||||
background-color: #f2f2f2;
|
||||
padding: 20px;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
.container h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-name span {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2);
|
||||
border-radius: 4px;
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: calc(var(--spacing) / 2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: calc(var(--spacing) / 2);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.2);
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(var(--spacing) / 2);
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2) var(--spacing);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: calc(var(--spacing) / 2);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--spacing);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Ring-MQTT Authentication</h2>
|
||||
<div id="displayName" class="display-name">Device Name: <span></span></div>
|
||||
<p id="errormsg" class="error-message" style="display: none;"></p>
|
||||
|
||||
<h2>Acquire Refresh Token</h2>
|
||||
<h3 id="systemId"></h3>
|
||||
Please enter your Ring account information below and the 2FA code on the following page to generate the refresh token required for this application to access the Ring API.<br>
|
||||
<p style="background-color:red" id="errormsg"></p>
|
||||
<h3>Login</h3>
|
||||
<div class="container">
|
||||
<form action="./submit-account" method="post">
|
||||
<label for="email">Email</label>
|
||||
<input type="text" id="email" name="email">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password">
|
||||
<input type="checkbox" onclick="showPassword()">Show Password
|
||||
<br><br>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
function showPassword() {
|
||||
var x = document.getElementById("password");
|
||||
if (x.type === "password") {
|
||||
x.type = "text";
|
||||
} else {
|
||||
x.type = "password";
|
||||
}
|
||||
}
|
||||
function getCookie(key) {
|
||||
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return keyValue ? keyValue[2] : null;
|
||||
}
|
||||
if (getCookie('error')) {
|
||||
document.getElementById("errormsg").innerHTML = getCookie('error')
|
||||
}
|
||||
if (getCookie('systemId')) {
|
||||
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
|
||||
}
|
||||
</script>
|
||||
<div class="container">
|
||||
<h3>Ring Account Info</h3>
|
||||
<form action="./submit-account" method="post">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="form-control"
|
||||
autocomplete="email"
|
||||
required
|
||||
aria-required="true">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="form-control"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
aria-required="true">
|
||||
<div class="password-toggle">
|
||||
<input type="checkbox" id="showPassword">
|
||||
<label for="showPassword">Show Password</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Password visibility toggle
|
||||
document.getElementById('showPassword').addEventListener('change', function() {
|
||||
const password = document.getElementById('password');
|
||||
password.type = this.checked ? 'text' : 'password';
|
||||
});
|
||||
|
||||
// Cookie handling
|
||||
function getCookie(key) {
|
||||
const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return value ? decodeURIComponent(value[2]) : null;
|
||||
}
|
||||
|
||||
// Error message handling
|
||||
const errorMessage = getCookie('error');
|
||||
const errorElement = document.getElementById('errormsg');
|
||||
if (errorMessage) {
|
||||
errorElement.style.display = 'block';
|
||||
errorElement.textContent = errorMessage;
|
||||
}
|
||||
|
||||
// System ID display
|
||||
const displayName = getCookie('displayName');
|
||||
if (displayName) {
|
||||
document.querySelector('#displayName span').textContent = displayName;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
215
web/code.html
215
web/code.html
@@ -1,67 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;}
|
||||
* {box-sizing: border-box;}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ring-MQTT Web Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #47a9e6;
|
||||
--primary-hover: #315b82;
|
||||
--bg-color: #f2f2f2;
|
||||
--error-color: #dc3545;
|
||||
--success-color: #39ff14;
|
||||
--text-color: #333;
|
||||
--spacing: 1rem;
|
||||
}
|
||||
|
||||
input[type=text], select, textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 16px;
|
||||
resize: vertical;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
color: white;
|
||||
background-color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
background-color: #47a9e6;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.container {
|
||||
background-color: var(--bg-color);
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: var(--text-color);
|
||||
margin-top: var(--spacing);
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
background-color: #315b82;
|
||||
}
|
||||
h2 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
border-radius: 5px;
|
||||
background-color: #f2f2f2;
|
||||
padding: 20px;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
h3 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-name span {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2);
|
||||
border-radius: 4px;
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: calc(var(--spacing) / 2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: calc(var(--spacing) / 2);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2) var(--spacing);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: calc(var(--spacing) / 2);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--spacing);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Ring-MQTT Two-Factor Authentication</h2>
|
||||
<div id="displayName" class="display-name">Device Name: <span></span></div>
|
||||
<p id="errormsg" class="error-message" style="display: none;"></p>
|
||||
|
||||
<h2>Enter 2FA Code</h2>
|
||||
<h3 id="systemId"></h3>
|
||||
<p style="background-color:Red;" id="errormsg"></p></br>
|
||||
<div class="container">
|
||||
<form action="./submit-code" method="post">
|
||||
<label for="2facode">Code</label>
|
||||
<input type="text" id="code" name="code">
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
function getCookie(key) {
|
||||
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return keyValue ? keyValue[2] : null;
|
||||
}
|
||||
if (getCookie('error')) {
|
||||
document.getElementById("errormsg").innerHTML = getCookie('error')
|
||||
}
|
||||
if (getCookie('systemId')) {
|
||||
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
|
||||
}
|
||||
</script>
|
||||
<div class="container">
|
||||
<form action="./submit-code" method="post">
|
||||
<div class="form-group">
|
||||
<label for="code">Enter 2FA Code</label>
|
||||
<input type="text"
|
||||
id="code"
|
||||
name="code"
|
||||
class="form-control"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
required
|
||||
aria-required="true">
|
||||
</div>
|
||||
<button type="submit" class="btn">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cookie handling
|
||||
function getCookie(key) {
|
||||
const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return value ? decodeURIComponent(value[2]) : null;
|
||||
}
|
||||
|
||||
// Error message handling
|
||||
const errorMessage = getCookie('error');
|
||||
const errorElement = document.getElementById('errormsg');
|
||||
if (errorMessage) {
|
||||
errorElement.style.display = 'block';
|
||||
errorElement.textContent = errorMessage;
|
||||
}
|
||||
|
||||
// System ID display
|
||||
const displayName = getCookie('displayName');
|
||||
if (displayName) {
|
||||
document.querySelector('#displayName span').textContent = displayName;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
@@ -1,46 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ring-MQTT Web Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #47a9e6;
|
||||
--primary-hover: #315b82;
|
||||
--bg-color: #f2f2f2;
|
||||
--error-color: #dc3545;
|
||||
--success-color: #39ff14;
|
||||
--text-color: #333;
|
||||
--spacing: 1rem;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
background-color: #47a9e6;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
color: white;
|
||||
background-color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
background-color: #315b82;
|
||||
}
|
||||
h2 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20px 5px;
|
||||
}
|
||||
</style>
|
||||
h3 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-name span {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--spacing);
|
||||
padding: var(--spacing) 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: calc(var(--spacing) / 2) var(--spacing);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(71, 169, 230, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: calc(var(--spacing) / 2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Ring Device Addon Connected</h2>
|
||||
<h3 id="systemId"></h3>
|
||||
It appears that this addon is already authenticated and connected to the Ring API, no additional action is required.
|
||||
If you wish to force reauthentication, for example, to change the account used by this addon, clicking the button below will restart the authentication process.
|
||||
<div class="container">
|
||||
<form action="./force-token-generation" method="get">
|
||||
<input type="submit" value="Force Reauthentication">
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
function getCookie(key) {
|
||||
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return keyValue ? keyValue[2] : null;
|
||||
}
|
||||
if (getCookie('systemId')) {
|
||||
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
|
||||
}
|
||||
</script>
|
||||
<h2>Ring-MQTT Addon Connected</h2>
|
||||
<div id="displayName" class="display-name">Device Name: <span></span></div>
|
||||
<div class="message">
|
||||
It appears that <strong>ring-mqtt</strong> is already connected to the Ring API, no additional action is required.
|
||||
</div>
|
||||
<div class="message">
|
||||
If you wish to force reauthentication, for example, to change the account used by this addon, clicking the button below will restart the authentication process.
|
||||
</div>
|
||||
<div class="container">
|
||||
<form action="./force-token-generation" method="get">
|
||||
<input type="submit" value="Force Reauthentication" class="btn">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getCookie(key) {
|
||||
const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return value ? decodeURIComponent(value[2]) : null;
|
||||
}
|
||||
|
||||
const displayName = getCookie('displayName');
|
||||
if (displayName) {
|
||||
document.querySelector('#displayName span').textContent = displayName;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
110
web/restart.html
110
web/restart.html
@@ -1,23 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white; background-color: gray;}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ring-MQTT Web Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #47a9e6;
|
||||
--primary-hover: #315b82;
|
||||
--bg-color: #f2f2f2;
|
||||
--error-color: #dc3545;
|
||||
--success-color: #39ff14;
|
||||
--text-color: #333;
|
||||
--spacing: 1rem;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
color: white;
|
||||
background-color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: var(--bg-color);
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: var(--text-color);
|
||||
margin-top: var(--spacing);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--spacing) 0;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin: calc(var(--spacing) / 2) 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-name span {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: var(--spacing);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: calc(var(--spacing) / 2);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--spacing);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Refresh Token Generation Complete</h2>
|
||||
<h3 id="systemId"></h3>
|
||||
The <b>Home Assistant ring-mqtt Add-on</b> will now automatically connect using the generated token. No additional configuration steps are required, please check the addon logs to monitor progress.
|
||||
<script>
|
||||
function getCookie(key) {
|
||||
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return keyValue ? keyValue[2] : null;
|
||||
}
|
||||
if (getCookie('systemId')) {
|
||||
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
|
||||
}
|
||||
</script>
|
||||
<h2>Ring-MQTT Authentication Complete</h2>
|
||||
<div id="displayName" class="display-name">Device Name: <span></span></div>
|
||||
<div class="message">
|
||||
Authentication with Ring was successful and <strong>ring-mqtt</strong> will now attempt to connect to Ring servers. No additional steps are required, please review the ring-mqtt logs to monitor progress.
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cookie handling
|
||||
function getCookie(key) {
|
||||
const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
|
||||
return value ? decodeURIComponent(value[2]) : null;
|
||||
}
|
||||
|
||||
// System ID display
|
||||
const displayName = getCookie('displayName');
|
||||
if (displayName) {
|
||||
document.querySelector('#displayName span').textContent = displayName;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
Reference in New Issue
Block a user