redesign fully scaffolded and web login works
This commit is contained in:
9
web/.env.example
Normal file
9
web/.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# Gateway API URL (use relative path for ingress routing)
|
||||
# In dev with minikube: http://dexorder.local/api
|
||||
# For local development: http://localhost:3000
|
||||
VITE_GATEWAY_URL=/api
|
||||
|
||||
# WebSocket URL (for real-time updates, if needed)
|
||||
# In dev with minikube: ws://dexorder.local/api/ws/chat
|
||||
# For local development: ws://localhost:8080/ws/chat
|
||||
VITE_WS_URL=ws://dexorder.local/api/ws/chat
|
||||
@@ -11,9 +11,9 @@ import { useShapeStore } from './stores/shapes'
|
||||
import { useIndicatorStore } from './stores/indicators'
|
||||
import { useStateSync } from './composables/useStateSync'
|
||||
import { wsManager } from './composables/useWebSocket'
|
||||
import { authService } from './composables/useAuth'
|
||||
|
||||
const isAuthenticated = ref(false)
|
||||
const needsConfirmation = ref(false)
|
||||
const isAuthenticated = authService.isAuthenticated
|
||||
const authError = ref<string>()
|
||||
const isDragging = ref(false)
|
||||
const isMobile = ref(false)
|
||||
@@ -37,57 +37,66 @@ watch(isMobile, (mobile) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Check if we need password confirmation on first load
|
||||
// Check if user is already authenticated on page load
|
||||
onMounted(async () => {
|
||||
// Check if secrets store is initialized by trying to fetch a status endpoint
|
||||
// For now, just default to false (user will see login screen)
|
||||
needsConfirmation.value = false
|
||||
// Try to restore session from stored token
|
||||
if (authService.getToken()) {
|
||||
const sessionValid = await authService.checkAuth()
|
||||
if (sessionValid) {
|
||||
isAuthenticated.value = true
|
||||
await initializeApp()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize mobile check
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
const handleAuthenticate = async (
|
||||
password: string,
|
||||
confirmPassword?: string,
|
||||
newPassword?: string,
|
||||
confirmNewPassword?: string
|
||||
) => {
|
||||
const handleAuthenticate = async (email: string, password: string) => {
|
||||
authError.value = undefined
|
||||
|
||||
try {
|
||||
const result = await wsManager.connect(password, confirmPassword, newPassword, confirmNewPassword)
|
||||
// Step 1: Login via HTTP to get JWT token
|
||||
const result = await authService.login(email, password)
|
||||
|
||||
if (result.success) {
|
||||
isAuthenticated.value = true
|
||||
|
||||
// Initialize state sync after successful authentication
|
||||
const orderStore = useOrderStore()
|
||||
const chartStore = useChartStore()
|
||||
const shapeStore = useShapeStore()
|
||||
const indicatorStore = useIndicatorStore()
|
||||
const stateSync = useStateSync({
|
||||
OrderStore: orderStore,
|
||||
ChartStore: chartStore,
|
||||
ShapeStore: shapeStore,
|
||||
IndicatorStore: indicatorStore
|
||||
})
|
||||
stateSyncCleanup = stateSync.cleanup
|
||||
} else {
|
||||
authError.value = result.message
|
||||
|
||||
// If server says we need confirmation, update the flag
|
||||
if (result.needsConfirmation) {
|
||||
needsConfirmation.value = true
|
||||
}
|
||||
if (!result.success) {
|
||||
authError.value = result.error || 'Login failed'
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
authError.value = 'Connection failed'
|
||||
|
||||
if (!result.token) {
|
||||
authError.value = 'No token received from server'
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2: Connect WebSocket with JWT token (if WebSocket is used for real-time sync)
|
||||
// For now, we're not connecting WebSocket until it's implemented in the gateway
|
||||
// await wsManager.connect(result.token)
|
||||
|
||||
// Step 3: Initialize application
|
||||
await initializeApp()
|
||||
} catch (err: any) {
|
||||
authError.value = err.message || 'Authentication failed'
|
||||
console.error('Authentication error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const initializeApp = async () => {
|
||||
// Initialize state sync after successful authentication
|
||||
const orderStore = useOrderStore()
|
||||
const chartStore = useChartStore()
|
||||
const shapeStore = useShapeStore()
|
||||
const indicatorStore = useIndicatorStore()
|
||||
const stateSync = useStateSync({
|
||||
OrderStore: orderStore,
|
||||
ChartStore: chartStore,
|
||||
ShapeStore: shapeStore,
|
||||
IndicatorStore: indicatorStore
|
||||
})
|
||||
stateSyncCleanup = stateSync.cleanup
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Listen for splitter drag events
|
||||
document.addEventListener('mousedown', (e) => {
|
||||
@@ -116,7 +125,6 @@ onBeforeUnmount(() => {
|
||||
<div class="app-container dark">
|
||||
<LoginScreen
|
||||
v-if="!isAuthenticated"
|
||||
:needs-confirmation="needsConfirmation"
|
||||
:error-message="authError"
|
||||
@authenticate="handleAuthenticate"
|
||||
/>
|
||||
|
||||
@@ -7,30 +7,23 @@ import Button from 'primevue/button'
|
||||
import Message from 'primevue/message'
|
||||
|
||||
const props = defineProps<{
|
||||
needsConfirmation: boolean
|
||||
errorMessage?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
authenticate: [password: string, confirmPassword?: string, newPassword?: string, confirmNewPassword?: string]
|
||||
authenticate: [email: string, password: string]
|
||||
}>()
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmNewPassword = ref('')
|
||||
const isLoading = ref(false)
|
||||
const isChangingPassword = ref(false)
|
||||
const passwordInput = ref<InstanceType<typeof Password> | null>(null)
|
||||
const emailInput = ref<InstanceType<typeof InputText> | null>(null)
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
if (!password.value || isLoading.value) return false
|
||||
if (props.needsConfirmation && password.value !== confirmPassword.value) return false
|
||||
if (isChangingPassword.value) {
|
||||
if (!newPassword.value || !confirmNewPassword.value) return false
|
||||
if (newPassword.value !== confirmNewPassword.value) return false
|
||||
}
|
||||
return true
|
||||
if (!email.value || !password.value || isLoading.value) return false
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email.value)
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
@@ -41,13 +34,7 @@ const handleSubmit = () => {
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
if (props.needsConfirmation) {
|
||||
emit('authenticate', password.value, confirmPassword.value)
|
||||
} else if (isChangingPassword.value) {
|
||||
emit('authenticate', password.value, undefined, newPassword.value, confirmNewPassword.value)
|
||||
} else {
|
||||
emit('authenticate', password.value)
|
||||
}
|
||||
emit('authenticate', email.value, password.value)
|
||||
|
||||
// Reset loading state after a timeout (parent will handle actual auth)
|
||||
setTimeout(() => {
|
||||
@@ -62,19 +49,10 @@ const handleKeypress = (event: KeyboardEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
const togglePasswordChange = () => {
|
||||
isChangingPassword.value = !isChangingPassword.value
|
||||
newPassword.value = ''
|
||||
confirmNewPassword.value = ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Focus on the password input when component mounts
|
||||
if (passwordInput.value?.$el) {
|
||||
const inputElement = passwordInput.value.$el.querySelector('input') as HTMLInputElement
|
||||
if (inputElement) {
|
||||
inputElement.focus()
|
||||
}
|
||||
// Focus on the email input when component mounts
|
||||
if (emailInput.value?.$el) {
|
||||
emailInput.value.$el.focus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -86,19 +64,13 @@ onMounted(() => {
|
||||
<template #title>
|
||||
<div class="login-title">
|
||||
<i class="pi pi-lock" style="font-size: 2rem; margin-bottom: 0.5rem;"></i>
|
||||
<h2>{{ needsConfirmation ? 'Welcome' : 'Authentication' }}</h2>
|
||||
<h2>Sign In</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="login-content">
|
||||
<p v-if="needsConfirmation" class="welcome-message">
|
||||
This is your first time connecting. Please create a password to secure your workspace.
|
||||
</p>
|
||||
<p v-else-if="isChangingPassword" class="welcome-message">
|
||||
Enter your current password and choose a new one.
|
||||
</p>
|
||||
<p v-else class="welcome-message">
|
||||
Enter your password to connect.
|
||||
<p class="welcome-message">
|
||||
Enter your credentials to access the platform.
|
||||
</p>
|
||||
|
||||
<Message v-if="errorMessage" severity="error" :closable="false">
|
||||
@@ -106,14 +78,28 @@ onMounted(() => {
|
||||
</Message>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="password">{{ isChangingPassword ? 'Current Password' : 'Password' }}</label>
|
||||
<label for="email">Email</label>
|
||||
<InputText
|
||||
ref="emailInput"
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
class="email-input"
|
||||
@keypress="handleKeypress"
|
||||
:disabled="isLoading"
|
||||
autocomplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="password">Password</label>
|
||||
<Password
|
||||
ref="passwordInput"
|
||||
id="password"
|
||||
v-model="password"
|
||||
:feedback="needsConfirmation"
|
||||
:feedback="false"
|
||||
toggleMask
|
||||
placeholder="Enter password"
|
||||
placeholder="Enter your password"
|
||||
class="password-input"
|
||||
@keypress="handleKeypress"
|
||||
:disabled="isLoading"
|
||||
@@ -121,75 +107,14 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="needsConfirmation" class="form-field">
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<Password
|
||||
id="confirm-password"
|
||||
v-model="confirmPassword"
|
||||
:feedback="false"
|
||||
toggleMask
|
||||
placeholder="Confirm password"
|
||||
class="password-input"
|
||||
@keypress="handleKeypress"
|
||||
:disabled="isLoading"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<small v-if="confirmPassword && password !== confirmPassword" class="p-error">
|
||||
Passwords do not match
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Password change fields -->
|
||||
<div v-if="isChangingPassword" class="password-change-section">
|
||||
<div class="form-field">
|
||||
<label for="new-password">New Password</label>
|
||||
<Password
|
||||
id="new-password"
|
||||
v-model="newPassword"
|
||||
:feedback="true"
|
||||
toggleMask
|
||||
placeholder="Enter new password"
|
||||
class="password-input"
|
||||
@keypress="handleKeypress"
|
||||
:disabled="isLoading"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="confirm-new-password">Confirm New Password</label>
|
||||
<Password
|
||||
id="confirm-new-password"
|
||||
v-model="confirmNewPassword"
|
||||
:feedback="false"
|
||||
toggleMask
|
||||
placeholder="Confirm new password"
|
||||
class="password-input"
|
||||
@keypress="handleKeypress"
|
||||
:disabled="isLoading"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<small v-if="confirmNewPassword && newPassword !== confirmNewPassword" class="p-error">
|
||||
Passwords do not match
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
:label="needsConfirmation ? 'Create & Connect' : isChangingPassword ? 'Change Password & Connect' : 'Connect'"
|
||||
:icon="isChangingPassword ? 'pi pi-key' : 'pi pi-sign-in'"
|
||||
label="Sign In"
|
||||
icon="pi pi-sign-in"
|
||||
@click="handleSubmit"
|
||||
:disabled="!canSubmit"
|
||||
:loading="isLoading"
|
||||
class="connect-button"
|
||||
/>
|
||||
|
||||
<!-- Toggle password change link -->
|
||||
<div v-if="!needsConfirmation" class="change-password-link">
|
||||
<a @click="togglePasswordChange" href="#">
|
||||
{{ isChangingPassword ? 'Cancel password change' : 'Change password' }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
@@ -255,6 +180,7 @@ onMounted(() => {
|
||||
color: var(--p-text-color);
|
||||
}
|
||||
|
||||
.email-input,
|
||||
.password-input {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -269,36 +195,4 @@ onMounted(() => {
|
||||
font-size: 1.1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.p-error {
|
||||
color: var(--p-red-500);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.password-change-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--p-surface-border);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.change-password-link {
|
||||
text-align: center;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.change-password-link a {
|
||||
color: var(--p-primary-color);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.change-password-link a:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
158
web/src/composables/useAuth.ts
Normal file
158
web/src/composables/useAuth.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean
|
||||
userId?: string
|
||||
token?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
user: {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
}
|
||||
session: {
|
||||
token: string
|
||||
expiresAt: string
|
||||
}
|
||||
}
|
||||
|
||||
const TOKEN_STORAGE_KEY = 'dexorder_auth_token'
|
||||
|
||||
class AuthService {
|
||||
public isAuthenticated = ref(false)
|
||||
private token: string | null = null
|
||||
private gatewayUrl: string
|
||||
|
||||
constructor() {
|
||||
// Use environment variable for gateway URL, fallback to localhost for dev
|
||||
this.gatewayUrl = import.meta.env.VITE_GATEWAY_URL || 'http://localhost:3000'
|
||||
|
||||
// Try to restore token from localStorage
|
||||
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY)
|
||||
if (storedToken) {
|
||||
this.token = storedToken
|
||||
this.isAuthenticated.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with email and password
|
||||
*/
|
||||
async login(email: string, password: string): Promise<LoginResponse> {
|
||||
try {
|
||||
const response = await fetch(`${this.gatewayUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.error || 'Login failed',
|
||||
}
|
||||
}
|
||||
|
||||
if (data.success && data.token) {
|
||||
this.token = data.token
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, data.token)
|
||||
this.isAuthenticated.value = true
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: data.userId,
|
||||
token: data.token,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid response from server',
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Login error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Network error',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout and clear session
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
if (this.token) {
|
||||
await fetch(`${this.gatewayUrl}/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
} finally {
|
||||
this.token = null
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||
this.isAuthenticated.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session information
|
||||
*/
|
||||
async getSession(): Promise<SessionData | null> {
|
||||
if (!this.token) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.gatewayUrl}/auth/session`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// Token is invalid or expired
|
||||
this.token = null
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||
this.isAuthenticated.value = false
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Get session error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current auth token
|
||||
*/
|
||||
getToken(): string | null {
|
||||
return this.token
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated by verifying session
|
||||
*/
|
||||
async checkAuth(): Promise<boolean> {
|
||||
const session = await this.getSession()
|
||||
return session !== null
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService()
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ref } from 'vue'
|
||||
import type { AuthMessage, AuthResponseMessage } from '../types/sync'
|
||||
|
||||
export interface WebSocketMessage {
|
||||
type: string
|
||||
@@ -8,36 +7,21 @@ export interface WebSocketMessage {
|
||||
|
||||
export type MessageHandler = (message: WebSocketMessage) => void
|
||||
|
||||
export interface AuthResult {
|
||||
success: boolean
|
||||
needsConfirmation: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
class WebSocketManager {
|
||||
private ws: WebSocket | null = null
|
||||
private handlers: Set<MessageHandler> = new Set()
|
||||
private reconnectTimeout: number | null = null
|
||||
public isConnected = ref(false)
|
||||
public isAuthenticated = ref(false)
|
||||
private authResolve: ((result: AuthResult) => void) | null = null
|
||||
private shouldReconnect = false // Disable auto-reconnect (user must re-authenticate)
|
||||
private storedPassword: string | null = null
|
||||
private storedConfirmPassword: string | undefined = undefined
|
||||
private token: string | null = null
|
||||
|
||||
async connect(
|
||||
password: string,
|
||||
confirmPassword?: string,
|
||||
changeToPassword?: string,
|
||||
confirmNewPassword?: string
|
||||
): Promise<AuthResult> {
|
||||
// Store password for potential reconnection
|
||||
this.storedPassword = password
|
||||
this.storedConfirmPassword = confirmPassword
|
||||
/**
|
||||
* Connect to WebSocket with JWT token for authentication
|
||||
*/
|
||||
async connect(token: string): Promise<void> {
|
||||
this.token = token
|
||||
|
||||
// Close existing connection if any
|
||||
if (this.ws) {
|
||||
this.shouldReconnect = false
|
||||
this.ws.close()
|
||||
this.ws = null
|
||||
}
|
||||
@@ -45,55 +29,21 @@ class WebSocketManager {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Use env variable for WebSocket URL, fallback to localhost for dev
|
||||
const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8080/ws'
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
this.authResolve = resolve
|
||||
// Append token as query parameter
|
||||
const wsUrlWithToken = `${wsUrl}?token=${encodeURIComponent(token)}`
|
||||
this.ws = new WebSocket(wsUrlWithToken)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connected, sending auth...')
|
||||
|
||||
// Send authentication message
|
||||
const authMsg: AuthMessage = {
|
||||
type: 'auth',
|
||||
password,
|
||||
confirm_password: confirmPassword,
|
||||
change_to_password: changeToPassword,
|
||||
confirm_new_password: confirmNewPassword
|
||||
}
|
||||
this.ws?.send(JSON.stringify(authMsg))
|
||||
console.log('WebSocket connected')
|
||||
this.isConnected.value = true
|
||||
resolve()
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data)
|
||||
|
||||
// Handle auth response
|
||||
if (message.type === 'auth_response') {
|
||||
const authResponse = message as AuthResponseMessage
|
||||
console.log('Auth response:', authResponse)
|
||||
|
||||
if (authResponse.success) {
|
||||
this.isConnected.value = true
|
||||
this.isAuthenticated.value = true
|
||||
this.shouldReconnect = true
|
||||
console.log('Authentication successful')
|
||||
} else {
|
||||
this.isAuthenticated.value = false
|
||||
console.warn('Authentication failed:', authResponse.message)
|
||||
}
|
||||
|
||||
if (this.authResolve) {
|
||||
this.authResolve({
|
||||
success: authResponse.success,
|
||||
needsConfirmation: authResponse.needs_confirmation || false,
|
||||
message: authResponse.message
|
||||
})
|
||||
this.authResolve = null
|
||||
}
|
||||
|
||||
// Don't return, let other handlers process this message too
|
||||
}
|
||||
|
||||
// Pass to all handlers
|
||||
this.handlers.forEach(handler => handler(message))
|
||||
} catch (err) {
|
||||
@@ -104,48 +54,21 @@ class WebSocketManager {
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
this.isConnected.value = false
|
||||
this.isAuthenticated.value = false
|
||||
|
||||
if (this.authResolve) {
|
||||
this.authResolve({
|
||||
success: false,
|
||||
needsConfirmation: false,
|
||||
message: 'Connection error'
|
||||
})
|
||||
this.authResolve = null
|
||||
}
|
||||
reject(error)
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
this.isConnected.value = false
|
||||
this.isAuthenticated.value = false
|
||||
console.log('WebSocket disconnected:', event.code, event.reason)
|
||||
|
||||
if (this.authResolve) {
|
||||
this.authResolve({
|
||||
success: false,
|
||||
needsConfirmation: false,
|
||||
message: event.reason || 'Connection closed'
|
||||
})
|
||||
this.authResolve = null
|
||||
}
|
||||
|
||||
// No auto-reconnect - user must re-authenticate manually
|
||||
// This prevents reconnection attempts without proper password
|
||||
}
|
||||
|
||||
// Timeout for auth
|
||||
// Connection timeout
|
||||
setTimeout(() => {
|
||||
if (this.authResolve) {
|
||||
this.authResolve({
|
||||
success: false,
|
||||
needsConfirmation: false,
|
||||
message: 'Authentication timeout'
|
||||
})
|
||||
this.authResolve = null
|
||||
if (!this.isConnected.value) {
|
||||
reject(new Error('WebSocket connection timeout'))
|
||||
this.ws?.close()
|
||||
}
|
||||
}, 30000)
|
||||
}, 10000)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
export default defineConfig({
|
||||
base: process.env.VITE_BASE_PATH || '/',
|
||||
server: {
|
||||
allowedHosts: ['.dexorder.ai']
|
||||
allowedHosts: ['.dexorder.ai', 'dexorder.local']
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
|
||||
Reference in New Issue
Block a user