adding user authentication

This commit is contained in:
DioCrafts
2025-03-20 09:22:31 +01:00
parent 961545aef7
commit cafad0fbfd
69 changed files with 6483 additions and 106 deletions
+201
View File
@@ -0,0 +1,201 @@
/* Auth styles for OxiCloud */
.auth-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-color: #f5f7fa;
}
.auth-panel {
width: 400px;
background-color: white;
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
padding: 30px;
text-align: center;
}
.auth-logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.auth-logo-icon {
width: 50px;
height: 50px;
background-color: #ff5e3a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.auth-logo-icon svg {
width: 30px;
height: 30px;
fill: white;
}
.auth-logo-text {
font-size: 24px;
font-weight: bold;
color: #2a3042;
}
.auth-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 25px;
color: #2a3042;
}
.auth-form {
width: 100%;
text-align: left;
}
.auth-input-group {
margin-bottom: 20px;
}
.auth-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #4b5563;
font-weight: 500;
}
.auth-input {
width: 100%;
padding: 12px 15px;
border-radius: 8px;
border: 1px solid #e2e8f0;
font-size: 14px;
background-color: #f9fafb;
transition: border-color 0.2s;
}
.auth-input:focus {
outline: none;
border-color: #ff5e3a;
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.1);
}
.auth-button {
width: 100%;
padding: 12px 15px;
border-radius: 8px;
background-color: #ff5e3a;
color: white;
font-weight: bold;
border: none;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
margin-top: 10px;
}
.auth-button:hover {
background-color: #e64a2e;
}
.auth-button:disabled {
background-color: #f9a799;
cursor: not-allowed;
}
.auth-toggle {
margin-top: 20px;
font-size: 14px;
color: #718096;
}
.auth-toggle-link {
color: #ff5e3a;
cursor: pointer;
text-decoration: none;
font-weight: 500;
}
.auth-toggle-link:hover {
text-decoration: underline;
}
.auth-error {
background-color: #fee2e2;
color: #b91c1c;
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
display: none;
}
.auth-success {
background-color: #dcfce7;
color: #15803d;
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
display: none;
}
/* Admin setup panel styles */
.admin-setup-panel {
display: none;
}
.setup-steps {
margin-bottom: 25px;
display: flex;
justify-content: space-between;
}
.setup-step {
display: flex;
flex-direction: column;
align-items: center;
width: 30%;
}
.step-number {
width: 30px;
height: 30px;
background-color: #e2e8f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
font-weight: bold;
margin-bottom: 5px;
}
.step-number.active {
background-color: #ff5e3a;
color: white;
}
.step-title {
font-size: 12px;
color: #64748b;
}
.step-title.active {
color: #1e293b;
font-weight: 500;
}
@media (max-width: 480px) {
.auth-panel {
width: 90%;
padding: 20px;
}
}
+12
View File
@@ -170,6 +170,18 @@ body {
align-items: center;
}
.logout-btn {
margin-left: 15px;
color: #64748b;
cursor: pointer;
font-size: 18px;
transition: color 0.2s;
}
.logout-btn:hover {
color: #ff5e3a;
}
.language-selector {
margin-right: 15px;
padding: 5px 12px;
+3
View File
@@ -87,6 +87,9 @@
<div class="user-controls">
<div id="language-selector" class="language-selector">ES</div>
<div class="user-avatar">MR</div>
<div id="logout-btn" class="logout-btn" title="Cerrar sesión">
<i class="fas fa-sign-out-alt"></i>
</div>
</div>
</div>
+59 -1
View File
@@ -43,6 +43,9 @@ function initApp() {
} else {
console.log('Using standard file rendering');
}
// Check authentication
checkAuthentication();
}
/**
@@ -58,6 +61,7 @@ function cacheElements() {
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
elements.breadcrumb = document.querySelector('.breadcrumb');
elements.logoutBtn = document.getElementById('logout-btn');
}
/**
@@ -100,6 +104,9 @@ function setupEventListeners() {
ui.switchToListView();
}
// Logout button
elements.logoutBtn.addEventListener('click', logout);
// Global events to close context menus
document.addEventListener('click', (e) => {
const folderMenu = document.getElementById('folder-context-menu');
@@ -124,7 +131,8 @@ async function loadFiles() {
try {
let url = '/api/folders';
if (app.currentPath) {
url += `/${app.currentPath}`;
// Use the correct endpoint for folder contents
url = `/api/folders/${app.currentPath}/contents`;
}
const response = await fetch(url);
@@ -212,5 +220,55 @@ window.selectFolder = (id, name) => {
loadFiles();
};
/**
* Check if user is authenticated
*/
function checkAuthentication() {
// Nombres de variables según auth.js
const TOKEN_KEY = 'oxicloud_token';
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
const USER_DATA_KEY = 'oxicloud_user';
const token = localStorage.getItem(TOKEN_KEY);
const tokenExpiry = localStorage.getItem(TOKEN_EXPIRY_KEY);
if (!token || !tokenExpiry || new Date(tokenExpiry) < new Date()) {
// No token or expired token
window.location.href = '/login';
return;
}
// Display user information if available
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
if (userData.username) {
// Update user avatar with initials
const userInitials = userData.username.substring(0, 2).toUpperCase();
const userAvatar = document.querySelector('.user-avatar');
if (userAvatar) {
userAvatar.textContent = userInitials;
}
}
}
/**
* Logout - clear all auth data and redirect to login
*/
function logout() {
// Nombres de variables según auth.js
const TOKEN_KEY = 'oxicloud_token';
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
const USER_DATA_KEY = 'oxicloud_user';
// Clear all authentication data
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
localStorage.removeItem(TOKEN_EXPIRY_KEY);
localStorage.removeItem(USER_DATA_KEY);
// Redirect to login page
window.location.href = '/login';
}
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', initApp);
+416
View File
@@ -0,0 +1,416 @@
/**
* OxiCloud Authentication JavaScript
* Handles login, registration, and admin setup
*/
// API endpoints
const API_URL = '/api/auth';
const LOGIN_ENDPOINT = `${API_URL}/login`;
const REGISTER_ENDPOINT = `${API_URL}/register`;
const ME_ENDPOINT = `${API_URL}/me`;
const REFRESH_ENDPOINT = `${API_URL}/refresh`;
// Storage keys
const TOKEN_KEY = 'oxicloud_token';
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
const USER_DATA_KEY = 'oxicloud_user';
// DOM elements
const loginPanel = document.getElementById('login-panel');
const registerPanel = document.getElementById('register-panel');
const adminSetupPanel = document.getElementById('admin-setup-panel');
const loginForm = document.getElementById('login-form');
const registerForm = document.getElementById('register-form');
const adminSetupForm = document.getElementById('admin-setup-form');
const loginError = document.getElementById('login-error');
const registerError = document.getElementById('register-error');
const registerSuccess = document.getElementById('register-success');
const adminSetupError = document.getElementById('admin-setup-error');
// Panel toggles
document.getElementById('show-register').addEventListener('click', () => {
loginPanel.style.display = 'none';
registerPanel.style.display = 'block';
adminSetupPanel.style.display = 'none';
});
document.getElementById('show-login').addEventListener('click', () => {
loginPanel.style.display = 'block';
registerPanel.style.display = 'none';
adminSetupPanel.style.display = 'none';
});
document.getElementById('show-admin-setup').addEventListener('click', () => {
loginPanel.style.display = 'none';
registerPanel.style.display = 'none';
adminSetupPanel.style.display = 'block';
});
document.getElementById('back-to-login').addEventListener('click', () => {
loginPanel.style.display = 'block';
registerPanel.style.display = 'none';
adminSetupPanel.style.display = 'none';
});
// Check if we already have a valid token
document.addEventListener('DOMContentLoaded', async () => {
try {
const tokenExpiry = localStorage.getItem(TOKEN_EXPIRY_KEY);
if (tokenExpiry && new Date(tokenExpiry) > new Date()) {
// Token still valid, redirect to main app
redirectToMainApp();
return;
}
// Token expired, try to refresh
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
if (refreshToken) {
try {
await refreshAuthToken(refreshToken);
redirectToMainApp();
} catch (error) {
// Refresh failed, continue with login page
console.log('Token refresh failed, user needs to login again');
}
}
// Check if admin account exists (customize this as needed)
const isFirstRun = await checkFirstRun();
if (isFirstRun) {
loginPanel.style.display = 'none';
registerPanel.style.display = 'none';
adminSetupPanel.style.display = 'block';
}
} catch (error) {
console.error('Authentication check failed:', error);
}
});
// Login form submission
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Clear previous errors
loginError.style.display = 'none';
const username = document.getElementById('login-username').value;
const password = document.getElementById('login-password').value;
try {
const data = await login(username, password);
// Store auth data
localStorage.setItem(TOKEN_KEY, data.token); // Nombre correcto del campo en la respuesta
localStorage.setItem(REFRESH_TOKEN_KEY, data.refreshToken);
// Extraer fecha de expiración desde el token JWT
const tokenParts = data.token.split('.');
if (tokenParts.length === 3) {
try {
const payload = JSON.parse(atob(tokenParts[1]));
if (payload.exp) {
// payload.exp está en segundos desde epoch
const expiryDate = new Date(payload.exp * 1000);
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryDate.toISOString());
} else {
// Si no hay exp, establecer un valor predeterminado (1 hora)
const expiryTime = new Date();
expiryTime.setHours(expiryTime.getHours() + 1);
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString());
}
} catch (e) {
console.error('Error parsing JWT token:', e);
// Valor predeterminado en caso de error
const expiryTime = new Date();
expiryTime.setHours(expiryTime.getHours() + 1);
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString());
}
} else {
// Token mal formado, establecer tiempo predeterminado
const expiryTime = new Date();
expiryTime.setHours(expiryTime.getHours() + 1);
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString());
}
// Fetch and store user data
// Usamos el token que acabamos de almacenar (en lugar de data.accessToken)
const token = localStorage.getItem(TOKEN_KEY);
// Como el endpoint /me no está implementado, usamos los datos del usuario de la respuesta directamente
const userData = data.user || { id: '123', username: 'testuser', email: 'test@example.com', role: 'user' };
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
// Redirect to main app
redirectToMainApp();
} catch (error) {
loginError.textContent = error.message || 'Error al iniciar sesión';
loginError.style.display = 'block';
}
});
// Register form submission
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Clear previous messages
registerError.style.display = 'none';
registerSuccess.style.display = 'none';
const username = document.getElementById('register-username').value;
const email = document.getElementById('register-email').value;
const password = document.getElementById('register-password').value;
const confirmPassword = document.getElementById('register-password-confirm').value;
// Validate passwords match
if (password !== confirmPassword) {
registerError.textContent = 'Las contraseñas no coinciden';
registerError.style.display = 'block';
return;
}
try {
const data = await register(username, email, password);
// Show success message
registerSuccess.textContent = '¡Cuenta creada con éxito! Puedes iniciar sesión ahora.';
registerSuccess.style.display = 'block';
// Clear form
registerForm.reset();
// Switch to login panel after 2 seconds
setTimeout(() => {
loginPanel.style.display = 'block';
registerPanel.style.display = 'none';
}, 2000);
} catch (error) {
registerError.textContent = error.message || 'Error al registrar cuenta';
registerError.style.display = 'block';
}
});
// Admin setup form submission
adminSetupForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Clear previous errors
adminSetupError.style.display = 'none';
const email = document.getElementById('admin-email').value;
const password = document.getElementById('admin-password').value;
const confirmPassword = document.getElementById('admin-password-confirm').value;
// Validate passwords match
if (password !== confirmPassword) {
adminSetupError.textContent = 'Las contraseñas no coinciden';
adminSetupError.style.display = 'block';
return;
}
try {
// Register admin account
const data = await register('admin', email, password, 'admin');
// Show success and switch to login
alert('¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.');
loginPanel.style.display = 'block';
adminSetupPanel.style.display = 'none';
} catch (error) {
adminSetupError.textContent = error.message || 'Error al crear cuenta de administrador';
adminSetupError.style.display = 'block';
}
});
// API Functions
/**
* Login with username and password
*/
async function login(username, password) {
try {
const response = await fetch(LOGIN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Falló la autenticación');
}
return await response.json();
} catch (error) {
console.error('Login error:', error);
throw error;
}
}
/**
* Register a new user
*/
async function register(username, email, password, role = 'user') {
try {
const response = await fetch(REGISTER_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, email, password, role })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Error en el registro');
}
return await response.json();
} catch (error) {
console.error('Registration error:', error);
throw error;
}
}
/**
* Fetch current user data
*/
async function fetchUserData(token) {
try {
const response = await fetch(ME_ENDPOINT, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Error al obtener datos del usuario');
}
return await response.json();
} catch (error) {
console.error('Error fetching user data:', error);
throw error;
}
}
/**
* Refresh authentication token
*/
async function refreshAuthToken(refreshToken) {
try {
const response = await fetch(REFRESH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken })
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const data = await response.json();
// Update stored tokens
localStorage.setItem(TOKEN_KEY, data.token);
localStorage.setItem(REFRESH_TOKEN_KEY, data.refreshToken);
// Extraer fecha de expiración desde el token JWT
const tokenParts = data.token.split('.');
if (tokenParts.length === 3) {
try {
const payload = JSON.parse(atob(tokenParts[1]));
if (payload.exp) {
// payload.exp está en segundos desde epoch
const expiryDate = new Date(payload.exp * 1000);
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryDate.toISOString());
} else {
// Si no hay exp, establecer un valor predeterminado (1 hora)
const expiryTime = new Date();
expiryTime.setHours(expiryTime.getHours() + 1);
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString());
}
} catch (e) {
console.error('Error parsing JWT token:', e);
// Valor predeterminado en caso de error
const expiryTime = new Date();
expiryTime.setHours(expiryTime.getHours() + 1);
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString());
}
} else {
// Token mal formado, establecer tiempo predeterminado
const expiryTime = new Date();
expiryTime.setHours(expiryTime.getHours() + 1);
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString());
}
return data;
} catch (error) {
console.error('Token refresh error:', error);
// Clear stored auth data on refresh failure
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
localStorage.removeItem(TOKEN_EXPIRY_KEY);
localStorage.removeItem(USER_DATA_KEY);
throw error;
}
}
/**
* Check if this is the first run (no admin exists)
*/
async function checkFirstRun() {
try {
// This is a simple check - in a real app, you'd create a specific endpoint
// to check if admin setup is needed
const response = await fetch(LOGIN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username: 'admin', password: 'invalid-password-just-checking' })
});
// If we get a 404, assume the auth system or admin doesn't exist yet
if (response.status === 404) {
return true;
}
// If we get a 401, the auth system exists but credentials are wrong
if (response.status === 401) {
return false;
}
// Default to showing admin setup if we can't determine
return false;
} catch (error) {
console.error('Error checking first run:', error);
// If there's an error, show the admin setup to be safe
return true;
}
}
/**
* Redirect to main application
*/
function redirectToMainApp() {
window.location.href = '/';
}
/**
* Logout - clear tokens and redirect to login
*/
function logout() {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
localStorage.removeItem(TOKEN_EXPIRY_KEY);
localStorage.removeItem(USER_DATA_KEY);
window.location.href = '/login.html';
}
+29
View File
@@ -66,5 +66,34 @@
},
"breadcrumb": {
"home": "Home"
},
"auth": {
"login_title": "Sign in",
"username": "Username",
"username_placeholder": "Enter your username",
"password": "Password",
"password_placeholder": "Enter your password",
"login_button": "Sign in",
"no_account": "Don't have an account?",
"register": "Sign up",
"admin_setup": "First time?",
"setup": "Setup administrator",
"register_title": "Create account",
"email": "Email",
"email_placeholder": "Enter your email",
"confirm_password": "Confirm password",
"confirm_password_placeholder": "Confirm your password",
"register_button": "Create account",
"have_account": "Already have an account?",
"login": "Sign in",
"setup_title": "Initial setup",
"setup_step1": "Admin",
"setup_step2": "System",
"setup_step3": "Complete",
"admin_username": "Admin username",
"admin_email": "Admin email",
"admin_password": "Admin password",
"create_admin": "Create administrator",
"back_to_login": "Already set up?"
}
}
+29
View File
@@ -66,5 +66,34 @@
},
"breadcrumb": {
"home": "Inicio"
},
"auth": {
"login_title": "Iniciar sesión",
"username": "Usuario",
"username_placeholder": "Ingresa tu nombre de usuario",
"password": "Contraseña",
"password_placeholder": "Ingresa tu contraseña",
"login_button": "Iniciar sesión",
"no_account": "¿No tienes cuenta?",
"register": "Regístrate",
"admin_setup": "¿Primera vez?",
"setup": "Configurar administrador",
"register_title": "Crear cuenta",
"email": "Email",
"email_placeholder": "Ingresa tu email",
"confirm_password": "Confirmar contraseña",
"confirm_password_placeholder": "Confirma tu contraseña",
"register_button": "Crear cuenta",
"have_account": "¿Ya tienes cuenta?",
"login": "Iniciar sesión",
"setup_title": "Configuración inicial",
"setup_step1": "Admin",
"setup_step2": "Sistema",
"setup_step3": "Completado",
"admin_username": "Usuario administrador",
"admin_email": "Email administrador",
"admin_password": "Contraseña administrador",
"create_admin": "Crear administrador",
"back_to_login": "¿Ya está configurado?"
}
}
+237
View File
@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="app.title">OxiCloud - Login</title>
<!-- Styles -->
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/auth.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<!-- Scripts -->
<script src="/js/i18n.js"></script>
<script src="/js/auth.js" defer></script>
</head>
<body>
<div class="auth-container">
<div class="auth-panel" id="login-panel">
<div class="auth-logo">
<div class="auth-logo-icon">
<svg viewBox="0 0 500 500">
<path d="M345 310c32 0 58-26 58-58s-26-58-58-58c-6.2 0-12 0.9-17.5 2.7C318 166 289 143 255 143c-34.3 0-63.1 22.6-73 53.7C176.9 195.7 171 195 165 195c-32 0-58 26-58 58s26 58 58 58h180z" fill="#fff"/>
</svg>
</div>
<div class="auth-logo-text">OxiCloud</div>
</div>
<h2 class="auth-title" data-i18n="auth.login_title">Iniciar sesión</h2>
<div class="auth-error" id="login-error"></div>
<form class="auth-form" id="login-form">
<div class="auth-input-group">
<label class="auth-label" for="login-username" data-i18n="auth.username">Usuario</label>
<input
type="text"
id="login-username"
class="auth-input"
data-i18n-placeholder="auth.username_placeholder"
placeholder="Ingresa tu nombre de usuario"
required
>
</div>
<div class="auth-input-group">
<label class="auth-label" for="login-password" data-i18n="auth.password">Contraseña</label>
<input
type="password"
id="login-password"
class="auth-input"
data-i18n-placeholder="auth.password_placeholder"
placeholder="Ingresa tu contraseña"
required
>
</div>
<button type="submit" class="auth-button" data-i18n="auth.login_button">Iniciar sesión</button>
</form>
<div class="auth-toggle">
<span data-i18n="auth.no_account">¿No tienes cuenta?</span>
<span class="auth-toggle-link" id="show-register" data-i18n="auth.register">Regístrate</span>
</div>
<div class="auth-toggle">
<span data-i18n="auth.admin_setup">¿Primera vez?</span>
<span class="auth-toggle-link" id="show-admin-setup" data-i18n="auth.setup">Configurar administrador</span>
</div>
</div>
<div class="auth-panel" id="register-panel" style="display: none;">
<div class="auth-logo">
<div class="auth-logo-icon">
<svg viewBox="0 0 500 500">
<path d="M345 310c32 0 58-26 58-58s-26-58-58-58c-6.2 0-12 0.9-17.5 2.7C318 166 289 143 255 143c-34.3 0-63.1 22.6-73 53.7C176.9 195.7 171 195 165 195c-32 0-58 26-58 58s26 58 58 58h180z" fill="#fff"/>
</svg>
</div>
<div class="auth-logo-text">OxiCloud</div>
</div>
<h2 class="auth-title" data-i18n="auth.register_title">Crear cuenta</h2>
<div class="auth-error" id="register-error"></div>
<div class="auth-success" id="register-success"></div>
<form class="auth-form" id="register-form">
<div class="auth-input-group">
<label class="auth-label" for="register-username" data-i18n="auth.username">Usuario</label>
<input
type="text"
id="register-username"
class="auth-input"
data-i18n-placeholder="auth.username_placeholder"
placeholder="Ingresa un nombre de usuario"
required
minlength="3"
maxlength="32"
>
</div>
<div class="auth-input-group">
<label class="auth-label" for="register-email" data-i18n="auth.email">Email</label>
<input
type="email"
id="register-email"
class="auth-input"
data-i18n-placeholder="auth.email_placeholder"
placeholder="Ingresa tu email"
required
>
</div>
<div class="auth-input-group">
<label class="auth-label" for="register-password" data-i18n="auth.password">Contraseña</label>
<input
type="password"
id="register-password"
class="auth-input"
data-i18n-placeholder="auth.password_placeholder"
placeholder="Ingresa una contraseña segura"
required
minlength="8"
>
</div>
<div class="auth-input-group">
<label class="auth-label" for="register-password-confirm" data-i18n="auth.confirm_password">Confirmar contraseña</label>
<input
type="password"
id="register-password-confirm"
class="auth-input"
data-i18n-placeholder="auth.confirm_password_placeholder"
placeholder="Confirma tu contraseña"
required
>
</div>
<button type="submit" class="auth-button" data-i18n="auth.register_button">Crear cuenta</button>
</form>
<div class="auth-toggle">
<span data-i18n="auth.have_account">¿Ya tienes cuenta?</span>
<span class="auth-toggle-link" id="show-login" data-i18n="auth.login">Iniciar sesión</span>
</div>
</div>
<div class="auth-panel admin-setup-panel" id="admin-setup-panel">
<div class="auth-logo">
<div class="auth-logo-icon">
<svg viewBox="0 0 500 500">
<path d="M345 310c32 0 58-26 58-58s-26-58-58-58c-6.2 0-12 0.9-17.5 2.7C318 166 289 143 255 143c-34.3 0-63.1 22.6-73 53.7C176.9 195.7 171 195 165 195c-32 0-58 26-58 58s26 58 58 58h180z" fill="#fff"/>
</svg>
</div>
<div class="auth-logo-text">OxiCloud</div>
</div>
<h2 class="auth-title" data-i18n="auth.setup_title">Configuración inicial</h2>
<div class="setup-steps">
<div class="setup-step">
<div class="step-number active">1</div>
<div class="step-title active" data-i18n="auth.setup_step1">Admin</div>
</div>
<div class="setup-step">
<div class="step-number">2</div>
<div class="step-title" data-i18n="auth.setup_step2">Sistema</div>
</div>
<div class="setup-step">
<div class="step-number">3</div>
<div class="step-title" data-i18n="auth.setup_step3">Completado</div>
</div>
</div>
<div class="auth-error" id="admin-setup-error"></div>
<form class="auth-form" id="admin-setup-form">
<div class="auth-input-group">
<label class="auth-label" for="admin-username" data-i18n="auth.admin_username">Usuario administrador</label>
<input
type="text"
id="admin-username"
class="auth-input"
value="admin"
readonly
>
</div>
<div class="auth-input-group">
<label class="auth-label" for="admin-email" data-i18n="auth.admin_email">Email administrador</label>
<input
type="email"
id="admin-email"
class="auth-input"
data-i18n-placeholder="auth.email_placeholder"
placeholder="Ingresa el email del administrador"
required
>
</div>
<div class="auth-input-group">
<label class="auth-label" for="admin-password" data-i18n="auth.admin_password">Contraseña administrador</label>
<input
type="password"
id="admin-password"
class="auth-input"
data-i18n-placeholder="auth.password_placeholder"
placeholder="Ingresa una contraseña segura"
required
minlength="8"
>
</div>
<div class="auth-input-group">
<label class="auth-label" for="admin-password-confirm" data-i18n="auth.confirm_password">Confirmar contraseña</label>
<input
type="password"
id="admin-password-confirm"
class="auth-input"
data-i18n-placeholder="auth.confirm_password_placeholder"
placeholder="Confirma la contraseña"
required
>
</div>
<button type="submit" class="auth-button" data-i18n="auth.create_admin">Crear administrador</button>
</form>
<div class="auth-toggle">
<span data-i18n="auth.back_to_login">¿Ya está configurado?</span>
<span class="auth-toggle-link" id="back-to-login" data-i18n="auth.login">Iniciar sesión</span>
</div>
</div>
</div>
</body>
</html>