Files
nextsnap/app/static/js/auth.js
kamaji 99fb5ff7e7 Add dual user classes: admin + tech users with PIN login
- 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>
2026-02-08 00:17:26 -06:00

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();
}