Offline-first photo capture app for Nextcloud with: - Camera capture with continuous mode (auto-reopens after each photo) - File browser with fullscreen image gallery, swipe navigation, and rename - Upload queue with background sync engine - Admin panel for Nextcloud user management - Service worker for offline-first caching (v13) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
161 lines
5.6 KiB
JavaScript
161 lines
5.6 KiB
JavaScript
// NextSnap - Authentication logic
|
|
'use strict';
|
|
|
|
const Auth = {
|
|
init() {
|
|
const loginForm = document.getElementById('login-form');
|
|
if (loginForm) {
|
|
loginForm.addEventListener('submit', this.handleLogin.bind(this));
|
|
|
|
// Add input validation
|
|
const usernameInput = document.getElementById('username');
|
|
const passwordInput = document.getElementById('password');
|
|
|
|
if (usernameInput) {
|
|
usernameInput.addEventListener('blur', () => this.validateUsername());
|
|
usernameInput.addEventListener('input', () => this.clearFieldError('username'));
|
|
}
|
|
|
|
if (passwordInput) {
|
|
passwordInput.addEventListener('input', () => this.clearFieldError('password'));
|
|
}
|
|
}
|
|
},
|
|
|
|
validateUsername() {
|
|
const username = document.getElementById('username').value.trim();
|
|
const usernameError = document.getElementById('username-error');
|
|
const usernameInput = document.getElementById('username');
|
|
|
|
if (!username) {
|
|
usernameError.textContent = 'Username is required';
|
|
usernameInput.classList.add('error');
|
|
return false;
|
|
}
|
|
|
|
if (username.length < 2) {
|
|
usernameError.textContent = 'Username must be at least 2 characters';
|
|
usernameInput.classList.add('error');
|
|
return false;
|
|
}
|
|
|
|
usernameError.textContent = '';
|
|
usernameInput.classList.remove('error');
|
|
return true;
|
|
},
|
|
|
|
validatePassword() {
|
|
const password = document.getElementById('password').value;
|
|
const passwordError = document.getElementById('password-error');
|
|
const passwordInput = document.getElementById('password');
|
|
|
|
if (!password) {
|
|
passwordError.textContent = 'Password is required';
|
|
passwordInput.classList.add('error');
|
|
return false;
|
|
}
|
|
|
|
passwordError.textContent = '';
|
|
passwordInput.classList.remove('error');
|
|
return true;
|
|
},
|
|
|
|
clearFieldError(field) {
|
|
const errorElement = document.getElementById(`${field}-error`);
|
|
const inputElement = document.getElementById(field);
|
|
if (errorElement) errorElement.textContent = '';
|
|
if (inputElement) inputElement.classList.remove('error');
|
|
},
|
|
|
|
async handleLogin(event) {
|
|
event.preventDefault();
|
|
|
|
// Validate fields
|
|
const usernameValid = this.validateUsername();
|
|
const passwordValid = this.validatePassword();
|
|
|
|
if (!usernameValid || !passwordValid) {
|
|
return;
|
|
}
|
|
|
|
const username = document.getElementById('username').value.trim();
|
|
const password = document.getElementById('password').value;
|
|
const errorMessage = document.getElementById('error-message');
|
|
const loginBtn = document.getElementById('login-btn');
|
|
const loginBtnText = document.getElementById('login-btn-text');
|
|
const loginBtnLoading = document.getElementById('login-btn-loading');
|
|
|
|
// Clear previous error
|
|
errorMessage.classList.add('hidden');
|
|
errorMessage.textContent = '';
|
|
|
|
// Disable button and show loading state
|
|
loginBtn.disabled = true;
|
|
loginBtnText.classList.add('hidden');
|
|
loginBtnLoading.classList.remove('hidden');
|
|
|
|
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) {
|
|
// Login successful - redirect to capture page
|
|
window.location.href = '/capture';
|
|
} else {
|
|
// Login failed - show error
|
|
const errorText = data.error || 'Login failed. Please check your credentials and try again.';
|
|
errorMessage.textContent = errorText;
|
|
errorMessage.classList.remove('hidden');
|
|
|
|
// Focus back on username for retry
|
|
document.getElementById('username').focus();
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
errorMessage.textContent = 'Network error. Please check your connection and try again.';
|
|
errorMessage.classList.remove('hidden');
|
|
} finally {
|
|
// Re-enable button and restore normal state
|
|
loginBtn.disabled = false;
|
|
loginBtnText.classList.remove('hidden');
|
|
loginBtnLoading.classList.add('hidden');
|
|
}
|
|
},
|
|
|
|
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);
|
|
// Redirect anyway
|
|
window.location.href = '/';
|
|
}
|
|
}
|
|
};
|
|
|
|
// Initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => Auth.init());
|
|
} else {
|
|
Auth.init();
|
|
}
|