mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-09-26 21:01:12 +08:00
@@ -1,3 +1,17 @@
|
||||
## v5.8.0
|
||||
The 5.8.x branch is focused primarily on cleaning up various portions of the code to improve long-term maintainability, there are no major features planned for this branch.
|
||||
|
||||
**Minor Enhancements**
|
||||
- The web based authenticator has been completely re-written in this release. It now serves an singlem dynamic html document, from memory, properly displays error messages, more safely handles sensitive data such as passwords/2fa codes (information is deleted from session state immediately after it is submitted) and uses an updated theme that integrates more cleanly into the Home Assistant UI, including automatic support for light/dark mode.
|
||||
|
||||
**Dependency Updates**
|
||||
- @homebridge/camera-utils 2.2.7
|
||||
- debug 4.4.0
|
||||
- expess 4.21.2
|
||||
- mqtt 5.10.3
|
||||
- ring-client-api 13.2.1
|
||||
- NodeJS 22.12.0
|
||||
|
||||
## v5.7.3
|
||||
**Minor Enhancements**
|
||||
- Instead of silently blocking device discovery, offline hubs will now be listed in the log which should make it easier for users to identify the device causing the issue.
|
||||
|
91
lib/main.js
91
lib/main.js
@@ -1,28 +1,29 @@
|
||||
export * from './exithandler.js'
|
||||
export * from './mqtt.js'
|
||||
import './process-handlers.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 webService from './web-service.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,50 @@ 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)
|
||||
}
|
||||
// For the HA addon, Web UI is always started
|
||||
if (process.env.RUNMODE === 'addon') {
|
||||
webService.start(state.data.systemId)
|
||||
}
|
||||
|
||||
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.'))
|
||||
const hasToken = state.data.ring_token || generatedToken
|
||||
if (!hasToken) {
|
||||
this.handleNoToken()
|
||||
return
|
||||
}
|
||||
|
||||
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'))
|
||||
webService.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 {
|
||||
webService.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()
|
119
lib/web-service.js
Normal file
119
lib/web-service.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { RingRestClient } from 'ring-client-api/rest-client'
|
||||
import utils from './utils.js'
|
||||
import express from 'express'
|
||||
import bodyParser from 'body-parser'
|
||||
import chalk from 'chalk'
|
||||
import debugModule from 'debug'
|
||||
import { webTemplate } from './web-template.js'
|
||||
|
||||
const debug = debugModule('ring-mqtt')
|
||||
|
||||
class WebService {
|
||||
constructor() {
|
||||
this.app = express()
|
||||
this.listener = null
|
||||
this.ringConnected = false
|
||||
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()
|
||||
res.json({ success: true })
|
||||
} catch (error) {
|
||||
if (restClient.using2fa) {
|
||||
debug('Username/Password was accepted, waiting for 2FA code to be entered.')
|
||||
res.json({ requires2fa: true })
|
||||
} else {
|
||||
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.status(400).json({ error: errorMessage })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleCodeSubmission(req, res, restClient) {
|
||||
try {
|
||||
const generatedToken = await restClient.getAuth(req.body.code)
|
||||
if (generatedToken) {
|
||||
utils.event.emit('generated_token', generatedToken.refresh_token)
|
||||
res.json({ success: true })
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || 'The 2FA code was not accepted, please verify the code and try again.'
|
||||
debug(chalk.red(errorMessage))
|
||||
res.status(400).json({ error: errorMessage })
|
||||
}
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
let restClient
|
||||
this.app.use(bodyParser.urlencoded({ extended: false }))
|
||||
this.app.use(bodyParser.json())
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/get-state', (req, res) => {
|
||||
res.json({
|
||||
connected: this.ringConnected,
|
||||
displayName: this.displayName
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/submit-account', async (req, res) => {
|
||||
restClient = new RingRestClient({
|
||||
email: req.body.email,
|
||||
password: req.body.password,
|
||||
controlCenterDisplayName: this.displayName,
|
||||
systemId: this.systemId
|
||||
})
|
||||
await this.handleAccountSubmission(req, res, restClient)
|
||||
})
|
||||
|
||||
router.post('/submit-code', async (req, res) => {
|
||||
await this.handleCodeSubmission(req, res, restClient)
|
||||
})
|
||||
|
||||
// Mount router at base URL
|
||||
this.app.use('/', router)
|
||||
|
||||
// Serve the static HTML
|
||||
this.app.get('*', (req, res) => {
|
||||
res.send(webTemplate)
|
||||
})
|
||||
}
|
||||
|
||||
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(55123, () => {
|
||||
debug('Successfully started the ring-mqtt web UI')
|
||||
})
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.listener) {
|
||||
await this.listener.close()
|
||||
this.listener = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebService()
|
565
lib/web-template.js
Normal file
565
lib/web-template.js
Normal file
@@ -0,0 +1,565 @@
|
||||
export const webTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Ring-MQTT Authenticator</title>
|
||||
<style>
|
||||
:root {
|
||||
/* Light theme variables */
|
||||
--bg-color: #f5f5f5;
|
||||
--container-bg: #fff;
|
||||
--text-color: #212529;
|
||||
--header-bg: #2f95c8;
|
||||
--header-color: #ffffff;
|
||||
--input-bg: #fff;
|
||||
--input-color: #495057;
|
||||
--input-border: #ced4da;
|
||||
--instruction-color: #6c757d;
|
||||
--error-bg: #f8d7da;
|
||||
--error-color: #721c24;
|
||||
--error-border: #f5c6cb;
|
||||
--success-bg: #e8f5ee;
|
||||
--success-color: #006400;
|
||||
--success-border: #c6e6d5;
|
||||
--button-primary-bg: #2f95c8;
|
||||
--button-primary-border: #2784b3;
|
||||
--button-secondary-bg: #6c757d;
|
||||
--button-secondary-border: #6c757d;
|
||||
--device-name-color: #00cc00;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: #1a1a1a;
|
||||
--container-bg: #2d2d2d;
|
||||
--text-color: #e0e0e0;
|
||||
--header-bg: #1e5c7c;
|
||||
--header-color: #ffffff;
|
||||
--input-bg: #3d3d3d;
|
||||
--input-color: #e0e0e0;
|
||||
--input-border: #4d4d4d;
|
||||
--instruction-color: #a0a0a0;
|
||||
--error-bg: #442326;
|
||||
--error-color: #ff9999;
|
||||
--error-border: #662629;
|
||||
--success-bg: #1e3323;
|
||||
--success-color: #90ee90;
|
||||
--success-border: #2d4d33;
|
||||
--button-primary-bg: #1e5c7c;
|
||||
--button-primary-border: #164459;
|
||||
--button-secondary-bg: #4d4d4d;
|
||||
--button-secondary-border: #404040;
|
||||
--device-name-color: #66ff66;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--header-color);
|
||||
background-color: var(--header-bg);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
background-color: var(--error-bg);
|
||||
border: 1px solid var(--error-border);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin-top: 1rem;
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: 1rem auto;
|
||||
padding: 1.5rem;
|
||||
background-color: var(--container-bg);
|
||||
border: 1px solid rgba(0,0,0,.125);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,.075);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.instruction {
|
||||
text-align: center;
|
||||
color: var(--instruction-color);
|
||||
margin-bottom: .75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
color: var(--input-color);
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
margin-top: 0.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
color: var(--input-color);
|
||||
background-color: var(--input-bg);
|
||||
border-color: #80bdff;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--header-color);
|
||||
background-color: var(--button-primary-bg);
|
||||
border: 1px solid var(--button-primary-border);
|
||||
cursor: pointer;
|
||||
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: var(--header-color);
|
||||
background-color: #2680b0;
|
||||
border-color: #206d94;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(47,149,200,.5);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--header-color);
|
||||
cursor: pointer;
|
||||
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
.button-group .back-button {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
background-color: var(--button-secondary-bg);
|
||||
border-color: var(--button-secondary-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
display: inline-block;
|
||||
width: 0.6em;
|
||||
height: 0.6em;
|
||||
border-left: 0.2em solid currentColor;
|
||||
border-bottom: 0.2em solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
margin-right: 0.2em;
|
||||
position: relative;
|
||||
top: -0.1em;
|
||||
}
|
||||
|
||||
.button-group .submit-button {
|
||||
flex: 1;
|
||||
background-color: var(--button-primary-bg);
|
||||
border-color: var(--button-primary-border);
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #5a6268;
|
||||
border-color: #545b62;
|
||||
}
|
||||
|
||||
.back-button:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(108,117,125,.5);
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #2680b0;
|
||||
border-color: #206d94;
|
||||
}
|
||||
|
||||
.submit-button:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(47,149,200,.5);
|
||||
}
|
||||
|
||||
#togglePassword {
|
||||
width: auto;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-44%);
|
||||
padding: 2px 6px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--header-color);
|
||||
background-color: var(--button-primary-bg);
|
||||
border: 1px solid var(--button-primary-border);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
#togglePassword:hover {
|
||||
background-color: #2680b0;
|
||||
border-color: #206d94;
|
||||
}
|
||||
|
||||
#togglePassword:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.5);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.password-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-container input {
|
||||
padding-right: 85px;
|
||||
}
|
||||
|
||||
#displayName {
|
||||
color: var(--instruction-color);
|
||||
text-align: center;
|
||||
margin-bottom: .75rem;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
color: var(--device-name-color);
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: var(--success-color);
|
||||
background-color: var(--success-bg);
|
||||
border: 1px solid var(--success-border);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin-top: 1rem;
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
#connectedMessage .message:last-child {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#reauth {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.container {
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 id="title">Ring-MQTT Authenticator</h1>
|
||||
<p class="instruction">Authenticate ring-mqtt to your Ring account</p>
|
||||
<div id="displayName"></div>
|
||||
|
||||
<div id="successMessage" class="hidden">
|
||||
<p 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.</p>
|
||||
</div>
|
||||
|
||||
<div id="connectedMessage" class="hidden">
|
||||
<p class="message">It appears that <strong>ring-mqtt</strong> is already connected to a Ring account.</p>
|
||||
</div>
|
||||
|
||||
<div id="reauthMessage" class="hidden">
|
||||
<p class="message">If you wish to force reauthentication, for example, to change the account used by this addon, click the button below to restart the authentication process.</p>
|
||||
<button id="reauth">Force Reauthentication</button>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="hidden">
|
||||
<div class="form-group">
|
||||
<label>Email Address</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<div class="password-container">
|
||||
<input type="password" id="password" name="password" required>
|
||||
<button type="button" id="togglePassword">Show</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<form id="twoFactorForm" class="hidden">
|
||||
<div class="form-group">
|
||||
<label>Enter 2FA Code</label>
|
||||
<input type="text" id="code" name="code" required>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button type="button" class="back-button" id="backToLogin">
|
||||
<span class="back-arrow"></span>
|
||||
Back
|
||||
</button>
|
||||
<button type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="error" class="error hidden"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
class UIState {
|
||||
static showElement(selector) {
|
||||
document.querySelector(selector).classList.remove('hidden');
|
||||
}
|
||||
|
||||
static hideElement(selector) {
|
||||
document.querySelector(selector).classList.add('hidden');
|
||||
}
|
||||
|
||||
static setDisplayName(name) {
|
||||
const element = document.querySelector('#displayName');
|
||||
element.textContent = 'Device Name: ';
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'device-name';
|
||||
nameSpan.textContent = name;
|
||||
element.appendChild(nameSpan);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorHandler {
|
||||
static #fadeTimeout;
|
||||
|
||||
static show(message) {
|
||||
const errorDiv = document.querySelector('#error');
|
||||
errorDiv.textContent = message;
|
||||
UIState.showElement('#error');
|
||||
|
||||
if (this.#fadeTimeout) {
|
||||
clearTimeout(this.#fadeTimeout);
|
||||
}
|
||||
|
||||
this.#fadeTimeout = setTimeout(() => {
|
||||
errorDiv.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
errorDiv.classList.add('hidden');
|
||||
errorDiv.classList.remove('fade-out');
|
||||
}, 500);
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
static hide() {
|
||||
const errorDiv = document.querySelector('#error');
|
||||
errorDiv.textContent = '';
|
||||
UIState.hideElement('#error');
|
||||
}
|
||||
}
|
||||
|
||||
class AuthService {
|
||||
static async getInitialState() {
|
||||
const response = await fetch('get-state');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
static async submitAccount(formData) {
|
||||
const response = await fetch('submit-account', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static async submitCode(formData) {
|
||||
const response = await fetch('submit-code', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class AuthForm {
|
||||
static async handleLoginSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const passwordInput = document.querySelector('#password');
|
||||
try {
|
||||
const data = await AuthService.submitAccount(new FormData(event.target));
|
||||
ErrorHandler.hide();
|
||||
|
||||
if (data.requires2fa) {
|
||||
UIState.hideElement('#loginForm');
|
||||
UIState.showElement('#twoFactorForm');
|
||||
} else if (data.success) {
|
||||
UIState.hideElement('#loginForm');
|
||||
UIState.showElement('#successMessage');
|
||||
}
|
||||
} catch (err) {
|
||||
ErrorHandler.show(err.message);
|
||||
passwordInput.focus();
|
||||
} finally {
|
||||
passwordInput.value = '';
|
||||
passwordInput.type = 'password';
|
||||
const toggleButton = document.querySelector('#togglePassword');
|
||||
toggleButton.textContent = 'Show';
|
||||
}
|
||||
}
|
||||
|
||||
static async handleTwoFactorSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const codeInput = document.querySelector('#code');
|
||||
try {
|
||||
const data = await AuthService.submitCode(new FormData(event.target));
|
||||
ErrorHandler.hide();
|
||||
|
||||
if (data.success) {
|
||||
UIState.hideElement('#twoFactorForm');
|
||||
UIState.hideElement('#connectedMessage');
|
||||
UIState.showElement('#successMessage');
|
||||
}
|
||||
} catch (err) {
|
||||
ErrorHandler.show(err.message);
|
||||
codeInput.focus();
|
||||
} finally {
|
||||
codeInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
static togglePasswordVisibility(event) {
|
||||
const passwordInput = document.querySelector('#password');
|
||||
const type = passwordInput.type === 'password' ? 'text' : 'password';
|
||||
passwordInput.type = type;
|
||||
event.target.textContent = type === 'password' ? 'Show' : 'Hide';
|
||||
}
|
||||
|
||||
static handleBackToLogin() {
|
||||
ErrorHandler.hide();
|
||||
UIState.hideElement('#twoFactorForm');
|
||||
UIState.showElement('#loginForm');
|
||||
const passwordInput = document.querySelector('#password');
|
||||
|
||||
setTimeout(() => {
|
||||
passwordInput.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
static handleReauth() {
|
||||
try {
|
||||
sessionStorage.setItem('forceReauth', 'true');
|
||||
UIState.hideElement('#connectedMessage');
|
||||
UIState.hideElement('#reauthMessage');
|
||||
UIState.showElement('#loginForm');
|
||||
} catch (err) {
|
||||
ErrorHandler.show('Failed to initiate reauthentication');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AuthApp {
|
||||
static async initialize() {
|
||||
try {
|
||||
const data = await AuthService.getInitialState();
|
||||
|
||||
if (data.displayName) {
|
||||
UIState.setDisplayName(data.displayName);
|
||||
}
|
||||
|
||||
if (data.connected) {
|
||||
UIState.showElement('#connectedMessage');
|
||||
if (sessionStorage.getItem('forceReauth')) {
|
||||
UIState.showElement('#loginForm');
|
||||
} else {
|
||||
UIState.showElement('#reauthMessage');
|
||||
}
|
||||
} else {
|
||||
UIState.showElement('#loginForm');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to get initial state:', err);
|
||||
UIState.showElement('#loginForm');
|
||||
}
|
||||
}
|
||||
|
||||
static setupEventListeners() {
|
||||
document.querySelector('#loginForm')
|
||||
.addEventListener('submit', AuthForm.handleLoginSubmit);
|
||||
|
||||
document.querySelector('#twoFactorForm')
|
||||
.addEventListener('submit', AuthForm.handleTwoFactorSubmit);
|
||||
|
||||
document.querySelector('#togglePassword')
|
||||
.addEventListener('click', AuthForm.togglePasswordVisibility);
|
||||
|
||||
document.querySelector('#backToLogin')
|
||||
.addEventListener('click', AuthForm.handleBackToLogin);
|
||||
|
||||
document.querySelector('#reauth')
|
||||
.addEventListener('click', AuthForm.handleReauth);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
sessionStorage.clear();
|
||||
AuthApp.initialize();
|
||||
AuthApp.setupEventListeners();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
211
package-lock.json
generated
211
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ring-mqtt",
|
||||
"version": "5.7.3",
|
||||
"version": "5.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ring-mqtt",
|
||||
"version": "5.7.3",
|
||||
"version": "5.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@homebridge/camera-utils": "^2.2.7",
|
||||
@@ -14,21 +14,21 @@
|
||||
"body-parser": "^1.20.3",
|
||||
"chalk": "^5.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"debug": "^4.3.7",
|
||||
"express": "^4.21.1",
|
||||
"debug": "^4.4.0",
|
||||
"express": "^4.21.2",
|
||||
"is-online": "^11.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mqtt": "^5.10.2",
|
||||
"ring-client-api": "13.2.1-beta.0",
|
||||
"mqtt": "^5.10.3",
|
||||
"ring-client-api": "^13.2.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"werift": "^0.20.1",
|
||||
"write-file-atomic": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"eslint": "^9.15.0",
|
||||
"globals": "^15.12.0"
|
||||
"eslint": "^9.16.0",
|
||||
"globals": "^15.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
@@ -101,13 +101,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz",
|
||||
"integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==",
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
|
||||
"integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.4",
|
||||
"@eslint/object-schema": "^2.1.5",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
},
|
||||
@@ -116,11 +116,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz",
|
||||
"integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==",
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz",
|
||||
"integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
@@ -163,9 +166,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz",
|
||||
"integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==",
|
||||
"version": "9.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz",
|
||||
"integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -173,9 +176,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
|
||||
"integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
|
||||
"integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -183,9 +186,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
|
||||
"integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz",
|
||||
"integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -693,12 +696,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.3.tgz",
|
||||
"integrity": "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==",
|
||||
"version": "22.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz",
|
||||
"integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.8"
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/readable-stream": {
|
||||
@@ -1220,16 +1223,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.0",
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
"set-function-length": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -1238,6 +1240,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.0.tgz",
|
||||
"integrity": "sha512-CCKAP2tkPau7D3GE8+V8R6sQubA9R5foIzGp+85EXCVSCivuxBNAWqcpn72PKYiIcqoViv/kcUDpaEIMBVi1lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -1472,9 +1487,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -1592,9 +1607,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -1603,6 +1618,20 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz",
|
||||
"integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -1732,13 +1761,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -1772,9 +1798,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.15.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz",
|
||||
"integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==",
|
||||
"version": "9.16.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz",
|
||||
"integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1783,7 +1809,7 @@
|
||||
"@eslint/config-array": "^0.19.0",
|
||||
"@eslint/core": "^0.9.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "9.15.0",
|
||||
"@eslint/js": "9.16.0",
|
||||
"@eslint/plugin-kit": "^0.2.3",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -2009,9 +2035,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
@@ -2033,7 +2059,7 @@
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
@@ -2048,6 +2074,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
@@ -2311,16 +2341,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz",
|
||||
"integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.0",
|
||||
"dunder-proto": "^1.0.0",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2399,9 +2432,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "15.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz",
|
||||
"integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==",
|
||||
"version": "15.13.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz",
|
||||
"integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2412,12 +2445,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -2485,22 +2518,10 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-proto": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3302,9 +3323,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt": {
|
||||
"version": "5.10.2",
|
||||
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.2.tgz",
|
||||
"integrity": "sha512-Q8NrMXB6FwQ2DulGONeDb6BtFHxyQHmXWzDrSC724iyofxLleq/wuZmztV3kg1Kda9I7l0oHP+FKesowoFxyUg==",
|
||||
"version": "5.10.3",
|
||||
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.3.tgz",
|
||||
"integrity": "sha512-hA/6YrUS4fywhBGCjH/XXUuLeueJiPqruVVWjK2A24Ma4KcWfZ/x8x07aoesBV+HXDWBC08tbT4IWfSXNW0Jtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/readable-stream": "^4.0.5",
|
||||
@@ -3649,9 +3670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pick-port": {
|
||||
@@ -3963,9 +3984,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ring-client-api": {
|
||||
"version": "13.2.1-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/ring-client-api/-/ring-client-api-13.2.1-beta.0.tgz",
|
||||
"integrity": "sha512-icZNLCAhkjCbwm013G5o5Z1yc/8z9tr4CrJtSDfF9sJxkevFosRMXL9J/ZZrDTpnxi1W661hd4sh0VbnvVX3wQ==",
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ring-client-api/-/ring-client-api-13.2.1.tgz",
|
||||
"integrity": "sha512-eM+I6NhL2bNPluK+5+EgneiFEwfnMADZ8P30zgoRHErWkUQl6E7KXmX48dChjRjB+R/mi6yI5IT1zMSU17N6fQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "paypal",
|
||||
@@ -4634,9 +4655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
|
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ring-mqtt",
|
||||
"version": "5.7.3",
|
||||
"version": "5.8.0",
|
||||
"type": "module",
|
||||
"description": "Ring Devices via MQTT",
|
||||
"main": "ring-mqtt.js",
|
||||
@@ -10,21 +10,21 @@
|
||||
"body-parser": "^1.20.3",
|
||||
"chalk": "^5.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"debug": "^4.3.7",
|
||||
"express": "^4.21.1",
|
||||
"debug": "^4.4.0",
|
||||
"express": "^4.21.2",
|
||||
"is-online": "^11.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mqtt": "^5.10.2",
|
||||
"ring-client-api": "13.2.1-beta.0",
|
||||
"mqtt": "^5.10.3",
|
||||
"ring-client-api": "^13.2.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"werift": "^0.20.1",
|
||||
"write-file-atomic": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"eslint": "^9.15.0",
|
||||
"globals": "^15.12.0"
|
||||
"eslint": "^9.16.0",
|
||||
"globals": "^15.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
|
@@ -1,2 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
export * from './lib/main.js'
|
||||
import './lib/main.js'
|
||||
|
@@ -1,92 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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;}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
background-color: #315b82;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
border-radius: 5px;
|
||||
background-color: #f2f2f2;
|
||||
padding: 20px;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
@@ -1,67 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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;}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
background-color: #47a9e6;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
background-color: #315b82;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
border-radius: 5px;
|
||||
background-color: #f2f2f2;
|
||||
padding: 20px;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
@@ -1,46 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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;}
|
||||
|
||||
input[type=submit] {
|
||||
background-color: #47a9e6;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
background-color: #315b82;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20px 5px;
|
||||
}
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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>
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user