11 KiB
11 KiB
Frontend Integration Guide (Nuxt.js / Next.js)
🎯 Architecture
Frontend (Nuxt/Next.js) Backend (Django)
Port: 3000 Port: 8000
├── Pages/Routes ├── API Endpoints
├── UI/UX ├── Authentication
├── API Calls ├── Database
└── Token Storage └── Business Logic
📧 Email Links Flow
How It Works:
- User registers → Backend sends email
- Email contains → Frontend URL (http://localhost:3000/activate/...)
- User clicks link → Opens Frontend page
- Frontend JavaScript → Calls Backend API
- Backend → Activates account, returns response
- Frontend → Shows success message
Email Link Format:
Activation: http://localhost:3000/activate/{uid}/{token}/
Password Reset: http://localhost:3000/password-reset/{uid}/{token}/
🚀 Nuxt.js Implementation
1. Environment Variables (.env)
# Nuxt.js .env
NUXT_PUBLIC_API_BASE=http://localhost:8000/api/v1
2. Nuxt Config (nuxt.config.ts)
export default defineNuxtConfig({
runtimeConfig: {
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:8000/api/v1'
}
},
// CORS configuration for development
nitro: {
devProxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
})
3. API Composable (composables/useApi.ts)
export const useApi = () => {
const config = useRuntimeConfig()
const apiBase = config.public.apiBase
return {
apiBase,
async fetch(endpoint: string, options: any = {}) {
return await $fetch(`${apiBase}${endpoint}`, options)
}
}
}
4. Auth Composable (composables/useAuth.ts)
export const useAuth = () => {
const { apiBase } = useApi()
const router = useRouter()
// Register
const register = async (userData: {
email: string
password: string
re_password: string
first_name: string
last_name: string
}) => {
return await $fetch(`${apiBase}/auth/users/`, {
method: 'POST',
body: userData
})
}
// Activate Account
const activate = async (uid: string, token: string) => {
return await $fetch(`${apiBase}/auth/users/activation/`, {
method: 'POST',
body: { uid, token }
})
}
// Login
const login = async (email: string, password: string) => {
const data = await $fetch(`${apiBase}/auth/jwt/create/`, {
method: 'POST',
body: { email, password }
})
// Save tokens
localStorage.setItem('access_token', data.access)
localStorage.setItem('refresh_token', data.refresh)
return data
}
// Social Login
const socialLogin = async (provider: string, accessToken: string) => {
const data = await $fetch(`${apiBase}/auth/social/${provider}/`, {
method: 'POST',
body: { access_token: accessToken }
})
// Save JWT tokens
localStorage.setItem('access_token', data.access)
localStorage.setItem('refresh_token', data.refresh)
return data
}
// Get Current User
const getUser = async () => {
const token = localStorage.getItem('access_token')
if (!token) return null
return await $fetch(`${apiBase}/auth/users/me/`, {
headers: {
Authorization: `Bearer ${token}`
}
})
}
// Logout
const logout = () => {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
router.push('/login')
}
return {
register,
activate,
login,
socialLogin,
getUser,
logout
}
}
5. Activation Page (pages/activate/[uid]/[token].vue)
<template>
<div class="activation-page">
<div v-if="loading" class="loading">
<div class="spinner"></div>
<h1>Activating Your Account...</h1>
<p>Please wait while we activate your account.</p>
</div>
<div v-else-if="success" class="success">
<div class="icon">✅</div>
<h1>Account Activated!</h1>
<p>Your account has been successfully activated.</p>
<NuxtLink to="/login" class="btn">Go to Login</NuxtLink>
</div>
<div v-else class="error">
<div class="icon">❌</div>
<h1>Activation Failed</h1>
<p>{{ error }}</p>
<NuxtLink to="/login" class="btn">Back to Login</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { activate } = useAuth()
const loading = ref(true)
const success = ref(false)
const error = ref('')
onMounted(async () => {
const uid = route.params.uid as string
const token = route.params.token as string
try {
await activate(uid, token)
success.value = true
} catch (e: any) {
error.value = e.data?.detail || e.data?.token?.[0] || 'Activation failed'
} finally {
loading.value = false
}
})
</script>
<style scoped>
.activation-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
margin: 0 auto 2rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
6. Register Page (pages/register.vue)
<template>
<div class="register-page">
<div class="card">
<h1>Create Account</h1>
<form @submit.prevent="handleRegister">
<input v-model="form.email" type="email" placeholder="Email" required />
<input v-model="form.first_name" placeholder="First Name" required />
<input v-model="form.last_name" placeholder="Last Name" required />
<input v-model="form.password" type="password" placeholder="Password" required />
<input v-model="form.re_password" type="password" placeholder="Confirm Password" required />
<button type="submit">Register</button>
</form>
<div v-if="registered" class="success">
✅ Registration successful! Please check your email to activate your account.
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</div>
</template>
<script setup lang="ts">
const { register } = useAuth()
const form = ref({
email: '',
password: '',
re_password: '',
first_name: '',
last_name: ''
})
const registered = ref(false)
const error = ref('')
const handleRegister = async () => {
try {
await register(form.value)
registered.value = true
} catch (e: any) {
error.value = Object.values(e.data).flat().join(', ')
}
}
</script>
7. Login Page (pages/login.vue)
<template>
<div class="login-page">
<div class="card">
<h1>Login</h1>
<form @submit.prevent="handleLogin">
<input v-model="email" type="email" placeholder="Email" required />
<input v-model="password" type="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
<div class="divider">OR</div>
<button @click="loginWithGoogle" class="btn-google">
Continue with Google
</button>
<div v-if="error" class="error">{{ error }}</div>
</div>
</div>
</template>
<script setup lang="ts">
const { login, socialLogin } = useAuth()
const router = useRouter()
const email = ref('')
const password = ref('')
const error = ref('')
const handleLogin = async () => {
try {
await login(email.value, password.value)
router.push('/dashboard')
} catch (e: any) {
error.value = e.data?.detail || 'Login failed'
}
}
const loginWithGoogle = async () => {
// Implement Google OAuth (use @nuxtjs/google-oauth2 or similar)
const googleToken = await getGoogleAccessToken()
try {
await socialLogin('google-oauth2', googleToken)
router.push('/dashboard')
} catch (e: any) {
error.value = e.data?.error || 'Social login failed'
}
}
</script>
🔐 Protected Pages (Middleware)
Auth Middleware (middleware/auth.ts)
export default defineNuxtRouteMiddleware((to, from) => {
const token = process.client ? localStorage.getItem('access_token') : null
if (!token) {
return navigateTo('/login')
}
})
Dashboard Page (pages/dashboard.vue)
<template>
<div class="dashboard">
<h1>Welcome, {{ user?.first_name }}!</h1>
<p>Email: {{ user?.email }}</p>
<button @click="logout">Logout</button>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
const { getUser, logout } = useAuth()
const user = ref(null)
onMounted(async () => {
user.value = await getUser()
})
</script>
🌐 Next.js Implementation
Very similar to Nuxt.js, just adjust the syntax:
// app/activate/[uid]/[token]/page.tsx
'use client'
import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
export default function ActivatePage() {
const params = useParams()
const [loading, setLoading] = useState(true)
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
const activate = async () => {
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE}/auth/users/activation/`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
uid: params.uid,
token: params.token
})
}
)
if (response.ok) {
setSuccess(true)
} else {
const data = await response.json()
setError(data.detail || 'Activation failed')
}
} catch (e) {
setError('Network error')
} finally {
setLoading(false)
}
}
activate()
}, [params])
if (loading) return <div>Activating...</div>
if (success) return <div>✅ Account Activated!</div>
return <div>❌ {error}</div>
}
📝 Summary
Email Links:
- Activation:
http://localhost:3000/activate/{uid}/{token}/ - Password Reset:
http://localhost:3000/password-reset/{uid}/{token}/
API Endpoints (Backend):
- Register:
POST http://localhost:8000/api/v1/auth/users/ - Activate:
POST http://localhost:8000/api/v1/auth/users/activation/ - Login:
POST http://localhost:8000/api/v1/auth/jwt/create/ - Social Login:
POST http://localhost:8000/api/v1/auth/social/{provider}/ - Current User:
GET http://localhost:8000/api/v1/auth/users/me/
Production URLs:
- Frontend:
https://yourdomain.com - Backend:
https://api.yourdomain.com
Update DOMAIN in Django settings for production!
Happy Coding! 🚀