Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- database.sqlite +0 -0
- server.py +35 -1
- student-login.html +69 -0
- teacher-login.html +75 -0
database.sqlite
CHANGED
|
Binary files a/database.sqlite and b/database.sqlite differ
|
|
|
server.py
CHANGED
|
@@ -30,8 +30,14 @@ def init_db():
|
|
| 30 |
c.execute('''CREATE TABLE IF NOT EXISTS users (
|
| 31 |
id text PRIMARY KEY, email text, name text, role text,
|
| 32 |
roll_number text, department text, institution text,
|
| 33 |
-
is_active boolean, face_data text, last_login datetime
|
|
|
|
| 34 |
)''')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
c.execute('''CREATE TABLE IF NOT EXISTS sessions (
|
| 36 |
id text PRIMARY KEY, teacher_id text, subject text,
|
| 37 |
qr_code text, qr_expires_at datetime, geofence_lat real,
|
|
@@ -206,6 +212,34 @@ async def handle_item(table_name: str, item_id: str, request: Request):
|
|
| 206 |
raise HTTPException(status_code=404, detail="Item not found")
|
| 207 |
return {"success": True}
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
# Mount static files correctly
|
| 210 |
# This should happen LAST so API routes take precedence
|
| 211 |
app.mount("/", StaticFiles(directory=".", html=True), name="static")
|
|
|
|
| 30 |
c.execute('''CREATE TABLE IF NOT EXISTS users (
|
| 31 |
id text PRIMARY KEY, email text, name text, role text,
|
| 32 |
roll_number text, department text, institution text,
|
| 33 |
+
is_active boolean, face_data text, last_login datetime,
|
| 34 |
+
password text
|
| 35 |
)''')
|
| 36 |
+
# Use try-except to add password column if it doesn't exist (for existing databases)
|
| 37 |
+
try:
|
| 38 |
+
c.execute("ALTER TABLE users ADD COLUMN password text")
|
| 39 |
+
except:
|
| 40 |
+
pass
|
| 41 |
c.execute('''CREATE TABLE IF NOT EXISTS sessions (
|
| 42 |
id text PRIMARY KEY, teacher_id text, subject text,
|
| 43 |
qr_code text, qr_expires_at datetime, geofence_lat real,
|
|
|
|
| 212 |
raise HTTPException(status_code=404, detail="Item not found")
|
| 213 |
return {"success": True}
|
| 214 |
|
| 215 |
+
@app.post("/login")
|
| 216 |
+
async def login(request: Request):
|
| 217 |
+
data = await request.json()
|
| 218 |
+
email = data.get("email", "").lower()
|
| 219 |
+
password = data.get("password", "")
|
| 220 |
+
|
| 221 |
+
conn = get_db()
|
| 222 |
+
conn.row_factory = dict_factory
|
| 223 |
+
c = conn.cursor()
|
| 224 |
+
|
| 225 |
+
# Query user by email
|
| 226 |
+
c.execute("SELECT * FROM users WHERE LOWER(email) = ?", (email,))
|
| 227 |
+
user = c.fetchone()
|
| 228 |
+
|
| 229 |
+
if not user:
|
| 230 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 231 |
+
|
| 232 |
+
if user.get("password") != password:
|
| 233 |
+
raise HTTPException(status_code=401, detail="Invalid password")
|
| 234 |
+
|
| 235 |
+
if not user.get("is_active"):
|
| 236 |
+
raise HTTPException(status_code=403, detail="Account deactivated")
|
| 237 |
+
|
| 238 |
+
# Exclude password from response
|
| 239 |
+
user_data = dict(user)
|
| 240 |
+
user_data.pop("password", None)
|
| 241 |
+
return user_data
|
| 242 |
+
|
| 243 |
# Mount static files correctly
|
| 244 |
# This should happen LAST so API routes take precedence
|
| 245 |
app.mount("/", StaticFiles(directory=".", html=True), name="static")
|
student-login.html
CHANGED
|
@@ -77,6 +77,25 @@
|
|
| 77 |
<div id="googleBtnContainer"></div>
|
| 78 |
</div>
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
<!-- Loading State -->
|
| 81 |
<div id="loadingState" class="auth-loading hidden">
|
| 82 |
<div class="spinner spinner-dark"></div>
|
|
@@ -262,6 +281,56 @@
|
|
| 262 |
}
|
| 263 |
}
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
// Google OAuth callback
|
| 266 |
function handleGoogleCredential(response) {
|
| 267 |
try {
|
|
|
|
| 77 |
<div id="googleBtnContainer"></div>
|
| 78 |
</div>
|
| 79 |
|
| 80 |
+
<div class="auth-divider">
|
| 81 |
+
<span>OR</span>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<!-- Direct Email/Password Login -->
|
| 85 |
+
<div id="emailLoginForm">
|
| 86 |
+
<div class="form-group">
|
| 87 |
+
<label class="form-label">Institution Email</label>
|
| 88 |
+
<input type="email" id="emailInput" class="form-control" placeholder="your@cbit.org.in">
|
| 89 |
+
</div>
|
| 90 |
+
<div class="form-group">
|
| 91 |
+
<label class="form-label">Password</label>
|
| 92 |
+
<input type="password" id="passwordInput" class="form-control" placeholder="••••••••">
|
| 93 |
+
</div>
|
| 94 |
+
<button class="btn btn-primary btn-full" onclick="loginWithEmail()">
|
| 95 |
+
Login with Email
|
| 96 |
+
</button>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
<!-- Loading State -->
|
| 100 |
<div id="loadingState" class="auth-loading hidden">
|
| 101 |
<div class="spinner spinner-dark"></div>
|
|
|
|
| 281 |
}
|
| 282 |
}
|
| 283 |
|
| 284 |
+
async function loginWithEmail() {
|
| 285 |
+
const email = document.getElementById('emailInput').value.trim();
|
| 286 |
+
const password = document.getElementById('passwordInput').value;
|
| 287 |
+
|
| 288 |
+
if (!email || !password) {
|
| 289 |
+
showAlert('Please enter both email and password.');
|
| 290 |
+
return;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
try {
|
| 294 |
+
showLoading('Verifying credentials...');
|
| 295 |
+
|
| 296 |
+
const response = await fetch('/login', {
|
| 297 |
+
method: 'POST',
|
| 298 |
+
headers: { 'Content-Type': 'application/json' },
|
| 299 |
+
body: JSON.stringify({ email, password })
|
| 300 |
+
});
|
| 301 |
+
|
| 302 |
+
if (!response.ok) {
|
| 303 |
+
const err = await response.json();
|
| 304 |
+
throw new Error(err.detail || 'Login failed');
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
const user = await response.json();
|
| 308 |
+
|
| 309 |
+
// Ensure it's a student
|
| 310 |
+
if (user.role !== 'student') {
|
| 311 |
+
showAlert(`Unauthorized: Registered as "${user.role}"`);
|
| 312 |
+
hideLoading();
|
| 313 |
+
return;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
const sessionUser = {
|
| 317 |
+
...user,
|
| 318 |
+
picture: user.picture || `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=4F46E5&color=fff`
|
| 319 |
+
};
|
| 320 |
+
|
| 321 |
+
AuthState.save(sessionUser);
|
| 322 |
+
await Logger.log('LOGIN_SUCCESS_EMAIL', 'auth', { email, role: 'student' }, 'success');
|
| 323 |
+
|
| 324 |
+
showWelcome(sessionUser);
|
| 325 |
+
setTimeout(() => { window.location.href = 'student-dashboard.html'; }, 1800);
|
| 326 |
+
|
| 327 |
+
} catch (err) {
|
| 328 |
+
console.error('Login error:', err);
|
| 329 |
+
hideLoading();
|
| 330 |
+
showAlert(err.message === 'User not found' ? 'Email not registered.' : 'Invalid password or account error.');
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
// Google OAuth callback
|
| 335 |
function handleGoogleCredential(response) {
|
| 336 |
try {
|
teacher-login.html
CHANGED
|
@@ -69,6 +69,25 @@
|
|
| 69 |
<div id="googleBtnContainer"></div>
|
| 70 |
</div>
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
<div id="loadingState" class="auth-loading hidden">
|
| 73 |
<div class="spinner spinner-dark"></div>
|
| 74 |
<span id="loadingText">Verifying faculty credentials...</span>
|
|
@@ -214,6 +233,62 @@
|
|
| 214 |
}
|
| 215 |
}
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
function handleGoogleCredential(response) {
|
| 218 |
try {
|
| 219 |
const parts = response.credential.split('.');
|
|
|
|
| 69 |
<div id="googleBtnContainer"></div>
|
| 70 |
</div>
|
| 71 |
|
| 72 |
+
<div class="auth-divider">
|
| 73 |
+
<span>OR</span>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<!-- Direct Email/Password Login -->
|
| 77 |
+
<div id="emailLoginForm">
|
| 78 |
+
<div class="form-group">
|
| 79 |
+
<label class="form-label">Institution Email</label>
|
| 80 |
+
<input type="email" id="emailInput" class="form-control" placeholder="teacher@cbit.org.in">
|
| 81 |
+
</div>
|
| 82 |
+
<div class="form-group">
|
| 83 |
+
<label class="form-label">Password</label>
|
| 84 |
+
<input type="password" id="passwordInput" class="form-control" placeholder="••••••••">
|
| 85 |
+
</div>
|
| 86 |
+
<button class="btn btn-primary btn-full" onclick="loginWithEmail()">
|
| 87 |
+
Login with Email
|
| 88 |
+
</button>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
<div id="loadingState" class="auth-loading hidden">
|
| 92 |
<div class="spinner spinner-dark"></div>
|
| 93 |
<span id="loadingText">Verifying faculty credentials...</span>
|
|
|
|
| 233 |
}
|
| 234 |
}
|
| 235 |
|
| 236 |
+
async function loginWithEmail() {
|
| 237 |
+
const email = document.getElementById('emailInput').value.trim();
|
| 238 |
+
const password = document.getElementById('passwordInput').value;
|
| 239 |
+
|
| 240 |
+
if (!email || !password) {
|
| 241 |
+
showAlert('Please enter both email and password.');
|
| 242 |
+
return;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
try {
|
| 246 |
+
showLoading('Verifying credentials...');
|
| 247 |
+
|
| 248 |
+
const response = await fetch('/login', {
|
| 249 |
+
method: 'POST',
|
| 250 |
+
headers: { 'Content-Type': 'application/json' },
|
| 251 |
+
body: JSON.stringify({ email, password })
|
| 252 |
+
});
|
| 253 |
+
|
| 254 |
+
if (!response.ok) {
|
| 255 |
+
const err = await response.json();
|
| 256 |
+
throw new Error(err.detail || 'Login failed');
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
const user = await response.json();
|
| 260 |
+
|
| 261 |
+
// Ensure it's faculty/admin
|
| 262 |
+
if (user.role !== 'teacher' && user.role !== 'admin') {
|
| 263 |
+
showAlert(`Unauthorized: Registered as "${user.role}"`);
|
| 264 |
+
hideLoading();
|
| 265 |
+
return;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
const sessionUser = {
|
| 269 |
+
...user,
|
| 270 |
+
picture: user.picture || `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=10B981&color=fff`
|
| 271 |
+
};
|
| 272 |
+
|
| 273 |
+
AuthState.save(sessionUser);
|
| 274 |
+
await Logger.log('LOGIN_SUCCESS_EMAIL', 'auth', { email, role: user.role }, 'success');
|
| 275 |
+
|
| 276 |
+
document.getElementById('loginCard').classList.add('hidden');
|
| 277 |
+
document.getElementById('welcomeCard').classList.remove('hidden');
|
| 278 |
+
document.getElementById('welcomeAvatar').src = sessionUser.picture;
|
| 279 |
+
document.getElementById('welcomeName').textContent = sessionUser.email;
|
| 280 |
+
|
| 281 |
+
setTimeout(() => {
|
| 282 |
+
window.location.href = user.role === 'admin' ? 'admin-panel.html' : 'teacher-dashboard.html';
|
| 283 |
+
}, 1800);
|
| 284 |
+
|
| 285 |
+
} catch (err) {
|
| 286 |
+
console.error('Login error:', err);
|
| 287 |
+
hideLoading();
|
| 288 |
+
showAlert(err.message === 'User not found' ? 'Email not registered.' : 'Invalid password or account error.');
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
function handleGoogleCredential(response) {
|
| 293 |
try {
|
| 294 |
const parts = response.credential.split('.');
|