- Add tech user management (JSON-backed CRUD with PIN auth) - Dual login: tabbed Tech Login (username+PIN) / Admin Login (NC credentials) - Admin panel: tappable user list with detail modal (enable/disable, reset PIN, reset NC password, delete) - Auto-provision Nextcloud accounts for tech users - Admin guard: tech users redirected away from admin panel - New data volume for persistent tech_users.json storage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
196 lines
7.0 KiB
JavaScript
196 lines
7.0 KiB
JavaScript
// NextSnap - Authentication logic
|
|
'use strict';
|
|
|
|
const Auth = {
|
|
currentTab: 'tech',
|
|
|
|
init() {
|
|
// Tab switching
|
|
document.querySelectorAll('.login-tab').forEach(tab => {
|
|
tab.addEventListener('click', () => this.switchTab(tab.dataset.tab));
|
|
});
|
|
|
|
// Tech login form
|
|
const techForm = document.getElementById('tech-login-form');
|
|
if (techForm) {
|
|
techForm.addEventListener('submit', (e) => this.handleTechLogin(e));
|
|
}
|
|
|
|
// Admin login form
|
|
const adminForm = document.getElementById('admin-login-form');
|
|
if (adminForm) {
|
|
adminForm.addEventListener('submit', (e) => this.handleAdminLogin(e));
|
|
}
|
|
|
|
// Clear field errors on input
|
|
document.querySelectorAll('.form-input').forEach(input => {
|
|
input.addEventListener('input', () => {
|
|
const errorEl = document.getElementById(input.id + '-error');
|
|
if (errorEl) errorEl.textContent = '';
|
|
input.classList.remove('error');
|
|
});
|
|
});
|
|
},
|
|
|
|
switchTab(tab) {
|
|
this.currentTab = tab;
|
|
|
|
// Update tab buttons
|
|
document.querySelectorAll('.login-tab').forEach(t => {
|
|
t.classList.toggle('active', t.dataset.tab === tab);
|
|
});
|
|
|
|
// Show/hide forms
|
|
const techForm = document.getElementById('tech-login-form');
|
|
const adminForm = document.getElementById('admin-login-form');
|
|
const helpText = document.getElementById('login-help');
|
|
|
|
if (tab === 'tech') {
|
|
techForm.style.display = '';
|
|
adminForm.style.display = 'none';
|
|
helpText.innerHTML = '<strong>Tip:</strong> Use your username and PIN to login';
|
|
const field = document.getElementById('tech-username');
|
|
if (field) field.focus();
|
|
} else {
|
|
techForm.style.display = 'none';
|
|
adminForm.style.display = '';
|
|
helpText.innerHTML = '<strong>Tip:</strong> Use your Nextcloud admin credentials';
|
|
const field = document.getElementById('admin-username');
|
|
if (field) field.focus();
|
|
}
|
|
|
|
// Clear errors on tab switch
|
|
document.querySelectorAll('.error-message').forEach(el => el.classList.add('hidden'));
|
|
document.querySelectorAll('.field-error').forEach(el => el.textContent = '');
|
|
document.querySelectorAll('.form-input').forEach(el => el.classList.remove('error'));
|
|
},
|
|
|
|
_setLoading(btn, loading) {
|
|
const text = btn.querySelector('.btn-text');
|
|
const spinner = btn.querySelector('.btn-loading');
|
|
btn.disabled = loading;
|
|
text.classList.toggle('hidden', loading);
|
|
spinner.classList.toggle('hidden', !loading);
|
|
},
|
|
|
|
async handleTechLogin(event) {
|
|
event.preventDefault();
|
|
|
|
const username = document.getElementById('tech-username').value.trim();
|
|
const pin = document.getElementById('tech-pin').value;
|
|
const errorMessage = document.getElementById('tech-error-message');
|
|
const btn = document.getElementById('tech-login-btn');
|
|
|
|
if (!username) {
|
|
document.getElementById('tech-username-error').textContent = 'Username is required';
|
|
document.getElementById('tech-username').classList.add('error');
|
|
return;
|
|
}
|
|
if (!pin) {
|
|
document.getElementById('tech-pin-error').textContent = 'PIN is required';
|
|
document.getElementById('tech-pin').classList.add('error');
|
|
return;
|
|
}
|
|
|
|
errorMessage.classList.add('hidden');
|
|
this._setLoading(btn, true);
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/login/tech', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, pin })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
window.location.href = '/capture';
|
|
} else {
|
|
errorMessage.textContent = data.error || 'Login failed. Please check your credentials.';
|
|
errorMessage.classList.remove('hidden');
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
errorMessage.textContent = 'Network error. Please check your connection and try again.';
|
|
errorMessage.classList.remove('hidden');
|
|
} finally {
|
|
this._setLoading(btn, false);
|
|
}
|
|
},
|
|
|
|
async handleAdminLogin(event) {
|
|
event.preventDefault();
|
|
|
|
const username = document.getElementById('admin-username').value.trim();
|
|
const password = document.getElementById('admin-password').value;
|
|
const errorMessage = document.getElementById('admin-error-message');
|
|
const btn = document.getElementById('admin-login-btn');
|
|
|
|
if (!username) {
|
|
document.getElementById('admin-username-error').textContent = 'Username is required';
|
|
document.getElementById('admin-username').classList.add('error');
|
|
return;
|
|
}
|
|
if (!password) {
|
|
document.getElementById('admin-password-error').textContent = 'Password is required';
|
|
document.getElementById('admin-password').classList.add('error');
|
|
return;
|
|
}
|
|
|
|
errorMessage.classList.add('hidden');
|
|
this._setLoading(btn, true);
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
window.location.href = '/capture';
|
|
} else {
|
|
errorMessage.textContent = data.error || 'Login failed. Please check your credentials.';
|
|
errorMessage.classList.remove('hidden');
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
errorMessage.textContent = 'Network error. Please check your connection and try again.';
|
|
errorMessage.classList.remove('hidden');
|
|
} finally {
|
|
this._setLoading(btn, false);
|
|
}
|
|
},
|
|
|
|
async checkStatus() {
|
|
try {
|
|
const response = await fetch('/api/auth/status');
|
|
const data = await response.json();
|
|
return data.authenticated ? data : null;
|
|
} catch (error) {
|
|
console.error('Auth status check failed:', error);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
async logout() {
|
|
try {
|
|
await fetch('/api/auth/logout', { method: 'POST' });
|
|
window.location.href = '/';
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
window.location.href = '/';
|
|
}
|
|
}
|
|
};
|
|
|
|
// Initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => Auth.init());
|
|
} else {
|
|
Auth.init();
|
|
}
|