Initial project scaffold
Full-stack homeschool web app with FastAPI backend, Vue 3 frontend, MySQL database, and Docker Compose orchestration. Includes JWT auth, WebSocket real-time TV dashboard, schedule builder, activity logging, and multi-child support. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
93e0494864
commit
417b3adfe8
68 files changed
+3919
No files matched your search
@@ -0,0 +1,55 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/',
|
||||
withCredentials: true, // send cookies (refresh token)
|
||||
})
|
||||
|
||||
// Attach access token to every request
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// Auto-refresh on 401
|
||||
let refreshing = false
|
||||
let waitQueue = []
|
||||
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
async (error) => {
|
||||
const original = error.config
|
||||
if (error.response?.status === 401 && !original._retry) {
|
||||
if (refreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
waitQueue.push({ resolve, reject })
|
||||
}).then(() => api(original))
|
||||
}
|
||||
|
||||
original._retry = true
|
||||
refreshing = true
|
||||
try {
|
||||
const res = await axios.post('/api/auth/refresh', {}, { withCredentials: true })
|
||||
const token = res.data.access_token
|
||||
localStorage.setItem('access_token', token)
|
||||
waitQueue.forEach(({ resolve }) => resolve())
|
||||
waitQueue = []
|
||||
original.headers.Authorization = `Bearer ${token}`
|
||||
return api(original)
|
||||
} catch (_) {
|
||||
waitQueue.forEach(({ reject }) => reject())
|
||||
waitQueue = []
|
||||
localStorage.removeItem('access_token')
|
||||
window.location.href = '/login'
|
||||
} finally {
|
||||
refreshing = false
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
|
||||
export function useWebSocket(childId, onMessage) {
|
||||
const connected = ref(false)
|
||||
let ws = null
|
||||
let reconnectTimer = null
|
||||
|
||||
function connect() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const url = `${protocol}://${window.location.host}/ws/${childId}`
|
||||
ws = new WebSocket(url)
|
||||
|
||||
ws.onopen = () => {
|
||||
connected.value = true
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
onMessage(data)
|
||||
} catch (e) {
|
||||
console.warn('WS parse error', e)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
connected.value = false
|
||||
// Reconnect after 3 seconds
|
||||
reconnectTimer = setTimeout(connect, 3000)
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer)
|
||||
if (ws) ws.close()
|
||||
}
|
||||
|
||||
connect()
|
||||
onUnmounted(disconnect)
|
||||
|
||||
return { connected, disconnect }
|
||||
}
|
||||
Reference in new issue
Block a user