added optional usser password

This commit is contained in:
2025-12-11 19:45:20 +01:00
parent 9f8316b7e6
commit c9a9f847f7
7 changed files with 748 additions and 229 deletions

View File

@@ -11,10 +11,6 @@ const modalCreateUser = document.getElementById('modalCreateUser');
const closeCreateModal = document.getElementById('closeCreateModal');
const cancelCreate = document.getElementById('cancelCreate');
const createUserForm = document.getElementById('createUserForm');
const usernameInput = document.getElementById('username');
const avatarInput = document.getElementById('avatarInput');
const avatarPreview = document.getElementById('avatarPreview');
const avatarUploadArea = document.getElementById('avatarUploadArea');
const modalUserActions = document.getElementById('modalUserActions');
const closeActionsModal = document.getElementById('closeActionsModal');
@@ -24,10 +20,6 @@ const modalEditUser = document.getElementById('modalEditUser');
const closeEditModal = document.getElementById('closeEditModal');
const cancelEdit = document.getElementById('cancelEdit');
const editUserForm = document.getElementById('editUserForm');
const editUsernameInput = document.getElementById('editUsername');
const editAvatarPreview = document.getElementById('editAvatarPreview');
const editAvatarUploadArea = document.getElementById('editAvatarUploadArea');
const editAvatarInput = document.getElementById('editAvatarInput');
const modalAniList = document.getElementById('modalAniList');
const closeAniListModal = document.getElementById('closeAniListModal');
@@ -45,23 +37,12 @@ if (anilistStatus === "success") {
if (anilistStatus === "error") {
showUserToast("❌ Failed to connect AniList");
}
document.addEventListener('DOMContentLoaded', () => {
initAvatarUpload('avatarUploadArea', 'avatarInput', 'avatarPreview');
initAvatarUpload('editAvatarUploadArea', 'editAvatarInput', 'editAvatarPreview');
loadUsers();
attachEventListeners();
});
function authFetch(url, options = {}) {
const token = localStorage.getItem('token');
const headers = {
...(options.headers || {}),
};
if (token) headers.Authorization = `Bearer ${token}`;
return fetch(url, { ...options, headers });
}
function showUserToast(message, type = 'info') {
if (!toastContainer) return;
@@ -162,6 +143,29 @@ function fileToBase64(file) {
});
}
function togglePasswordVisibility(inputId, buttonElement) {
const input = document.getElementById(inputId);
if (!input) return;
if (input.type === 'password') {
input.type = 'text';
buttonElement.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
`;
} else {
input.type = 'password';
buttonElement.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
`;
}
}
async function loadUsers() {
try {
const res = await fetch(`${API_BASE}/users`);
@@ -175,63 +179,6 @@ async function loadUsers() {
}
}
async function createUser(username, profilePictureUrl) {
try {
const body = { username };
if (profilePictureUrl) body.profilePictureUrl = profilePictureUrl;
const res = await fetch(`${API_BASE}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || 'Error creating user');
}
return await res.json();
} catch (err) {
throw err;
}
}
async function updateUser(userId, username, profilePictureUrl) {
try {
const updates = { username };
if (profilePictureUrl !== undefined) updates.profilePictureUrl = profilePictureUrl;
const res = await fetch(`${API_BASE}/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || 'Error updating user');
}
return await res.json();
} catch (err) {
throw err;
}
}
async function deleteUser(userId) {
try {
const res = await fetch(`${API_BASE}/users/${userId}`, { method: 'DELETE' });
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || 'Error deleting user');
}
return await res.json();
} catch (err) {
throw err;
}
}
function renderUsers() {
if (!usersGrid) return;
@@ -251,10 +198,13 @@ function createUserCard(user) {
const card = document.createElement('div');
card.className = 'user-card';
card.addEventListener('click', (e) => {
if (user.has_password) {
card.classList.add('has-password');
}
card.addEventListener('click', (e) => {
if (!e.target.closest('.user-config-btn')) {
loginUser(user.id);
loginUser(user.id, user.has_password);
}
});
@@ -271,10 +221,6 @@ function createUserCard(user) {
<div class="user-avatar">${avatarContent}</div>
<div class="user-info">
<div class="user-name">${user.username}</div>
<div class="user-status">
<span class="status-dot"></span>
<span>Available</span>
</div>
</div>
<button class="user-config-btn" title="Manage User" data-user-id="${user.id}">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -310,17 +256,70 @@ function showEmptyState() {
}
function openCreateModal() {
modalCreateUser.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h2>Create New User</h2>
<button class="modal-close" onclick="closeModal()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<form id="createUserFormDynamic">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required maxlength="20" placeholder="Enter your name">
</div>
<div class="form-group">
<label for="createPassword">
Password <span class="optional-label">(Optional)</span>
</label>
<div class="password-toggle-wrapper">
<input type="password" id="createPassword" name="password" placeholder="Leave empty for no password">
<button type="button" class="password-toggle-btn" onclick="togglePasswordVisibility('createPassword', this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
</div>
<div class="form-group">
<label>Profile Picture</label>
<div class="avatar-upload-area" id="avatarUploadArea">
<div class="avatar-preview" id="avatarPreview">
<svg class="avatar-preview-placeholder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<p class="avatar-upload-text">Click to upload or drag and drop</p>
<p class="avatar-upload-hint">PNG, JPG up to 5MB</p>
</div>
<input type="file" id="avatarInput" accept="image/*">
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick="closeModal()">Cancel</button>
<button type="submit" class="btn-primary">Create User</button>
</div>
</form>
</div>
`;
modalCreateUser.classList.add('active');
if (usernameInput) usernameInput.focus();
initAvatarUpload('avatarUploadArea', 'avatarInput', 'avatarPreview');
document.getElementById('createUserFormDynamic').addEventListener('submit', handleCreateUser);
document.getElementById('username').focus();
selectedFile = null;
if (avatarPreview) {
avatarPreview.innerHTML = `
<svg class="avatar-preview-placeholder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
}
}
function closeModal() {
@@ -328,23 +327,21 @@ function closeModal() {
modalAniList.classList.remove('active');
modalUserActions.classList.remove('active');
modalEditUser.classList.remove('active');
if (createUserForm) createUserForm.reset();
if (editUserForm) editUserForm.reset();
selectedFile = null;
const modalHeader = modalAniList.querySelector('.modal-header h2');
if (modalHeader) modalHeader.textContent = 'AniList Integration';
}
async function handleCreateUser(e) {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('createPassword').value.trim();
if (!username) {
showUserToast('Please enter a username', 'error');
return;
}
const submitBtn = createUserForm.querySelector('button[type="submit"]');
const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Creating...';
@@ -352,7 +349,20 @@ async function handleCreateUser(e) {
let profilePictureUrl = null;
if (selectedFile) profilePictureUrl = await fileToBase64(selectedFile);
await createUser(username, profilePictureUrl);
const body = { username };
if (profilePictureUrl) body.profilePictureUrl = profilePictureUrl;
if (password) body.password = password;
const res = await fetch(`${API_BASE}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || 'Error creating user');
}
closeModal();
await loadUsers();
@@ -382,15 +392,30 @@ function openUserActionsModal(userId) {
content.innerHTML = `
<div class="manage-actions-modal">
<button class="btn-action edit" onclick="openEditModal(${user.id})">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
</svg>
Edit Profile
</button>
<button class="btn-action password" onclick="openPasswordModal(${user.id})">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
${user.has_password ? 'Change Password' : 'Add Password'}
</button>
<button class="btn-action anilist" onclick="openAniListModal(${user.id})">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
AniList Integration
</button>
<button class="btn-action delete" onclick="handleDeleteConfirmation(${user.id})">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="18" y1="9" x2="12" y2="15"></line><line x1="12" y1="9" x2="18" y2="15"></line></svg>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path>
<line x1="18" y1="9" x2="12" y2="15"></line>
<line x1="12" y1="9" x2="18" y2="15"></line>
</svg>
Delete Profile
</button>
<button class="btn-action cancel" onclick="closeModal()">
@@ -408,24 +433,57 @@ window.openEditModal = function(userId) {
const user = users.find(u => u.id === userId);
if (!user) return;
editUsernameInput.value = user.username || '';
selectedFile = null;
modalEditUser.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h2>Edit Profile</h2>
<button class="modal-close" onclick="closeModal()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<form id="editUserFormDynamic">
<div class="form-group">
<label for="editUsername">Username</label>
<input type="text" id="editUsername" name="username" required maxlength="20" value="${user.username}">
</div>
<div class="form-group">
<label>Profile Picture</label>
<div class="avatar-upload-area" id="editAvatarUploadArea">
<div class="avatar-preview" id="editAvatarPreview">
${user.profile_picture_url
? `<img src="${user.profile_picture_url}" alt="Avatar">`
: `<svg class="avatar-preview-placeholder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>`
}
</div>
<p class="avatar-upload-text">Click to upload or drag and drop</p>
<p class="avatar-upload-hint">PNG, JPG up to 5MB</p>
</div>
<input type="file" id="editAvatarInput" accept="image/*">
</div>
const avatarPlaceholder = `
<svg class="avatar-preview-placeholder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick="closeModal()">Cancel</button>
<button type="submit" class="btn-primary">Save Changes</button>
</div>
</form>
</div>
`;
if (user.profile_picture_url) {
editAvatarPreview.innerHTML = `<img src="${user.profile_picture_url}" alt="Avatar preview">`;
} else {
editAvatarPreview.innerHTML = avatarPlaceholder;
}
if (editAvatarInput) editAvatarInput.value = '';
modalEditUser.classList.add('active');
initAvatarUpload('editAvatarUploadArea', 'editAvatarInput', 'editAvatarPreview');
selectedFile = null;
document.getElementById('editUserFormDynamic').addEventListener('submit', handleEditUser);
};
async function handleEditUser(e) {
@@ -434,13 +492,13 @@ async function handleEditUser(e) {
const user = users.find(u => u.id === currentUserId);
if (!user) return;
const username = editUsernameInput.value.trim();
const username = document.getElementById('editUsername').value.trim();
if (!username) {
showUserToast('Please enter a username', 'error');
return;
}
const submitBtn = editUserForm.querySelector('.btn-primary');
const submitBtn = e.target.querySelector('.btn-primary');
submitBtn.disabled = true;
submitBtn.textContent = 'Saving...';
@@ -448,7 +506,19 @@ async function handleEditUser(e) {
let profilePictureUrl;
if (selectedFile) profilePictureUrl = await fileToBase64(selectedFile);
await updateUser(currentUserId, username, profilePictureUrl);
const updates = { username };
if (profilePictureUrl !== undefined) updates.profilePictureUrl = profilePictureUrl;
const res = await fetch(`${API_BASE}/users/${currentUserId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || 'Error updating user');
}
closeModal();
await loadUsers();
@@ -462,6 +532,162 @@ async function handleEditUser(e) {
}
}
window.openPasswordModal = function(userId) {
currentUserId = userId;
modalUserActions.classList.remove('active');
const user = users.find(u => u.id === userId);
if (!user) return;
modalAniList.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h2>${user.has_password ? 'Change Password' : 'Add Password'}</h2>
<button class="modal-close" onclick="closeModal()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="password-modal-content">
${user.has_password ? `
<div class="password-info">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
<span>This profile is currently password protected</span>
</div>
` : ''}
<form id="passwordForm">
${user.has_password ? `
<div class="form-group">
<label for="currentPassword">Current Password</label>
<div class="password-toggle-wrapper">
<input type="password" id="currentPassword" required placeholder="Enter current password">
<button type="button" class="password-toggle-btn" onclick="togglePasswordVisibility('currentPassword', this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
</div>
` : ''}
<div class="form-group">
<label for="newPassword">New Password ${user.has_password ? '' : '<span class="optional-label">(Optional)</span>'}</label>
<div class="password-toggle-wrapper">
<input type="password" id="newPassword" ${user.has_password ? '' : ''} placeholder="Enter new password">
<button type="button" class="password-toggle-btn" onclick="togglePasswordVisibility('newPassword', this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick="closeModal()">Cancel</button>
${user.has_password ? `
<button type="button" class="btn-disconnect" onclick="handleRemovePassword()">
Remove Password
</button>
` : ''}
<button type="submit" class="btn-primary">
${user.has_password ? 'Update' : 'Set'} Password
</button>
</div>
</form>
</div>
</div>
`;
modalAniList.classList.add('active');
document.getElementById('passwordForm').addEventListener('submit', handlePasswordSubmit);
};
async function handlePasswordSubmit(e) {
e.preventDefault();
const user = users.find(u => u.id === currentUserId);
if (!user) return;
const currentPassword = user.has_password ? document.getElementById('currentPassword').value : null;
const newPassword = document.getElementById('newPassword').value;
if (!newPassword && !user.has_password) {
showUserToast('Please enter a password', 'error');
return;
}
const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Updating...';
try {
const body = { newPassword: newPassword || null };
if (currentPassword) body.currentPassword = currentPassword;
const res = await fetch(`${API_BASE}/users/${currentUserId}/password`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || 'Error updating password');
}
closeModal();
await loadUsers();
showUserToast('Password updated successfully!', 'success');
} catch (err) {
console.error(err);
showUserToast(err.message || 'Error updating password', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = user.has_password ? 'Update Password' : 'Set Password';
}
}
window.handleRemovePassword = async function() {
if (!confirm('Are you sure you want to remove the password protection from this profile?')) return;
try {
const currentPassword = document.getElementById('currentPassword').value;
if (!currentPassword) {
showUserToast('Please enter your current password', 'error');
return;
}
const res = await fetch(`${API_BASE}/users/${currentUserId}/password`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ currentPassword, newPassword: null })
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || 'Error removing password');
}
closeModal();
await loadUsers();
showUserToast('Password removed successfully!', 'success');
} catch (err) {
console.error(err);
showUserToast(err.message || 'Error removing password', 'error');
}
};
window.handleDeleteConfirmation = function(userId) {
const user = users.find(u => u.id === userId);
if (!user) return;
@@ -480,7 +706,13 @@ window.handleConfirmedDeleteUser = async function(userId) {
showUserToast('Deleting user...', 'info');
try {
await deleteUser(userId);
const res = await fetch(`${API_BASE}/users/${userId}`, { method: 'DELETE' });
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || 'Error deleting user');
}
await loadUsers();
showUserToast('User deleted successfully!', 'success');
} catch (err) {
@@ -492,27 +724,36 @@ window.handleConfirmedDeleteUser = async function(userId) {
function showConfirmationModal(title, message, confirmAction) {
closeModal();
const modalHeader = modalAniList.querySelector('.modal-header h2');
if (modalHeader) modalHeader.textContent = title;
aniListContent.innerHTML = `
<div style="text-align: center; padding: 1rem;">
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" style="opacity: 0.8; margin: 0 auto 1rem; display: block;">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<p style="color: var(--color-text-secondary); margin-bottom: 2rem; font-size: 1rem;">
${message}
</p>
<div style="display: flex; gap: 1rem;">
<button class="btn-secondary" style="flex: 1;" onclick="closeModal()">
Cancel
</button>
<button class="btn-disconnect" style="flex: 1; background: #ef4444; color: white;" onclick="window.${confirmAction}">
Confirm Delete
modalAniList.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2>${title}</h2>
<button class="modal-close" onclick="closeModal()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div style="text-align: center; padding: 1rem;">
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" style="opacity: 0.8; margin: 0 auto 1rem; display: block;">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<p style="color: var(--color-text-secondary); margin-bottom: 2rem; font-size: 1rem;">
${message}
</p>
<div style="display: flex; gap: 1rem;">
<button class="btn-secondary" style="flex: 1;" onclick="closeModal()">
Cancel
</button>
<button class="btn-disconnect" style="flex: 1; background: #ef4444; color: white;" onclick="window.${confirmAction}">
Confirm Delete
</button>
</div>
</div>
</div>
`;
@@ -522,22 +763,41 @@ function showConfirmationModal(title, message, confirmAction) {
function openAniListModal(userId) {
currentUserId = userId;
aniListContent.innerHTML = `<div style="text-align: center; padding: 2rem;">Loading integration status...</div>`;
modalUserActions.classList.remove('active');
modalEditUser.classList.remove('active');
aniListContent.innerHTML = `<div style="text-align: center; padding: 2rem;">Loading integration status...</div>`;
modalAniList.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h2>AniList Integration</h2>
<button class="modal-close" onclick="closeModal()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div id="aniListContent">
<div style="text-align: center; padding: 2rem;">Loading integration status...</div>
</div>
</div>
`;
modalAniList.classList.add('active');
getIntegrationStatus(userId).then(integration => {
aniListContent.innerHTML = `
const content = document.getElementById('aniListContent');
content.innerHTML = `
<div class="anilist-status">
${integration.connected ? `
<div class="anilist-connected">
<div class="anilist-icon">
<img
src="https://anilist.co/img/icons/icon.svg"
alt="AniList"
style="width:40px; height:40px;"
>
</div>
<img src="https://anilist.co/img/icons/icon.svg" alt="AniList" style="width:40px; height:40px;">
</div>
<div class="anilist-info">
<h3>Connected to AniList</h3>
<p>User ID: ${integration.anilistUserId}</p>
@@ -553,31 +813,27 @@ function openAniListModal(userId) {
<p style="color: var(--color-text-secondary); margin-bottom: 1.5rem;">
Sync your anime list by logging in with AniList.
</p>
<div style="display:flex; justify-content:center;">
<button class="btn-connect" onclick="redirectToAniListLogin()">
Login with AniList
</button>
</div>
<p style="font-size:0.85rem; margin-top:1rem; color:var(--color-text-secondary);">You will be redirected and then returned here.</p>
<p style="font-size:0.85rem; margin-top:1rem; color:var(--color-text-secondary)">
You will be redirected and then returned here.
</p>
</div>
`}
</div>
`;
modalAniList.classList.add('active');
}).catch(err => {
console.error(err);
aniListContent.innerHTML = `<div style="text-align:center;padding:1rem;color:var(--color-text-secondary)">Error loading integration status.</div>`;
modalAniList.classList.add('active');
const content = document.getElementById('aniListContent');
content.innerHTML = `<div style="text-align:center;padding:1rem;color:var(--color-text-secondary)">Error loading integration status.</div>`;
});
}
async function redirectToAniListLogin() {
try {
const res = await fetch(`/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -590,10 +846,7 @@ async function redirectToAniListLogin() {
localStorage.setItem('token', data.token);
const clientId = 32898;
const redirectUri = encodeURIComponent(
window.location.origin + '/api/anilist'
);
const redirectUri = encodeURIComponent(window.location.origin + '/api/anilist');
const state = encodeURIComponent(currentUserId);
window.location.href =
@@ -611,7 +864,6 @@ async function redirectToAniListLogin() {
async function getIntegrationStatus(userId) {
try {
const res = await fetch(`${API_BASE}/users/${userId}/integration`);
if (!res.ok) {
return { connected: false };
@@ -623,11 +875,15 @@ async function getIntegrationStatus(userId) {
}
}
async function disconnectAniList(userId) {
try {
window.handleDisconnectAniList = async function() {
if (!confirm('Are you sure you want to disconnect AniList?')) return;
const res = await authFetch(`${API_BASE}/users/${userId}/integration`, {
method: 'DELETE'
try {
const res = await fetch(`${API_BASE}/users/${currentUserId}/integration`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!res.ok) {
@@ -635,31 +891,71 @@ async function disconnectAniList(userId) {
throw new Error(error.error || 'Error disconnecting AniList');
}
return await res.json();
} catch (err) {
throw err;
}
}
window.handleDisconnectAniList = async function() {
if (!confirm('Are you sure you want to disconnect AniList?')) return;
try {
await disconnectAniList(currentUserId);
showUserToast('AniList disconnected successfully', 'success');
await openAniListModal(currentUserId);
openAniListModal(currentUserId);
} catch (err) {
console.error(err);
showUserToast('Error disconnecting AniList', 'error');
}
};
async function loginUser(userId) {
async function loginUser(userId, hasPassword) {
if (hasPassword) {
// Mostrar modal de contraseña
modalAniList.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2>Enter Password</h2>
<button class="modal-close" onclick="closeModal()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<form id="loginPasswordForm">
<div class="form-group">
<label for="loginPassword">Password</label>
<div class="password-toggle-wrapper">
<input type="password" id="loginPassword" required placeholder="Enter your password" autofocus>
<button type="button" class="password-toggle-btn" onclick="togglePasswordVisibility('loginPassword', this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick="closeModal()">Cancel</button>
<button type="submit" class="btn-primary">Login</button>
</div>
</form>
</div>
`;
modalAniList.classList.add('active');
document.getElementById('loginPasswordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('loginPassword').value;
await performLogin(userId, password);
});
} else {
await performLogin(userId);
}
}
async function performLogin(userId, password = null) {
try {
const body = { userId };
if (password) body.password = password;
const res = await fetch(`${API_BASE}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId })
body: JSON.stringify(body)
});
if (!res.ok) {
@@ -673,13 +969,9 @@ async function loginUser(userId) {
window.location.href = '/anime';
} catch (err) {
console.error('Login error', err);
showUserToast('Login failed', 'error');
showUserToast(err.message || 'Login failed', 'error');
}
}
window.openAniListModal = openAniListModal;
window.openEditModal = window.openEditModal;
window.handleDeleteConfirmation = window.handleDeleteConfirmation;
window.handleConfirmedDeleteUser = window.handleConfirmedDeleteUser;
window.redirectToAniListLogin = redirectToAniListLogin;