15 KiB
15 KiB
Authentication Views Usage Examples
This document provides practical examples of how to use the authentication views in the Sub2API frontend.
Quick Start
1. Login Flow
Scenario: User wants to log into their existing account
// Route: /login
// Component: LoginView.vue
// User interactions:
// 1. Navigate to /login
// 2. Enter username: "john_doe"
// 3. Enter password: "MySecurePass123"
// 4. Optionally check "Remember me"
// 5. Click "Sign In"
// What happens:
// - Form validation runs (client-side)
// - If valid, authStore.login() is called
// - API request to POST /api/auth/login
// - On success:
// - Token stored in localStorage
// - User data stored in state
// - Success toast: "Login successful! Welcome back."
// - Redirect to /dashboard (or intended route)
// - On error:
// - Error message displayed inline
// - Error toast shown
// - User can retry
2. Registration Flow
Scenario: New user wants to create an account
// Route: /register
// Component: RegisterView.vue
// User interactions:
// 1. Navigate to /register
// 2. Enter username: "jane_smith"
// 3. Enter email: "jane@example.com"
// 4. Enter password: "SecurePass123"
// 5. Enter confirm password: "SecurePass123"
// 6. Click "Create Account"
// What happens:
// - Form validation runs (client-side)
// - Username: 3-50 chars, alphanumeric + _ -
// - Email: Valid format
// - Password: 8+ chars, letters + numbers
// - Passwords match
// - If valid, authStore.register() is called
// - API request to POST /api/auth/register
// - On success:
// - Token stored in localStorage
// - User data stored in state
// - Success toast: "Account created successfully! Welcome to Sub2API."
// - Redirect to /dashboard
// - On error:
// - Error message displayed inline
// - Error toast shown
// - User can retry
Code Examples
Importing the Views
// Method 1: Direct import
import LoginView from '@/views/auth/LoginView.vue';
import RegisterView from '@/views/auth/RegisterView.vue';
// Method 2: Named exports from index
import { LoginView, RegisterView } from '@/views/auth';
// Method 3: Lazy loading (recommended for routes)
const LoginView = () => import('@/views/auth/LoginView.vue');
const RegisterView = () => import('@/views/auth/RegisterView.vue');
Using in Router
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/auth/LoginView.vue'),
meta: { requiresAuth: false },
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/auth/RegisterView.vue'),
meta: { requiresAuth: false },
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
Navigation to Auth Views
// From template
<router-link to="/login">Login</router-link>
<router-link to="/register">Sign Up</router-link>
// From script
import { useRouter } from 'vue-router';
const router = useRouter();
// Navigate to login
router.push('/login');
// Navigate to register
router.push('/register');
// Navigate with redirect query
router.push({
path: '/login',
query: { redirect: '/dashboard' }
});
Programmatic Auth Flow
import { useAuthStore } from '@/stores';
import { useAppStore } from '@/stores';
import { useRouter } from 'vue-router';
const authStore = useAuthStore();
const appStore = useAppStore();
const router = useRouter();
// Login
async function login() {
try {
await authStore.login({
username: 'john_doe',
password: 'MySecurePass123'
});
appStore.showSuccess('Login successful!');
router.push('/dashboard');
} catch (error) {
appStore.showError('Login failed. Please check your credentials.');
}
}
// Register
async function register() {
try {
await authStore.register({
username: 'jane_smith',
email: 'jane@example.com',
password: 'SecurePass123'
});
appStore.showSuccess('Account created successfully!');
router.push('/dashboard');
} catch (error) {
appStore.showError('Registration failed. Please try again.');
}
}
Validation Examples
Login Validation
// Valid inputs
✅ Username: "john_doe" (3+ chars)
✅ Password: "SecurePass123" (6+ chars)
// Invalid inputs
❌ Username: "jo" → Error: "Username must be at least 3 characters"
❌ Password: "12345" → Error: "Password must be at least 6 characters"
❌ Username: "" → Error: "Username is required"
❌ Password: "" → Error: "Password is required"
Registration Validation
// Valid inputs
✅ Username: "jane_smith" (3-50 chars, alphanumeric + _ -)
✅ Email: "jane@example.com" (valid format)
✅ Password: "SecurePass123" (8+ chars, letters + numbers)
✅ Confirm: "SecurePass123" (matches password)
// Invalid inputs
❌ Username: "ja" → Error: "Username must be at least 3 characters"
❌ Username: "jane@smith" → Error: "Username can only contain letters, numbers, underscores, and hyphens"
❌ Email: "invalid-email" → Error: "Please enter a valid email address"
❌ Password: "short" → Error: "Password must be at least 8 characters with letters and numbers"
❌ Password: "12345678" → Error: "Password must be at least 8 characters with letters and numbers" (no letters)
❌ Password: "password" → Error: "Password must be at least 8 characters with letters and numbers" (no numbers)
❌ Confirm: "DifferentPass" → Error: "Passwords do not match"
Error Handling Examples
Backend Errors
// Example 1: Username already exists
{
response: {
data: {
detail: "Username 'john_doe' is already taken"
}
}
}
// Displayed: "Username 'john_doe' is already taken"
// Example 2: Invalid credentials
{
response: {
data: {
detail: "Invalid username or password"
}
}
}
// Displayed: "Invalid username or password"
// Example 3: Network error
{
message: "Network Error"
}
// Displayed: "Network Error" + Error toast
// Example 4: Unknown error
{}
// Displayed: "Login failed. Please check your credentials and try again." (default)
Client-side Validation Errors
// Multiple validation errors displayed simultaneously
errors = {
username: "Username must be at least 3 characters",
email: "Please enter a valid email address",
password: "Password must be at least 8 characters with letters and numbers",
confirmPassword: "Passwords do not match"
}
// Each error appears below its respective input field with red styling
Testing Examples
Unit Test: Login View
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { createPinia } from 'pinia';
import LoginView from '@/views/auth/LoginView.vue';
describe('LoginView', () => {
it('validates required fields', async () => {
const wrapper = mount(LoginView, {
global: {
plugins: [createPinia()],
},
});
// Submit empty form
await wrapper.find('form').trigger('submit');
// Check for validation errors
expect(wrapper.text()).toContain('Username is required');
expect(wrapper.text()).toContain('Password is required');
});
it('calls authStore.login on valid submission', async () => {
const wrapper = mount(LoginView, {
global: {
plugins: [createPinia()],
},
});
// Fill in form
await wrapper.find('#username').setValue('john_doe');
await wrapper.find('#password').setValue('SecurePass123');
// Submit form
await wrapper.find('form').trigger('submit');
// Verify authStore.login was called
// (mock implementation needed)
});
});
E2E Test: Registration Flow
import { test, expect } from '@playwright/test';
test('user can register successfully', async ({ page }) => {
// Navigate to register page
await page.goto('/register');
// Fill in registration form
await page.fill('#username', 'new_user');
await page.fill('#email', 'new_user@example.com');
await page.fill('#password', 'SecurePass123');
await page.fill('#confirmPassword', 'SecurePass123');
// Submit form
await page.click('button[type="submit"]');
// Wait for redirect to dashboard
await page.waitForURL('/dashboard');
// Verify success toast appears
await expect(page.locator('.toast-success')).toBeVisible();
await expect(page.locator('.toast-success')).toContainText('Account created successfully');
});
test('shows validation errors for invalid inputs', async ({ page }) => {
await page.goto('/register');
// Enter mismatched passwords
await page.fill('#password', 'SecurePass123');
await page.fill('#confirmPassword', 'DifferentPass');
// Submit form
await page.click('button[type="submit"]');
// Verify error message
await expect(page.locator('text=Passwords do not match')).toBeVisible();
});
Integration with Navigation Guards
Router Guard Example
import { useAuthStore } from '@/stores';
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
// Redirect authenticated users away from auth pages
if (authStore.isAuthenticated && (to.path === '/login' || to.path === '/register')) {
next('/dashboard');
return;
}
// Redirect unauthenticated users to login
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next({
path: '/login',
query: { redirect: to.fullPath }
});
return;
}
next();
});
Customization Examples
Custom Success Redirect
// In LoginView.vue
async function handleLogin(): Promise<void> {
try {
await authStore.login({
username: formData.username,
password: formData.password,
});
appStore.showSuccess('Login successful!');
// Custom redirect logic
const isAdmin = authStore.isAdmin;
const redirectTo = isAdmin ? '/admin/dashboard' : '/dashboard';
await router.push(redirectTo);
} catch (error) {
// Error handling...
}
}
Custom Validation Rules
// Custom password strength validation
function validatePasswordStrength(password: string): boolean {
const hasMinLength = password.length >= 12;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return hasMinLength && hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar;
}
// Use in validation
if (!validatePasswordStrength(formData.password)) {
errors.password = 'Password must be at least 12 characters with uppercase, lowercase, numbers, and special characters';
isValid = false;
}
Custom Error Handling
// In RegisterView.vue
async function handleRegister(): Promise<void> {
try {
await authStore.register({
username: formData.username,
email: formData.email,
password: formData.password,
});
appStore.showSuccess('Account created successfully!');
await router.push('/dashboard');
} catch (error: unknown) {
const err = error as { response?: { status?: number; data?: { detail?: string } } };
// Custom error handling based on status code
if (err.response?.status === 409) {
errorMessage.value = 'This username or email is already registered. Please use a different one.';
} else if (err.response?.status === 422) {
errorMessage.value = 'Invalid input. Please check your information and try again.';
} else if (err.response?.status === 500) {
errorMessage.value = 'Server error. Please try again later.';
} else {
errorMessage.value = err.response?.data?.detail || 'Registration failed. Please try again.';
}
appStore.showError(errorMessage.value);
}
}
Accessibility Examples
Keyboard Navigation
// Tab order:
// 1. Username input
// 2. Password input
// 3. Remember me checkbox (login) / Confirm password (register)
// 4. Submit button
// 5. Footer link (register/login)
// Enter key submits form
// Escape key can be used to clear focus
Screen Reader Support
<!-- Proper labels for screen readers -->
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">
Username
</label>
<input
id="username"
type="text"
aria-label="Username"
aria-required="true"
aria-invalid="false"
aria-describedby="username-error"
/>
<p id="username-error" role="alert" class="text-sm text-red-600">
<!-- Error message here -->
</p>
<!-- Loading state announced -->
<button type="submit" aria-busy="true" aria-label="Signing in...">
<span class="sr-only">Signing in...</span>
<!-- Visual content -->
</button>
Performance Considerations
Lazy Loading
// Router configuration with lazy loading
{
path: '/login',
component: () => import('@/views/auth/LoginView.vue'), // ✅ Lazy loaded
}
// Direct import (not recommended for routes)
import LoginView from '@/views/auth/LoginView.vue'; // ❌ Eager loaded
Optimization Tips
- Use
v-oncefor static content - Debounce expensive validation operations
- Minimize reactive dependencies
- Use
shallowReffor complex objects when possible - Avoid unnecessary watchers
Security Best Practices
- Never log passwords or tokens
- Use HTTPS in production
- Implement rate limiting on backend
- Validate all inputs server-side
- Use secure password hashing (bcrypt, argon2)
- Implement CSRF protection
- Set secure cookie flags
- Use Content Security Policy headers
- Sanitize all user inputs
- Implement account lockout after failed attempts
Common Issues and Solutions
Issue: Token not persisting after refresh
// Solution: Initialize auth state on app mount
// In main.ts or App.vue
import { useAuthStore } from '@/stores';
const authStore = useAuthStore();
authStore.checkAuth(); // Restore auth from localStorage
Issue: Redirect loop after login
// Solution: Check router guard logic
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
// ✅ Correct: Check specific routes
if (authStore.isAuthenticated && (to.path === '/login' || to.path === '/register')) {
next('/dashboard');
return;
}
// ❌ Wrong: Blanket redirect
// if (authStore.isAuthenticated) {
// next('/dashboard'); // This causes loops!
// }
next();
});
Issue: Form not clearing after successful submission
// Solution: Reset form data
async function handleLogin(): Promise<void> {
try {
await authStore.login({...});
// Reset form
formData.username = '';
formData.password = '';
formData.remember = false;
// Clear errors
errors.username = '';
errors.password = '';
await router.push('/dashboard');
} catch (error) {
// Error handling...
}
}