Files
location_managemet/app/templates/tuya/auth_settings.html

221 lines
8.1 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ board.name }} Tuya Settings{% endblock %}
{% block content %}
<div class="container py-4" style="max-width:560px">
<a href="{{ url_for('tuya.gateway', board_id=board.id) }}" class="btn btn-outline-secondary btn-sm mb-3">
<i class="bi bi-arrow-left me-1"></i>Back to Gateway
</a>
<div class="card bg-dark border-secondary">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-qr-code me-2 text-info"></i>Tuya / Smart Life Account
</h5>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} py-2 alert-dismissible fade show" role="alert">
{{ msg }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<p class="text-muted small mb-4">
Link your <strong>Tuya / Smart Life</strong> account by scanning a QR code with
the mobile app. No tokens or passwords are stored in plain text only the
access/refresh token pair returned by SSO is persisted.
</p>
{# Current status #}
{% if board.config.get('tuya_token_info') %}
<div class="alert alert-success py-2 d-flex align-items-center gap-2" id="linked-status">
<i class="bi bi-check-circle-fill"></i>
<span>
Account linked.
<small class="opacity-75">(User code: {{ board.config.get('tuya_user_code', '?') }})</small>
</span>
</div>
{% else %}
<div class="alert alert-secondary py-2 d-flex align-items-center gap-2" id="linked-status">
<i class="bi bi-x-circle"></i>
<span>No account linked yet.</span>
</div>
{% endif %}
{# QR section #}
<div id="qr-section">
<div class="mb-3">
<label for="input-user-code" class="form-label fw-semibold">Your Tuya User Code</label>
<input type="text" id="input-user-code" class="form-control"
placeholder="e.g. myname123"
value="{{ board.config.get('tuya_user_code', '') }}">
<div class="form-text">
Choose any short identifier for this installation (letters and numbers, no spaces).
You will need to remember it if you ever re-link the account.
</div>
</div>
<button id="btn-start-qr" class="btn btn-info mb-3">
<i class="bi bi-qr-code me-1"></i>Generate QR Code
</button>
<div id="qr-error-area"></div>
<div id="qr-area" class="text-center d-none">
<div id="qr-canvas" class="d-inline-block p-2 bg-white rounded mb-3"></div>
<p class="text-muted small mb-1" id="qr-instruction">
Open the <strong>Smart Life</strong> (or Tuya Smart) app → tap the
<i class="bi bi-qr-code-scan"></i> scan icon → scan this QR code.
</p>
<div class="d-flex align-items-center justify-content-center gap-2 mt-2">
<div class="spinner-border spinner-border-sm text-info" role="status" id="qr-spinner">
<span class="visually-hidden">Waiting…</span>
</div>
<span class="text-muted small" id="qr-poll-msg">Waiting for scan…</span>
</div>
<button id="btn-cancel-qr" class="btn btn-outline-secondary btn-sm mt-3">Cancel</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- QR code renderer (MIT, CDN) -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1/build/qrcode.min.js"></script>
<script>
(function() {
const BOARD_ID = {{ board.id }};
const QR_URL = "{{ url_for('tuya.generate_qr', board_id=board.id) }}";
const POLL_URL = "{{ url_for('tuya.poll_login', board_id=board.id) }}";
const SAVE_URL = "{{ url_for('tuya.save_auth', board_id=board.id) }}";
const GW_URL = "{{ url_for('tuya.gateway', board_id=board.id) }}";
let pollTimer = null;
let userCode = null;
let qrToken = null;
const btnStart = document.getElementById('btn-start-qr');
const btnCancel = document.getElementById('btn-cancel-qr');
const qrArea = document.getElementById('qr-area');
const qrCanvas = document.getElementById('qr-canvas');
const spinner = document.getElementById('qr-spinner');
const pollMsg = document.getElementById('qr-poll-msg');
const statusDiv = document.getElementById('linked-status');
const inputCode = document.getElementById('input-user-code');
btnStart.addEventListener('click', startQr);
btnCancel.addEventListener('click', cancelQr);
function startQr() {
document.getElementById('qr-error-area').innerHTML = '';
const code = inputCode.value.trim();
if (!code) {
inputCode.focus();
inputCode.classList.add('is-invalid');
return;
}
inputCode.classList.remove('is-invalid');
btnStart.disabled = true;
btnStart.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Generating…';
fetch(QR_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_code: code }),
})
.then(r => r.json())
.then(function(data) {
if (!data.ok) { showError(data.error); btnStart.disabled = false; btnStart.innerHTML = '<i class="bi bi-qr-code me-1"></i>Generate QR Code'; return; }
userCode = data.user_code;
qrToken = data.qr_token;
// Render QR code into canvas div
qrCanvas.innerHTML = '';
QRCode.toCanvas(document.createElement('canvas'), data.qr_data, { width: 220 }, function(err, canvas) {
if (err) { showError('QR render failed: ' + err); return; }
qrCanvas.appendChild(canvas);
});
qrArea.classList.remove('d-none');
btnStart.classList.add('d-none');
inputCode.disabled = true;
pollMsg.textContent = 'Waiting for scan…';
spinner.classList.remove('d-none');
// Start polling every 3 s
pollTimer = setInterval(pollLogin, 3000);
})
.catch(function(err) { showError(err); btnStart.disabled = false; btnStart.innerHTML = '<i class="bi bi-qr-code me-1"></i>Generate QR Code'; });
}
function pollLogin() {
const url = POLL_URL + '?token=' + encodeURIComponent(qrToken) + '&user_code=' + encodeURIComponent(userCode);
fetch(url)
.then(r => r.json())
.then(function(data) {
if (data.ok) {
clearInterval(pollTimer);
pollMsg.textContent = '✓ Scanned! Saving…';
spinner.classList.add('d-none');
saveTokens(data.info);
} else if (data.error) {
clearInterval(pollTimer);
pollMsg.textContent = 'Error: ' + data.error;
spinner.classList.add('d-none');
}
// else data.pending: keep waiting
})
.catch(function() { /* network hiccup keep polling */ });
}
function saveTokens(info) {
fetch(SAVE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ info: info, user_code: userCode }),
})
.then(r => r.json())
.then(function(data) {
if (data.ok) {
pollMsg.textContent = '✓ Account linked successfully! Redirecting…';
setTimeout(function() { window.location.href = data.redirect || GW_URL; }, 1500);
} else {
showError(data.error || 'Save failed');
}
})
.catch(function(err) { showError(err); });
}
function cancelQr() {
clearInterval(pollTimer);
qrArea.classList.add('d-none');
btnStart.classList.remove('d-none');
btnStart.disabled = false;
btnStart.innerHTML = '<i class="bi bi-qr-code me-1"></i>Generate QR Code';
qrCanvas.innerHTML = '';
inputCode.disabled = false;
}
function showError(msg) {
const area = document.getElementById('qr-error-area');
area.innerHTML = '';
const el = document.createElement('div');
el.className = 'alert alert-danger py-2 mt-2';
el.textContent = msg;
area.appendChild(el);
}
})();
</script>
{% endblock %}