first commit
This commit is contained in:
475
FRONTEND_INTEGRATION.md
Normal file
475
FRONTEND_INTEGRATION.md
Normal file
@@ -0,0 +1,475 @@
|
||||
# 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:
|
||||
|
||||
1. **User registers** → Backend sends email
|
||||
2. **Email contains** → Frontend URL (http://localhost:3000/activate/...)
|
||||
3. **User clicks link** → Opens Frontend page
|
||||
4. **Frontend JavaScript** → Calls Backend API
|
||||
5. **Backend** → Activates account, returns response
|
||||
6. **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`)
|
||||
|
||||
```bash
|
||||
# Nuxt.js .env
|
||||
NUXT_PUBLIC_API_BASE=http://localhost:8000/api/v1
|
||||
```
|
||||
|
||||
### 2. Nuxt Config (`nuxt.config.ts`)
|
||||
|
||||
```typescript
|
||||
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`)
|
||||
|
||||
```typescript
|
||||
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`)
|
||||
|
||||
```typescript
|
||||
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`)
|
||||
|
||||
```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`)
|
||||
|
||||
```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`)
|
||||
|
||||
```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`)
|
||||
|
||||
```typescript
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const token = process.client ? localStorage.getItem('access_token') : null
|
||||
|
||||
if (!token) {
|
||||
return navigateTo('/login')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Dashboard Page (`pages/dashboard.vue`)
|
||||
|
||||
```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:
|
||||
|
||||
```typescript
|
||||
// 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! 🚀**
|
||||
|
||||
Reference in New Issue
Block a user