Add NextSnap PWA with photo gallery viewer and continuous capture
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>
This commit is contained in:
160
app/static/js/auth.js
Normal file
160
app/static/js/auth.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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();
|
||||
}
|
||||
Reference in New Issue
Block a user