Moved remote-link links to same location with reset links, avoiding vite routing issue. Realtime lookup of session when three words have been entered.

This commit is contained in:
Leo Vasanko
2025-12-07 00:15:56 +00:00
parent 269aefa9c8
commit dd49907c8d
7 changed files with 280 additions and 53 deletions
+147 -12
View File
@@ -12,21 +12,44 @@
v-model="code"
type="text"
:placeholder="placeholder"
:disabled="loading"
:disabled="loading || completed"
autocomplete="off"
autocapitalize="characters"
spellcheck="false"
class="pairing-input"
@input="handleInput"
/>
</div>
<!-- Looking up state -->
<div v-if="lookingUp" class="lookup-status">
<span class="lookup-spinner"></span>
<span>Looking up device</span>
</div>
<!-- Device info display (shown when 3 words match a request) -->
<div v-if="deviceInfo && !error && !completed" class="device-info">
<p class="device-info-label">📱 Device requesting login:</p>
<div class="device-info-details">
<div class="device-detail">
<span class="detail-label">Site:</span>
<span class="detail-value">{{ deviceInfo.host }}</span>
</div>
<div class="device-detail">
<span class="detail-label">Browser:</span>
<span class="detail-value">{{ deviceInfo.user_agent_pretty }}</span>
</div>
</div>
<button
type="submit"
:disabled="!isValid || loading"
:disabled="loading"
class="btn-primary"
style="margin-top: 0.75rem; width: 100%;"
>
{{ loading ? 'Connecting…' : 'Connect' }}
{{ loading ? 'Authenticating…' : 'Authenticate to Log In Device' }}
</button>
</div>
<p v-if="error" class="error-message">{{ error }}</p>
</form>
@@ -38,15 +61,16 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { startAuthentication } from '@simplewebauthn/browser'
import aWebSocket from '@/utils/awaitable-websocket'
import { getSettings } from '@/utils/settings'
import { apiJson } from '@/utils/api'
const props = defineProps({
title: { type: String, default: 'Help Another Device Sign In' },
description: { type: String, default: 'Enter the code shown on the device that needs to sign in.' },
placeholder: { type: String, default: 'Enter code' }
placeholder: { type: String, default: 'Enter three words' }
})
const emit = defineEmits(['completed', 'error', 'cancelled'])
@@ -54,25 +78,78 @@ const emit = defineEmits(['completed', 'error', 'cancelled'])
const inputRef = ref(null)
const code = ref('')
const loading = ref(false)
const lookingUp = ref(false)
const error = ref(null)
const completed = ref(false)
const completedMessage = ref('')
const deviceInfo = ref(null)
let ws = null
let lookupTimeout = null
// Valid if we have 3 words separated by dots or spaces
const isValid = computed(() => {
// Check if we have exactly 3 valid words
const hasThreeWords = computed(() => {
const trimmed = code.value.trim()
if (!trimmed) return false
const words = trimmed.split(/[.\s]+/).filter(w => w.length > 0)
return words.length >= 3
return words.length === 3
})
function handleInput() {
// Normalize code to dot-separated lowercase
function normalizeCode(input) {
return input.trim().toLowerCase().split(/[.\s]+/).filter(w => w).join('.')
}
// Watch for code changes and do real-time lookup
watch(code, () => {
// Clear previous timeout
if (lookupTimeout) {
clearTimeout(lookupTimeout)
lookupTimeout = null
}
// Reset device info and error when code changes
deviceInfo.value = null
error.value = null
// Only lookup when we have 3 words
if (hasThreeWords.value) {
// Debounce the lookup slightly to avoid too many requests
lookupTimeout = setTimeout(() => {
lookupDeviceInfo()
}, 150)
}
})
async function lookupDeviceInfo() {
if (!hasThreeWords.value || loading.value) return
lookingUp.value = true
error.value = null
try {
const normalizedCode = normalizeCode(code.value)
const info = await apiJson(`/auth/api/remote-auth-info?code=${encodeURIComponent(normalizedCode)}`)
deviceInfo.value = info
} catch (err) {
// 404 means no matching request found
if (err.status === 404) {
error.value = 'No device found with this code. Check the code and try again.'
} else {
console.error('Lookup error:', err)
error.value = err.message || 'Failed to look up device'
}
deviceInfo.value = null
} finally {
lookingUp.value = false
}
}
function handleInput() {
// Error is cleared by the watcher
}
async function submitCode() {
if (!isValid.value || loading.value) return
if (!deviceInfo.value || loading.value) return
loading.value = true
error.value = null
@@ -81,8 +158,7 @@ async function submitCode() {
const settings = await getSettings()
const authHost = settings?.auth_host
// Normalize the code: lowercase words joined by dots
const normalizedCode = code.value.trim().toLowerCase().split(/[.\s]+/).filter(w => w).join('.')
const normalizedCode = normalizeCode(code.value)
const wsPath = `/auth/ws/remote-auth/pair/${encodeURIComponent(normalizedCode)}`
const wsUrl = authHost && location.host !== authHost ? `//${authHost}${wsPath}` : wsPath
@@ -131,6 +207,8 @@ function reset() {
error.value = null
completed.value = false
completedMessage.value = ''
deviceInfo.value = null
lookingUp.value = false
}
onMounted(() => {
@@ -196,6 +274,63 @@ defineExpose({ reset })
opacity: 0.6;
}
.lookup-status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--color-text-muted);
}
.lookup-spinner {
width: 14px;
height: 14px;
border: 2px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.device-info {
padding: 0.875rem;
background: var(--color-surface-hover, rgba(0, 0, 0, 0.03));
border: 1px solid var(--color-border);
border-radius: var(--radius-sm, 4px);
}
.device-info-label {
margin: 0 0 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
}
.device-info-details {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.device-detail {
display: flex;
gap: 0.5rem;
font-size: 0.875rem;
}
.detail-label {
color: var(--color-text-muted);
min-width: 4rem;
}
.detail-value {
color: var(--color-text);
font-weight: 500;
}
.error-message {
margin: 0;
font-size: 0.875rem;