phase 3.1
This commit is contained in:
35
frontend/src/hooks.server.ts
Normal file
35
frontend/src/hooks.server.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* SvelteKit server hooks for route protection
|
||||
*/
|
||||
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
// Protected routes that require authentication
|
||||
const protectedRoutes = ['/boards', '/library', '/settings'];
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const { url, cookies } = event;
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Check if route requires authentication
|
||||
const requiresAuth = protectedRoutes.some(route => pathname.startsWith(route));
|
||||
|
||||
if (requiresAuth) {
|
||||
// Check for auth token in cookies (or you could check localStorage via client-side)
|
||||
const authToken = cookies.get('auth_token');
|
||||
|
||||
if (!authToken) {
|
||||
// Redirect to login if not authenticated
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
location: `/login?redirect=${encodeURIComponent(pathname)}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const response = await resolve(event);
|
||||
return response;
|
||||
};
|
||||
|
||||
114
frontend/src/routes/login/+page.svelte
Normal file
114
frontend/src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authApi } from '$lib/api/auth';
|
||||
import type { ApiError } from '$lib/api/client';
|
||||
import LoginForm from '$lib/components/auth/LoginForm.svelte';
|
||||
import { authStore } from '$lib/stores/auth';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let error: string = '';
|
||||
let isLoading = false;
|
||||
|
||||
onMount(() => {
|
||||
// Redirect if already authenticated
|
||||
authStore.subscribe(state => {
|
||||
if (state.isAuthenticated) {
|
||||
goto('/boards');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function handleLogin(event: CustomEvent<{ email: string; password: string }>) {
|
||||
const { email, password } = event.detail;
|
||||
error = '';
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const response = await authApi.login({ email, password });
|
||||
authStore.login(response.user, response.access_token);
|
||||
goto('/boards');
|
||||
} catch (err) {
|
||||
const apiError = err as ApiError;
|
||||
error = apiError.error || 'Login failed. Please try again.';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<h1>Login to Reference Board Viewer</h1>
|
||||
|
||||
{#if error}
|
||||
<div class="error-message" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<LoginForm on:submit={handleLogin} {isLoading} />
|
||||
|
||||
<div class="register-link">
|
||||
<p>Don't have an account? <a href="/register">Register here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.login-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
padding: 2.5rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background-color: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 6px;
|
||||
color: #c33;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.register-link p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.register-link a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.register-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
143
frontend/src/routes/register/+page.svelte
Normal file
143
frontend/src/routes/register/+page.svelte
Normal file
@@ -0,0 +1,143 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authApi } from '$lib/api/auth';
|
||||
import type { ApiError } from '$lib/api/client';
|
||||
import RegisterForm from '$lib/components/auth/RegisterForm.svelte';
|
||||
import { authStore } from '$lib/stores/auth';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let error: string = '';
|
||||
let success: string = '';
|
||||
let isLoading = false;
|
||||
|
||||
onMount(() => {
|
||||
// Redirect if already authenticated
|
||||
authStore.subscribe(state => {
|
||||
if (state.isAuthenticated) {
|
||||
goto('/boards');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function handleRegister(event: CustomEvent<{ email: string; password: string }>) {
|
||||
const { email, password } = event.detail;
|
||||
error = '';
|
||||
success = '';
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
await authApi.register({ email, password });
|
||||
success = 'Registration successful! Redirecting to login...';
|
||||
|
||||
// Auto-login after successful registration
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const response = await authApi.login({ email, password });
|
||||
authStore.login(response.user, response.access_token);
|
||||
goto('/boards');
|
||||
} catch (loginErr) {
|
||||
// If auto-login fails, just redirect to login page
|
||||
goto('/login');
|
||||
}
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
const apiError = err as ApiError;
|
||||
error = apiError.error || apiError.detail || 'Registration failed. Please try again.';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="register-page">
|
||||
<div class="register-container">
|
||||
<h1>Create Your Account</h1>
|
||||
|
||||
{#if error}
|
||||
<div class="error-message" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<div class="success-message" role="status">
|
||||
{success}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<RegisterForm on:submit={handleRegister} {isLoading} />
|
||||
|
||||
<div class="login-link">
|
||||
<p>Already have an account? <a href="/login">Login here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.register-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.register-container {
|
||||
background: white;
|
||||
padding: 2.5rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background-color: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 6px;
|
||||
color: #c33;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background-color: #efe;
|
||||
border: 1px solid #cfc;
|
||||
border-radius: 6px;
|
||||
color: #3c3;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-link p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.login-link a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user