feat(auth): add user registration and response DTOs, implement registration endpoint in AuthService
feat(auth): create registration route and integrate with AuthService feat(auth): implement user registration logic in AuthService with Keycloak integration fix(config): add Keycloak admin credentials to settings fix(middleware): update tenant middleware to include new auth routes chore(docs): remove outdated testing guide and add project architecture documentation feat(frontend): implement user registration page and integrate with API feat(frontend): create login page with tenant selection and error handling refactor(frontend): update layout to use custom auth store and improve loading states
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { initKeycloak, authStore } from '$lib/auth';
|
||||
import { initAuth, authStore as customAuthStore } from '$lib/auth';
|
||||
import '../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
let initialized = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
// Inicializar Keycloak al cargar la aplicación
|
||||
await initKeycloak();
|
||||
// Usar auth-custom que es instantáneo (sin peticiones de red)
|
||||
await initAuth();
|
||||
initialized = true;
|
||||
});
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
{#if initialized && !$authStore.isLoading}
|
||||
{#if initialized}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { isAuthenticated, currentUser, login, logout } from '$lib/auth';
|
||||
import { isAuthenticated, currentUser, logout } from '$lib/auth';
|
||||
import { api } from '$lib/api';
|
||||
|
||||
let licenseInfo: any = $state(null);
|
||||
let loadingLicense = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
// Si está autenticado, cargar información de licencia
|
||||
// La autenticación ya se inicializó en +layout.svelte
|
||||
// Solo cargar información de licencia si está autenticado
|
||||
if ($isAuthenticated && $currentUser?.tenantId) {
|
||||
await loadLicenseInfo();
|
||||
loadLicenseInfo(); // Sin await - carga en background
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
await login();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
@@ -50,14 +51,14 @@
|
||||
</div>
|
||||
<button
|
||||
onclick={handleLogout}
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||
>
|
||||
Cerrar Sesión
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
onclick={handleLogin}
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Iniciar Sesión
|
||||
</button>
|
||||
@@ -81,9 +82,15 @@
|
||||
Ideal para maquilas, empresas IMMEX y agentes aduanales.
|
||||
</p>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a
|
||||
href="/register"
|
||||
class="rounded-md border-2 border-blue-600 px-6 py-3 text-base font-semibold text-blue-600 hover:bg-blue-50 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Registrarse
|
||||
</a>
|
||||
<button
|
||||
onclick={handleLogin}
|
||||
class="rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
class="rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Iniciar Sesión
|
||||
</button>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/auth';
|
||||
|
||||
onMount(async () => {
|
||||
// Esperar a que Keycloak procese el callback
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Redirigir al home
|
||||
if ($authStore.isAuthenticated) {
|
||||
goto('/');
|
||||
} else {
|
||||
goto('/?error=auth_failed');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"></div>
|
||||
<p class="text-gray-600">Procesando autenticación...</p>
|
||||
</div>
|
||||
</div>
|
||||
120
frontend/src/routes/login/+page.svelte
Normal file
120
frontend/src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { login } from '$lib/auth';
|
||||
|
||||
let username = $state('demo');
|
||||
let password = $state('demo123');
|
||||
let tenantSlug = $state('aduanasoft');
|
||||
let error = $state('');
|
||||
let loading = $state(false);
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = '';
|
||||
loading = true;
|
||||
|
||||
const result = await login({
|
||||
username,
|
||||
password,
|
||||
tenant_slug: tenantSlug
|
||||
});
|
||||
|
||||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
goto('/');
|
||||
} else {
|
||||
error = result.error || 'Error de autenticación';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Anexo76
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
Inicia sesión en tu cuenta
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="mt-8 space-y-6" onsubmit={handleSubmit}>
|
||||
{#if error}
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
{error}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="tenant-slug" class="sr-only">Tenant</label>
|
||||
<input
|
||||
id="tenant-slug"
|
||||
name="tenant"
|
||||
type="text"
|
||||
required
|
||||
bind:value={tenantSlug}
|
||||
disabled={loading}
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Tenant (empresa)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="username" class="sr-only">Usuario</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
bind:value={username}
|
||||
disabled={loading}
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Usuario"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="sr-only">Contraseña</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
bind:value={password}
|
||||
disabled={loading}
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Contraseña"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{#if loading}
|
||||
Iniciando sesión...
|
||||
{:else}
|
||||
Iniciar sesión
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-xs text-gray-500">
|
||||
Credenciales de prueba:<br />
|
||||
Usuario: demo | Contraseña: demo123 | Tenant: aduanasoft
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
237
frontend/src/routes/register/+page.svelte
Normal file
237
frontend/src/routes/register/+page.svelte
Normal file
@@ -0,0 +1,237 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '$lib/api';
|
||||
|
||||
let formData = $state({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
tenant_slug: 'aduanasoft' // Por defecto
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state('');
|
||||
let passwordError = $state('');
|
||||
|
||||
async function handleRegister(e: Event) {
|
||||
e.preventDefault();
|
||||
error = '';
|
||||
passwordError = '';
|
||||
|
||||
// Validar que las contraseñas coincidan
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
passwordError = 'Las contraseñas no coinciden';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar longitud de contraseña
|
||||
if (formData.password.length < 8) {
|
||||
passwordError = 'La contraseña debe tener al menos 8 caracteres';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const response = await api.auth.register({
|
||||
username: formData.username,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
first_name: formData.first_name,
|
||||
last_name: formData.last_name,
|
||||
tenant_slug: formData.tenant_slug
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
// Registro exitoso, redirigir al login
|
||||
alert(`¡Registro exitoso! Bienvenido ${response.data.username}`);
|
||||
goto('/login');
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al registrar usuario';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
goto('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-md">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900">Crear cuenta</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
¿Ya tienes una cuenta?
|
||||
<a href="/login" class="font-medium text-blue-600 hover:text-blue-500">
|
||||
Inicia sesión
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de registro -->
|
||||
<div class="mt-8">
|
||||
<div class="rounded-lg bg-white px-6 py-8 shadow">
|
||||
<form class="space-y-6" onsubmit={handleRegister}>
|
||||
<!-- Username -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">
|
||||
Usuario
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
bind:value={formData.username}
|
||||
required
|
||||
minlength="3"
|
||||
maxlength="50"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="usuario123"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
bind:value={formData.email}
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="usuario@ejemplo.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nombre -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="first_name" class="block text-sm font-medium text-gray-700">
|
||||
Nombre
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="first_name"
|
||||
bind:value={formData.first_name}
|
||||
required
|
||||
minlength="2"
|
||||
maxlength="50"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="Juan"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="last_name" class="block text-sm font-medium text-gray-700">
|
||||
Apellido
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="last_name"
|
||||
bind:value={formData.last_name}
|
||||
required
|
||||
minlength="2"
|
||||
maxlength="50"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="Pérez"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contraseña -->
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">
|
||||
Contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
bind:value={formData.password}
|
||||
required
|
||||
minlength="8"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Confirmar contraseña -->
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700">
|
||||
Confirmar contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
bind:value={formData.confirmPassword}
|
||||
required
|
||||
minlength="8"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
placeholder="Repite tu contraseña"
|
||||
/>
|
||||
{#if passwordError}
|
||||
<p class="mt-1 text-sm text-red-600">{passwordError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Tenant -->
|
||||
<div>
|
||||
<label for="tenant_slug" class="block text-sm font-medium text-gray-700">
|
||||
Empresa
|
||||
</label>
|
||||
<select
|
||||
id="tenant_slug"
|
||||
bind:value={formData.tenant_slug}
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
>
|
||||
<option value="aduanasoft">AduanaSoft</option>
|
||||
<!-- Agregar más tenants aquí -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Errores -->
|
||||
{#if error}
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<p class="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Botones -->
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleCancel}
|
||||
disabled={loading}
|
||||
class="flex-1 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="flex-1 rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Registrando...' : 'Registrarse'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Info adicional -->
|
||||
<div class="mt-6 border-t border-gray-200 pt-6">
|
||||
<p class="text-xs text-gray-500">
|
||||
Al registrarte, aceptas nuestros términos de servicio y política de privacidad.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user