Files
Apollo/src_assets/common/assets/web/pin.html
2024-09-10 18:13:44 +08:00

226 lines
7.9 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-bs-theme="auto">
<head>
<%- header %>
<script type="text/javascript" src="/assets/js/qrcode.min.js"></script>
<style scoped type="text/css">
.content-container {
padding-top: 2em;
}
.pin-tab-bar {
margin-bottom: 2em !important;
}
</style>
</head>
<body id="app" v-cloak>
<Navbar></Navbar>
<div id="content" class="container content-container d-flex flex-column align-items-center">
<ul class="nav nav-pills pin-tab-bar justify-content-center">
<li class="nav-item">
<a class="nav-link" :class="{active: currentTab !== '#PIN'}" href="#OTP" @click.prevent="switchTab('OTP')">{{ $t('pin.otp_pairing') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{active: currentTab === '#PIN'}" href="#PIN" @click.prevent="switchTab('PIN')">{{ $t('pin.pin_pairing') }}</a>
</li>
</ul>
<form v-if="currentTab === '#PIN'" class="form d-flex flex-column align-items-center" id="form" @submit.prevent="registerDevice">
<div class="card flex-column d-flex p-4 mb-4">
<input type="text" pattern="\d*" :placeholder="`${$t('navbar.pin')}`" autofocus id="pin-input" class="form-control mt-2" required />
<input type="text" :placeholder="`${$t('pin.device_name')}`" id="name-input" class="form-control my-4" />
<button class="btn btn-primary">{{ $t('pin.send') }}</button>
</div>
<div id="status"></div>
</form>
<form v-else class="form d-flex flex-column align-items-center" @submit.prevent="requestOTP">
<div class="card flex-column d-flex p-4 mb-4">
<div v-show="editingHost || (otp && hostAddr)" id="qrRef"></div>
<p v-if="editingHost || (otp && hostAddr)" class="text-center text-secondary"><a class="text-secondary" :href="deepLink">art://{{ hostAddr }}:{{ hostPort }}</a> <i class="fas fa-fw fa-pen-to-square" @click="editHost"></i></p>
<h1 class="mb-4 text-center">{{ otp && otp || '????' }}</h1>
<div v-if="editingHost" class="d-flex flex-column align-items-stretch">
<input type="text" placeholder="HOST" v-model="hostAddr" autofocus class="form-control mt-2" />
<input type="text" placeholder="PORT" v-model="hostPort" class="form-control my-4" />
<button class="btn btn-primary" :disabled="!this.canSaveHost" @click.prevent="saveHost">{{ $t('_common.save') }}</button>
</div>
<div v-else class="d-flex flex-column align-items-stretch">
<input type="text" pattern="[0-9a-zA-Z]{4,}" :placeholder="`${$t('pin.otp_passphrase')}`" v-model="passphrase" required autofocus class="form-control mt-2" />
<input type="text" :placeholder="`${$t('pin.device_name')}`" v-model="deviceName" class="form-control my-4" />
<button class="btn btn-primary">{{ $t('pin.generate_pin') }}</button>
</div>
</div>
<div v-if="otpMessage" class="alert" :class="['alert-' + otpStatus]">{{ otpMessage }}</div>
<div class="alert alert-info">{{ $t('pin.otp_msg') }}</div>
</form>
<div class="alert alert-warning">
<b>{{ $t('_common.warning') }}</b> {{ $t('pin.warning_msg') }}
</div>
</div>
</body>
<script type="module">
import { createApp } from 'vue'
import { initApp } from './init'
import Navbar from './Navbar.vue'
let resetOTPTimeout = null;
const qrContainer = document.createElement('div');
qrContainer.className = "mb-2 p-2 bg-white"
const qrCode = new QRCode(qrContainer);
const updateQR = (url) => {
qrCode.clear()
qrCode.makeCode(url)
const refContainer = document.querySelector('#qrRef');
if (refContainer) refContainer.appendChild(qrContainer);
}
let hostInfoCache = JSON.parse(sessionStorage.getItem('hostInfo'));
let hostManuallySet = false;
if (hostInfoCache) hostManuallySet = true;
const saveHostCache = ({hostAddr, hostPort}, manual) => {
hostInfoCache = {hostAddr, hostPort}
if (manual) {
sessionStorage.setItem('hostInfo', JSON.stringify(hostInfoCache))
hostManuallySet = true;
}
}
const data = () => {
return {
editingHost: false,
currentTab: location.hash || '#OTP',
otp: '',
passphrase: '',
otpMessage: '',
otpStatus: 'warning',
deviceName: '',
hostAddr: '',
hostPort: '',
hostName: ''
}
}
let app = createApp({
components: {
Navbar
},
inject: ['i18n'],
data,
computed: {
deepLink() {
return encodeURI(`art://${this.hostAddr}:${this.hostPort}?pin=${this.otp}&passphrase=${this.passphrase}&name=${this.hostName}`);
},
canSaveHost() {
return !!(this.hostAddr && this.hostPort);
}
},
methods: {
switchTab(currentTab) {
location.hash = currentTab;
Object.assign(this, data());
hostInfoCache = null;
clearTimeout(resetOTPTimeout);
},
editHost() {
this.editingHost = !this.editingHost;
Object.assign(this, hostInfoCache);
},
saveHost() {
if (!this.canSaveHost) return;
updateQR(this.deepLink);
this.editingHost = false;
saveHostCache(this, true);
},
registerDevice(e) {
let pin = document.querySelector("#pin-input").value;
let name = document.querySelector("#name-input").value;
document.querySelector("#status").innerHTML = "";
let b = JSON.stringify({pin: pin, name: name});
fetch("/api/pin", {
credentials: 'include',
method: "POST",
body: b
})
.then((response) => response.json())
.then((response) => {
if (response.status.toString().toLowerCase() === "true") {
document.querySelector(
"#status"
).innerHTML = `<div class="alert alert-success" role="alert">${this.i18n.t('pin.pair_success')}</div>`;
document.querySelector("#pin-input").value = "";
document.querySelector("#name-input").value = "";
} else {
document.querySelector(
"#status"
).innerHTML = `<div class="alert alert-danger" role="alert">${this.i18n.t('pin.pair_failure')}</div>`;
}
});
},
requestOTP() {
if (this.editingHost) return;
fetch(`/api/otp?passphrase=${this.passphrase}${this.deviceName && `&deviceName=${this.deviceName}` || ''}`, {
credentials: 'include'
})
.then(resp => resp.json())
.then(resp => {
if (resp.status !== 'true') {
this.otpMessage = resp.message
this.otpStatus = 'danger'
return
}
this.otp = resp.otp
this.hostName = resp.name
this.otpStatus = 'success'
this.otpMessage = this.i18n.t('pin.otp_success')
const isLocalHost = ['localhost', '127.0.0.1', '[::1]'].indexOf(location.hostname) < 0
if (hostManuallySet) {
Object.assign(this, hostInfoCache);
} else {
this.hostAddr = resp.ip
this.hostPort = parseInt(location.port, 10) - 1
if (isLocalHost) {
this.hostAddr = location.hostname
}
saveHostCache(this);
}
if (this.hostAddr) {
updateQR(this.deepLink);
if (resetOTPTimeout !== null) clearTimeout(resetOTPTimeout)
resetOTPTimeout = setTimeout(() => {
Object.assign(this, data(), {
otp: this.i18n.t('pin.otp_expired'),
otpMessage: this.i18n.t('pin.otp_expired_msg')
})
resetOTPTimeout = null
}, 3 * 60 * 1000)
if (isLocalHost) {
setTimeout(() => {
if (window.confirm(this.i18n.t('pin.otp_pair_now'))) {
window.open(this.deepLink);
}
}, 0)
}
}
})
}
}
});
initApp(app);
</script>