2025-12-18 13:50:39 +08:00
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Method 1: Direct import
|
2025-12-25 08:41:36 -08:00
|
|
|
import LoginView from '@/views/auth/LoginView.vue'
|
|
|
|
|
import RegisterView from '@/views/auth/RegisterView.vue'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Method 2: Named exports from index
|
2025-12-25 08:41:36 -08:00
|
|
|
import { LoginView, RegisterView } from '@/views/auth'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Method 3: Lazy loading (recommended for routes)
|
2025-12-25 08:41:36 -08:00
|
|
|
const LoginView = () => import('@/views/auth/LoginView.vue')
|
|
|
|
|
const RegisterView = () => import('@/views/auth/RegisterView.vue')
|
2025-12-18 13:50:39 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Using in Router
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-25 08:41:36 -08:00
|
|
|
import { createRouter, createWebHistory } from 'vue-router'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
const routes = [
|
|
|
|
|
{
|
|
|
|
|
path: '/login',
|
|
|
|
|
name: 'Login',
|
|
|
|
|
component: () => import('@/views/auth/LoginView.vue'),
|
2025-12-25 08:41:36 -08:00
|
|
|
meta: { requiresAuth: false }
|
2025-12-18 13:50:39 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/register',
|
|
|
|
|
name: 'Register',
|
|
|
|
|
component: () => import('@/views/auth/RegisterView.vue'),
|
2025-12-25 08:41:36 -08:00
|
|
|
meta: { requiresAuth: false }
|
|
|
|
|
}
|
|
|
|
|
]
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
history: createWebHistory(),
|
2025-12-25 08:41:36 -08:00
|
|
|
routes
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
export default router
|
2025-12-18 13:50:39 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Navigation to Auth Views
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-25 08:41:36 -08:00
|
|
|
import { useAuthStore } from '@/stores'
|
|
|
|
|
import { useAppStore } from '@/stores'
|
|
|
|
|
import { useRouter } from 'vue-router'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
const appStore = useAppStore()
|
|
|
|
|
const router = useRouter()
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Login
|
|
|
|
|
async function login() {
|
|
|
|
|
try {
|
|
|
|
|
await authStore.login({
|
|
|
|
|
username: 'john_doe',
|
|
|
|
|
password: 'MySecurePass123'
|
2025-12-25 08:41:36 -08:00
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
appStore.showSuccess('Login successful!')
|
|
|
|
|
router.push('/dashboard')
|
2025-12-18 13:50:39 +08:00
|
|
|
} catch (error) {
|
2025-12-25 08:41:36 -08:00
|
|
|
appStore.showError('Login failed. Please check your credentials.')
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Register
|
|
|
|
|
async function register() {
|
|
|
|
|
try {
|
|
|
|
|
await authStore.register({
|
|
|
|
|
username: 'jane_smith',
|
|
|
|
|
email: 'jane@example.com',
|
|
|
|
|
password: 'SecurePass123'
|
2025-12-25 08:41:36 -08:00
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
appStore.showSuccess('Account created successfully!')
|
|
|
|
|
router.push('/dashboard')
|
2025-12-18 13:50:39 +08:00
|
|
|
} catch (error) {
|
2025-12-25 08:41:36 -08:00
|
|
|
appStore.showError('Registration failed. Please try again.')
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Validation Examples
|
|
|
|
|
|
|
|
|
|
### Login Validation
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 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: {
|
2025-12-25 08:41:36 -08:00
|
|
|
detail: 'Invalid username or password'
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Displayed: "Invalid username or password"
|
|
|
|
|
|
|
|
|
|
// Example 3: Network error
|
|
|
|
|
{
|
2025-12-25 08:41:36 -08:00
|
|
|
message: 'Network Error'
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
// Displayed: "Network Error" + Error toast
|
|
|
|
|
|
|
|
|
|
// Example 4: Unknown error
|
2025-12-25 08:41:36 -08:00
|
|
|
{
|
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
// Displayed: "Login failed. Please check your credentials and try again." (default)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Client-side Validation Errors
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Multiple validation errors displayed simultaneously
|
|
|
|
|
errors = {
|
2025-12-25 08:41:36 -08:00
|
|
|
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'
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Each error appears below its respective input field with red styling
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Testing Examples
|
|
|
|
|
|
|
|
|
|
### Unit Test: Login View
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-25 08:41:36 -08:00
|
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
|
|
|
import { mount } from '@vue/test-utils'
|
|
|
|
|
import { createPinia } from 'pinia'
|
|
|
|
|
import LoginView from '@/views/auth/LoginView.vue'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
describe('LoginView', () => {
|
|
|
|
|
it('validates required fields', async () => {
|
|
|
|
|
const wrapper = mount(LoginView, {
|
|
|
|
|
global: {
|
2025-12-25 08:41:36 -08:00
|
|
|
plugins: [createPinia()]
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Submit empty form
|
2025-12-25 08:41:36 -08:00
|
|
|
await wrapper.find('form').trigger('submit')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Check for validation errors
|
2025-12-25 08:41:36 -08:00
|
|
|
expect(wrapper.text()).toContain('Username is required')
|
|
|
|
|
expect(wrapper.text()).toContain('Password is required')
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
it('calls authStore.login on valid submission', async () => {
|
|
|
|
|
const wrapper = mount(LoginView, {
|
|
|
|
|
global: {
|
2025-12-25 08:41:36 -08:00
|
|
|
plugins: [createPinia()]
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Fill in form
|
2025-12-25 08:41:36 -08:00
|
|
|
await wrapper.find('#username').setValue('john_doe')
|
|
|
|
|
await wrapper.find('#password').setValue('SecurePass123')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Submit form
|
2025-12-25 08:41:36 -08:00
|
|
|
await wrapper.find('form').trigger('submit')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Verify authStore.login was called
|
|
|
|
|
// (mock implementation needed)
|
2025-12-25 08:41:36 -08:00
|
|
|
})
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### E2E Test: Registration Flow
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-25 08:41:36 -08:00
|
|
|
import { test, expect } from '@playwright/test'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
test('user can register successfully', async ({ page }) => {
|
|
|
|
|
// Navigate to register page
|
2025-12-25 08:41:36 -08:00
|
|
|
await page.goto('/register')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Fill in registration form
|
2025-12-25 08:41:36 -08:00
|
|
|
await page.fill('#username', 'new_user')
|
|
|
|
|
await page.fill('#email', 'new_user@example.com')
|
|
|
|
|
await page.fill('#password', 'SecurePass123')
|
|
|
|
|
await page.fill('#confirmPassword', 'SecurePass123')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Submit form
|
2025-12-25 08:41:36 -08:00
|
|
|
await page.click('button[type="submit"]')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Wait for redirect to dashboard
|
2025-12-25 08:41:36 -08:00
|
|
|
await page.waitForURL('/dashboard')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Verify success toast appears
|
2025-12-25 08:41:36 -08:00
|
|
|
await expect(page.locator('.toast-success')).toBeVisible()
|
|
|
|
|
await expect(page.locator('.toast-success')).toContainText('Account created successfully')
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
test('shows validation errors for invalid inputs', async ({ page }) => {
|
2025-12-25 08:41:36 -08:00
|
|
|
await page.goto('/register')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Enter mismatched passwords
|
2025-12-25 08:41:36 -08:00
|
|
|
await page.fill('#password', 'SecurePass123')
|
|
|
|
|
await page.fill('#confirmPassword', 'DifferentPass')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Submit form
|
2025-12-25 08:41:36 -08:00
|
|
|
await page.click('button[type="submit"]')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Verify error message
|
2025-12-25 08:41:36 -08:00
|
|
|
await expect(page.locator('text=Passwords do not match')).toBeVisible()
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Integration with Navigation Guards
|
|
|
|
|
|
|
|
|
|
### Router Guard Example
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-25 08:41:36 -08:00
|
|
|
import { useAuthStore } from '@/stores'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
router.beforeEach((to, from, next) => {
|
2025-12-25 08:41:36 -08:00
|
|
|
const authStore = useAuthStore()
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Redirect authenticated users away from auth pages
|
|
|
|
|
if (authStore.isAuthenticated && (to.path === '/login' || to.path === '/register')) {
|
2025-12-25 08:41:36 -08:00
|
|
|
next('/dashboard')
|
|
|
|
|
return
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Redirect unauthenticated users to login
|
|
|
|
|
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
|
|
|
next({
|
|
|
|
|
path: '/login',
|
|
|
|
|
query: { redirect: to.fullPath }
|
2025-12-25 08:41:36 -08:00
|
|
|
})
|
|
|
|
|
return
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
next()
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Customization Examples
|
|
|
|
|
|
|
|
|
|
### Custom Success Redirect
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// In LoginView.vue
|
|
|
|
|
async function handleLogin(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await authStore.login({
|
|
|
|
|
username: formData.username,
|
2025-12-25 08:41:36 -08:00
|
|
|
password: formData.password
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
appStore.showSuccess('Login successful!')
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Custom redirect logic
|
2025-12-25 08:41:36 -08:00
|
|
|
const isAdmin = authStore.isAdmin
|
|
|
|
|
const redirectTo = isAdmin ? '/admin/dashboard' : '/dashboard'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
await router.push(redirectTo)
|
2025-12-18 13:50:39 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
// Error handling...
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Custom Validation Rules
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Custom password strength validation
|
|
|
|
|
function validatePasswordStrength(password: string): boolean {
|
2025-12-25 08:41:36 -08:00
|
|
|
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)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
return hasMinLength && hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use in validation
|
|
|
|
|
if (!validatePasswordStrength(formData.password)) {
|
2025-12-25 08:41:36 -08:00
|
|
|
errors.password =
|
|
|
|
|
'Password must be at least 12 characters with uppercase, lowercase, numbers, and special characters'
|
|
|
|
|
isValid = false
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Custom Error Handling
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// In RegisterView.vue
|
|
|
|
|
async function handleRegister(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await authStore.register({
|
|
|
|
|
username: formData.username,
|
|
|
|
|
email: formData.email,
|
2025-12-25 08:41:36 -08:00
|
|
|
password: formData.password
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
appStore.showSuccess('Account created successfully!')
|
|
|
|
|
await router.push('/dashboard')
|
2025-12-18 13:50:39 +08:00
|
|
|
} catch (error: unknown) {
|
2025-12-25 08:41:36 -08:00
|
|
|
const err = error as { response?: { status?: number; data?: { detail?: string } } }
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// Custom error handling based on status code
|
|
|
|
|
if (err.response?.status === 409) {
|
2025-12-25 08:41:36 -08:00
|
|
|
errorMessage.value =
|
|
|
|
|
'This username or email is already registered. Please use a different one.'
|
2025-12-18 13:50:39 +08:00
|
|
|
} else if (err.response?.status === 422) {
|
2025-12-25 08:41:36 -08:00
|
|
|
errorMessage.value = 'Invalid input. Please check your information and try again.'
|
2025-12-18 13:50:39 +08:00
|
|
|
} else if (err.response?.status === 500) {
|
2025-12-25 08:41:36 -08:00
|
|
|
errorMessage.value = 'Server error. Please try again later.'
|
2025-12-18 13:50:39 +08:00
|
|
|
} else {
|
2025-12-25 08:41:36 -08:00
|
|
|
errorMessage.value = err.response?.data?.detail || 'Registration failed. Please try again.'
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
appStore.showError(errorMessage.value)
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Accessibility Examples
|
|
|
|
|
|
|
|
|
|
### Keyboard Navigation
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
<!-- Proper labels for screen readers -->
|
2025-12-25 08:41:36 -08:00
|
|
|
<label for="username" class="mb-1 block text-sm font-medium text-gray-700"> Username </label>
|
2025-12-18 13:50:39 +08:00
|
|
|
<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
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
1. Use `v-once` for static content
|
|
|
|
|
2. Debounce expensive validation operations
|
|
|
|
|
3. Minimize reactive dependencies
|
|
|
|
|
4. Use `shallowRef` for complex objects when possible
|
|
|
|
|
5. Avoid unnecessary watchers
|
|
|
|
|
|
|
|
|
|
## Security Best Practices
|
|
|
|
|
|
|
|
|
|
1. Never log passwords or tokens
|
|
|
|
|
2. Use HTTPS in production
|
|
|
|
|
3. Implement rate limiting on backend
|
|
|
|
|
4. Validate all inputs server-side
|
|
|
|
|
5. Use secure password hashing (bcrypt, argon2)
|
|
|
|
|
6. Implement CSRF protection
|
|
|
|
|
7. Set secure cookie flags
|
|
|
|
|
8. Use Content Security Policy headers
|
|
|
|
|
9. Sanitize all user inputs
|
|
|
|
|
10. Implement account lockout after failed attempts
|
|
|
|
|
|
|
|
|
|
## Common Issues and Solutions
|
|
|
|
|
|
|
|
|
|
### Issue: Token not persisting after refresh
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Solution: Initialize auth state on app mount
|
|
|
|
|
// In main.ts or App.vue
|
2025-12-25 08:41:36 -08:00
|
|
|
import { useAuthStore } from '@/stores'
|
2025-12-18 13:50:39 +08:00
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
authStore.checkAuth() // Restore auth from localStorage
|
2025-12-18 13:50:39 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Issue: Redirect loop after login
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Solution: Check router guard logic
|
|
|
|
|
router.beforeEach((to, from, next) => {
|
2025-12-25 08:41:36 -08:00
|
|
|
const authStore = useAuthStore()
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
// ✅ Correct: Check specific routes
|
|
|
|
|
if (authStore.isAuthenticated && (to.path === '/login' || to.path === '/register')) {
|
2025-12-25 08:41:36 -08:00
|
|
|
next('/dashboard')
|
|
|
|
|
return
|
2025-12-18 13:50:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ❌ Wrong: Blanket redirect
|
|
|
|
|
// if (authStore.isAuthenticated) {
|
|
|
|
|
// next('/dashboard'); // This causes loops!
|
|
|
|
|
// }
|
|
|
|
|
|
2025-12-25 08:41:36 -08:00
|
|
|
next()
|
|
|
|
|
})
|
2025-12-18 13:50:39 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Issue: Form not clearing after successful submission
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 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...
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Additional Resources
|
|
|
|
|
|
|
|
|
|
- [Vue 3 Documentation](https://vuejs.org/)
|
|
|
|
|
- [Vue Router Documentation](https://router.vuejs.org/)
|
|
|
|
|
- [Pinia Documentation](https://pinia.vuejs.org/)
|
|
|
|
|
- [TailwindCSS Documentation](https://tailwindcss.com/)
|
|
|
|
|
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
|