Spaces:
Sleeping
Sleeping
| /** | |
| * AttendAI Pro - Application Configuration | |
| * Production-ready configuration management | |
| */ | |
| const APP_CONFIG = { | |
| name: 'AttendAI Pro', | |
| version: '2.0.0', | |
| tagline: 'AI-Powered Anti-Proxy Attendance System', | |
| // Google OAuth - Replace with your actual Google OAuth Client ID | |
| // To get: console.cloud.google.com → Create Project → OAuth 2.0 Client IDs | |
| googleClientId: '578078243530-ridjf8bv9ie0cbustjmefvf56b77qfe5.apps.googleusercontent.com', | |
| // API Base URL (relative paths for the Table API) | |
| apiBase: 'tables', | |
| // Attendance Settings | |
| qrRotateInterval: 60, // seconds - QR code rotates every 60s | |
| geofenceRadius: 100, // meters - default geofence radius | |
| faceMatchThreshold: 0.6, // 0-1 face match confidence threshold | |
| attendanceCutoff: 75, // percent - attendance warning threshold | |
| lateThresholdMinutes: 10, // minutes after session start = late | |
| // Security | |
| sessionTimeout: 8 * 60 * 60 * 1000, // 8 hours in ms | |
| maxAttemptsPerSession: 1, // max attendance attempts per session | |
| fraudDetectionEnabled: true, | |
| // Allowed institutional email domains (configure per institution) | |
| // Leave empty to allow all emails (admin manages via database) | |
| allowedStudentDomains: [], // e.g., ['university.edu', 'college.ac.in'] | |
| // Supported institution types | |
| institutionTypes: ['University', 'College', 'High School', 'Corporate', 'Training Center'], | |
| // Routes | |
| routes: { | |
| landing: 'index.html', | |
| studentLogin: 'student-login.html', | |
| teacherLogin: 'teacher-login.html', | |
| studentDash: 'student-dashboard.html', | |
| teacherDash: 'teacher-dashboard.html', | |
| adminPanel: 'admin-panel.html', | |
| } | |
| }; | |
| /** | |
| * Auth State Manager | |
| */ | |
| const AuthState = { | |
| KEY: 'attendai_session', | |
| save(userData) { | |
| const session = { | |
| ...userData, | |
| loginTime: Date.now(), | |
| expires: Date.now() + APP_CONFIG.sessionTimeout | |
| }; | |
| sessionStorage.setItem(this.KEY, JSON.stringify(session)); | |
| localStorage.setItem(this.KEY + '_persist', JSON.stringify(session)); | |
| }, | |
| get() { | |
| try { | |
| const s = sessionStorage.getItem(this.KEY) || localStorage.getItem(this.KEY + '_persist'); | |
| if (!s) return null; | |
| const data = JSON.parse(s); | |
| if (Date.now() > data.expires) { this.clear(); return null; } | |
| return data; | |
| } catch { return null; } | |
| }, | |
| clear() { | |
| sessionStorage.removeItem(this.KEY); | |
| localStorage.removeItem(this.KEY + '_persist'); | |
| }, | |
| isLoggedIn() { return !!this.get(); }, | |
| getRole() { | |
| const s = this.get(); | |
| return s ? s.role : null; | |
| } | |
| }; | |
| /** | |
| * API Helper | |
| */ | |
| const API = { | |
| async get(table, params = {}) { | |
| const q = new URLSearchParams(params).toString(); | |
| const url = `${APP_CONFIG.apiBase}/${table}${q ? '?' + q : ''}`; | |
| const res = await fetch(url); | |
| if (!res.ok) throw new Error(`API Error: ${res.status}`); | |
| return res.json(); | |
| }, | |
| async getOne(table, id) { | |
| const res = await fetch(`${APP_CONFIG.apiBase}/${table}/${id}`); | |
| if (!res.ok) throw new Error(`API Error: ${res.status}`); | |
| return res.json(); | |
| }, | |
| async post(table, data) { | |
| const res = await fetch(`${APP_CONFIG.apiBase}/${table}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(data) | |
| }); | |
| if (!res.ok) throw new Error(`API Error: ${res.status}`); | |
| return res.json(); | |
| }, | |
| async put(table, id, data) { | |
| const res = await fetch(`${APP_CONFIG.apiBase}/${table}/${id}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(data) | |
| }); | |
| if (!res.ok) throw new Error(`API Error: ${res.status}`); | |
| return res.json(); | |
| }, | |
| async patch(table, id, data) { | |
| const res = await fetch(`${APP_CONFIG.apiBase}/${table}/${id}`, { | |
| method: 'PATCH', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(data) | |
| }); | |
| if (!res.ok) throw new Error(`API Error: ${res.status}`); | |
| return res.json(); | |
| }, | |
| async delete(table, id) { | |
| const res = await fetch(`${APP_CONFIG.apiBase}/${table}/${id}`, { method: 'DELETE' }); | |
| if (!res.ok) throw new Error(`API Error: ${res.status}`); | |
| return true; | |
| } | |
| }; | |
| /** | |
| * Utility Functions | |
| */ | |
| const Utils = { | |
| formatDate(ts) { | |
| if (!ts) return '—'; | |
| return new Date(ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); | |
| }, | |
| formatTime(ts) { | |
| if (!ts) return '—'; | |
| return new Date(ts).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); | |
| }, | |
| formatDateTime(ts) { | |
| if (!ts) return '—'; | |
| return new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); | |
| }, | |
| timeAgo(ts) { | |
| const s = Math.floor((Date.now() - ts) / 1000); | |
| if (s < 60) return `${s}s ago`; | |
| if (s < 3600) return `${Math.floor(s / 60)}m ago`; | |
| if (s < 86400) return `${Math.floor(s / 3600)}h ago`; | |
| return `${Math.floor(s / 86400)}d ago`; | |
| }, | |
| generateToken(len = 32) { | |
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | |
| let token = ''; | |
| for (let i = 0; i < len; i++) token += chars[Math.floor(Math.random() * chars.length)]; | |
| return token; | |
| }, | |
| generateUUID() { | |
| return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { | |
| const r = Math.random() * 16 | 0; | |
| return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); | |
| }); | |
| }, | |
| getDistance(lat1, lon1, lat2, lon2) { | |
| const R = 6371000; | |
| const dLat = (lat2 - lat1) * Math.PI / 180; | |
| const dLon = (lon2 - lon1) * Math.PI / 180; | |
| const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2; | |
| return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | |
| }, | |
| async getGeoLocation() { | |
| return new Promise((resolve, reject) => { | |
| if (!navigator.geolocation) { reject(new Error('Geolocation not supported')); return; } | |
| navigator.geolocation.getCurrentPosition( | |
| pos => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude, accuracy: pos.coords.accuracy }), | |
| err => reject(new Error(err.message)), | |
| { enableHighAccuracy: true, timeout: 10000 } | |
| ); | |
| }); | |
| }, | |
| getDeviceFingerprint() { | |
| const data = `${navigator.userAgent}|${screen.width}x${screen.height}|${navigator.language}|${Intl.DateTimeFormat().resolvedOptions().timeZone}`; | |
| let hash = 0; | |
| for (let i = 0; i < data.length; i++) { hash = ((hash << 5) - hash) + data.charCodeAt(i); hash |= 0; } | |
| return Math.abs(hash).toString(16); | |
| }, | |
| debounce(fn, ms) { | |
| let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; | |
| }, | |
| percentColor(pct) { | |
| if (pct >= 75) return 'success'; | |
| if (pct >= 60) return 'warning'; | |
| return 'danger'; | |
| } | |
| }; | |
| /** | |
| * Toast Notification System | |
| */ | |
| const Toast = { | |
| container: null, | |
| init() { | |
| if (!this.container) { | |
| this.container = document.createElement('div'); | |
| this.container.className = 'toast-container'; | |
| document.body.appendChild(this.container); | |
| } | |
| }, | |
| show(message, type = 'info', duration = 4000) { | |
| this.init(); | |
| const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.innerHTML = ` | |
| <span style="font-size:18px">${icons[type] || icons.info}</span> | |
| <div style="flex:1"><p style="color:#1e293b;margin:0;font-size:14px">${message}</p></div> | |
| <button class="toast-close" onclick="this.parentElement.remove()">×</button> | |
| `; | |
| this.container.appendChild(toast); | |
| if (duration > 0) setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; toast.style.transition = '0.3s'; setTimeout(() => toast.remove(), 300); }, duration); | |
| return toast; | |
| }, | |
| success(msg, d) { return this.show(msg, 'success', d); }, | |
| error(msg, d) { return this.show(msg, 'error', d); }, | |
| warning(msg, d) { return this.show(msg, 'warning', d); }, | |
| info(msg, d) { return this.show(msg, 'info', d); } | |
| }; | |
| /** | |
| * Guard: Redirect if not authenticated | |
| */ | |
| function requireAuth(role) { | |
| const user = AuthState.get(); | |
| if (!user) { | |
| window.location.href = 'index.html'; | |
| return null; | |
| } | |
| if (role && user.role !== role && user.role !== 'admin') { | |
| window.location.href = 'index.html'; | |
| return null; | |
| } | |
| return user; | |
| } | |
| /** | |
| * Logout handler | |
| */ | |
| function logout() { | |
| AuthState.clear(); | |
| // Revoke Google session if available | |
| if (typeof google !== 'undefined' && google.accounts) { | |
| google.accounts.id.disableAutoSelect(); | |
| } | |
| window.location.href = 'index.html'; | |
| } | |
| /** | |
| * System Logger | |
| */ | |
| const Logger = { | |
| async log(action, resource, details, status = 'success') { | |
| try { | |
| const user = AuthState.get(); | |
| await API.post('system_logs', { | |
| id: Utils.generateUUID(), | |
| user_id: user?.id || 'system', | |
| action, | |
| resource, | |
| details: JSON.stringify(details), | |
| ip_address: 'client', | |
| user_agent: navigator.userAgent.substring(0, 200), | |
| status, | |
| timestamp: Date.now() | |
| }); | |
| } catch (e) { console.warn('Log failed:', e); } | |
| } | |
| }; | |