ns_projects_frontend_changes.md•20.9 kB
# 🚀 NS-PROJECTS.VERCEL.APP - FRONTEND CHANGES DOCUMENTATION
## 📋 प्रोजेक्ट ओवरव्यू
**Repository:** `Pritrj/saas-backend` (Next.js 14.2.33)
**Goal:** MCP Token Generation page add करना Railway MCP server के साथ integration के लिए
**Architecture:** Frontend (Vercel) + Backend (Railway MCP Server) - Microservices approach
---
## 🎯 चेंजेज सारांश
### 🔑 Key Information:
- **Railway MCP Server URL:** `https://mcp-server-railway-production.up.railway.app`
- **API Endpoint:** `/auth/token` (POST)
- **Test Credentials:** username=testuser, password=testuser123
- **Token Format:** JWT with custom expiry times
---
## 📁 FILES TO CREATE/MODIFY
### 1. **New Page Creation**
**File:** `app/mcp-token/page.tsx`
```typescript
import { Suspense } from 'react';
import TokenGenerationForm from '@/components/mcp/TokenForm';
import { cookies } from 'next/headers';
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { redirect } from 'next/navigation';
export default async function MCPPage() {
const supabase = createServerComponentClient({ cookies });
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
redirect('/auth/login');
}
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">
MCP Token Generation
</h1>
<p className="mt-2 text-gray-600">
Generate secure JWT tokens for MCP server access
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="space-y-6">
<TokenGenerationForm />
</div>
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow-sm border">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Recent Tokens
</h3>
<TokenDisplay />
</div>
</div>
</div>
</div>
</div>
);
}
function TokenDisplay() {
return (
<Suspense fallback={<div>Loading tokens...</div>}>
<div className="space-y-3">
<p className="text-sm text-gray-500">
Generated tokens will appear here
</p>
</div>
</Suspense>
);
}
```
### 2. **API Configuration**
**File:** `lib/api.ts` (CREATE NEW)
```typescript
// MCP API Configuration
const MCP_SERVER_URL = process.env.MCP_SERVER || 'https://mcp-server-railway-production.up.railway.app';
export interface TokenRequest {
username: string;
password: string;
expiry_hours: number;
}
export interface TokenResponse {
success: boolean;
token?: string;
message?: string;
expires_in?: number;
}
export class MCPApi {
static async generateToken(credentials: TokenRequest): Promise<TokenResponse> {
try {
const response = await fetch(`${MCP_SERVER_URL}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Token generation failed');
}
return data;
} catch (error) {
console.error('MCP API Error:', error);
throw error;
}
}
static validateToken(token: string): boolean {
// Basic JWT format validation
const parts = token.split('.');
return parts.length === 3;
}
static formatTokenForDisplay(token: string): string {
// Show first 8 and last 8 characters
if (token.length < 20) return token;
return `${token.substring(0, 8)}...${token.substring(token.length - 8)}`;
}
}
```
### 3. **TypeScript Types**
**File:** `lib/types/mcp.ts` (CREATE NEW)
```typescript
export interface MCPToken {
id: string;
token: string;
created_at: string;
expires_at: string;
is_active: boolean;
expires_in_hours: number;
formatted_token: string;
}
export interface TokenFormData {
username: string;
password: string;
expiry_hours: number;
}
export interface MCPServerResponse {
success: boolean;
token?: string;
message?: string;
expires_in?: number;
error?: string;
}
export const EXPIRY_OPTIONS = [
{ value: 1, label: '1 Hour' },
{ value: 6, label: '6 Hours' },
{ value: 24, label: '1 Day' },
{ value: 168, label: '7 Days' },
{ value: 720, label: '30 Days' },
] as const;
export type ExpiryOption = typeof EXPIRY_OPTIONS[number]['value'];
```
### 4. **Token Generation Form**
**File:** `components/mcp/TokenForm.tsx` (CREATE NEW)
```typescript
'use client';
import { useState } from 'react';
import { useSession } from '@supabase/auth-helpers-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Copy, Key, Clock, Loader2, CheckCircle, XCircle } from 'lucide-react';
import { MCPApi, TokenRequest, TokenResponse } from '@/lib/api';
import { EXPIRY_OPTIONS, ExpiryOption } from '@/lib/types/mcp';
interface TokenGenerationState {
loading: boolean;
error: string | null;
success: string | null;
token: string | null;
}
export default function TokenForm() {
const session = useSession();
const [formData, setFormData] = useState({
username: '',
password: '',
expiry_hours: 24 as ExpiryOption,
});
const [state, setState] = useState<TokenGenerationState>({
loading: false,
error: null,
success: null,
token: null,
});
const handleInputChange = (field: string, value: string | number) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear previous messages
if (state.error || state.success) {
setState(prev => ({ ...prev, error: null, success: null }));
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.username || !formData.password) {
setState(prev => ({ ...prev, error: 'Username and password are required' }));
return;
}
setState({ loading: true, error: null, success: null, token: null });
try {
const tokenRequest: TokenRequest = {
username: formData.username,
password: formData.password,
expiry_hours: formData.expiry_hours,
};
const response: TokenResponse = await MCPApi.generateToken(tokenRequest);
if (response.success && response.token) {
setState({
loading: false,
error: null,
success: `Token generated successfully! Expires in ${formData.expiry_hours} hours.`,
token: response.token,
});
} else {
throw new Error(response.message || 'Token generation failed');
}
} catch (error) {
setState({
loading: false,
error: error instanceof Error ? error.message : 'An error occurred',
success: null,
token: null,
});
}
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setState(prev => ({ ...prev, success: 'Token copied to clipboard!' }));
setTimeout(() => {
setState(prev => ({ ...prev, success: null }));
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="h-5 w-5" />
Generate MCP Token
</CardTitle>
<CardDescription>
Create JWT tokens for MCP server access with custom expiry times
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
type="text"
placeholder="Enter username"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
disabled={state.loading}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="Enter password"
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
disabled={state.loading}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="expiry">Token Expiry</Label>
<Select
value={formData.expiry_hours.toString()}
onValueChange={(value) => handleInputChange('expiry_hours', parseInt(value))}
disabled={state.loading}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{EXPIRY_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{state.error && (
<Alert variant="destructive">
<XCircle className="h-4 w-4" />
<AlertDescription>{state.error}</AlertDescription>
</Alert>
)}
{state.success && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>{state.success}</AlertDescription>
</Alert>
)}
{state.token && (
<div className="space-y-3">
<div className="p-3 bg-gray-50 rounded border">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Generated Token:</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => copyToClipboard(state.token!)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
<code className="text-xs break-all">
{MCPApi.formatTokenForDisplay(state.token)}
</code>
</div>
</div>
)}
<Button
type="submit"
className="w-full"
disabled={state.loading}
>
{state.loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating Token...
</>
) : (
<>
<Key className="mr-2 h-4 w-4" />
Generate Token
</>
)}
</Button>
</form>
</CardContent>
</Card>
);
}
```
### 5. **Token Display Component**
**File:** `components/mcp/TokenDisplay.tsx` (CREATE NEW)
```typescript
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Copy, Clock, Shield, Trash2 } from 'lucide-react';
import { MCPApi } from '@/lib/api';
import { formatDistanceToNow } from 'date-fns';
interface DisplayedToken {
id: string;
token: string;
created_at: string;
expires_at: string;
expires_in_hours: number;
is_active: boolean;
}
export default function TokenDisplay() {
const [tokens, setTokens] = useState<DisplayedToken[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Load tokens from localStorage or API
loadTokens();
}, []);
const loadTokens = () => {
try {
const storedTokens = localStorage.getItem('mcp_tokens');
if (storedTokens) {
const parsedTokens = JSON.parse(storedTokens);
setTokens(parsedTokens);
}
} catch (error) {
console.error('Error loading tokens:', error);
} finally {
setLoading(false);
}
};
const saveToken = (token: string, expiry_hours: number) => {
const newToken: DisplayedToken = {
id: Date.now().toString(),
token,
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + expiry_hours * 60 * 60 * 1000).toISOString(),
expires_in_hours: expiry_hours,
is_active: true,
};
const updatedTokens = [newToken, ...tokens].slice(0, 10); // Keep only last 10
setTokens(updatedTokens);
localStorage.setItem('mcp_tokens', JSON.stringify(updatedTokens));
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const isExpired = (expiresAt: string) => {
return new Date(expiresAt) < new Date();
};
const deleteToken = (id: string) => {
const updatedTokens = tokens.filter(token => token.id !== id);
setTokens(updatedTokens);
localStorage.setItem('mcp_tokens', JSON.stringify(updatedTokens));
};
if (loading) {
return (
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
</CardContent>
</Card>
);
}
if (tokens.length === 0) {
return (
<Card>
<CardContent className="p-6">
<div className="text-center text-gray-500">
<Shield className="mx-auto h-12 w-12 mb-4 opacity-50" />
<p>No tokens generated yet</p>
<p className="text-sm">Generate your first MCP token to see it here</p>
</div>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
{tokens.map((token) => (
<Card key={token.id} className={`${isExpired(token.expires_at) ? 'opacity-60' : ''}`}>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<code className="text-sm font-mono truncate">
{MCPApi.formatTokenForDisplay(token.token)}
</code>
<Badge variant={isExpired(token.expires_at) ? "destructive" : "default"}>
{isExpired(token.expires_at) ? "Expired" : "Active"}
</Badge>
</div>
<div className="flex items-center gap-4 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{isExpired(token.expires_at)
? `Expired ${formatDistanceToNow(new Date(token.expires_at), { addSuffix: true })}`
: `Expires ${formatDistanceToNow(new Date(token.expires_at), { addSuffix: true })}`
}
</div>
</div>
</div>
<div className="flex gap-2 ml-4">
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(token.token)}
>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => deleteToken(token.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
);
}
```
### 6. **Navigation Update**
**File:** `components/Navigation.tsx` (MODIFY EXISTING)
```typescript
// Add this item to the navigation menu items array:
{
name: 'MCP Token',
href: '/mcp-token',
icon: Key, // Import Key from lucide-react
current: pathname === '/mcp-token',
}
```
---
## 🛠️ IMPLEMENTATION STEPS
### Step 1: Environment Variables
Add to `.env.local`:
```bash
MCP_SERVER=https://mcp-server-railway-production.up.railway.app
```
### Step 2: Install Dependencies
```bash
npm install @supabase/auth-helpers-react date-fns
```
### Step 3: Import Required Icons
Ensure these are imported in respective components:
- `Key`, `Clock`, `Copy`, `Loader2`, `CheckCircle`, `XCircle` from `lucide-react`
### Step 4: Database Integration (Optional)
If you want to store tokens in Supabase database:
```sql
-- Create tokens table
CREATE TABLE mcp_tokens (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
token_hash TEXT NOT NULL,
token_preview TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
is_active BOOLEAN DEFAULT TRUE
);
```
### Step 5: Security Middleware
Add rate limiting for token generation (optional but recommended):
```typescript
// In lib/api.ts, add rate limiting logic
let rateLimitMap = new Map();
```
---
## 🔒 SECURITY CONSIDERATIONS
### 1. **Token Storage**
- Store tokens in localStorage with expiry
- Never log full tokens
- Show only preview in UI
### 2. **Rate Limiting**
- Implement rate limiting for API calls
- Prevent brute force attacks
### 3. **Input Validation**
- Validate all user inputs
- Sanitize username/password
- Check token format before display
### 4. **Error Handling**
- Don't expose sensitive information in errors
- Log errors server-side only
- Show generic error messages to users
---
## 🎨 UI/UX GUIDELINES
### 1. **Consistency**
- Use existing design system (shadcn/ui)
- Match current color scheme and typography
- Follow existing component patterns
### 2. **Accessibility**
- Proper ARIA labels
- Keyboard navigation support
- Screen reader compatibility
### 3. **Loading States**
- Show loading spinners during API calls
- Disable form during processing
- Clear feedback for all actions
### 4. **Error States**
- Clear error messages
- Retry functionality
- Graceful degradation
---
## 📊 API INTEGRATION DETAILS
### Request Format:
```json
{
"username": "testuser",
"password": "testuser123",
"expiry_hours": 24
}
```
### Success Response:
```json
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"message": "Token generated successfully",
"expires_in": 86400
}
```
### Error Response:
```json
{
"success": false,
"message": "Invalid credentials",
"error": "Unauthorized"
}
```
---
## ✅ TESTING CHECKLIST
### 1. **Functional Testing**
- [ ] Token generation with valid credentials
- [ ] Token generation with invalid credentials
- [ ] Different expiry time options
- [ ] Token copy to clipboard
- [ ] Token display and management
### 2. **Error Handling**
- [ ] Network error handling
- [ ] Invalid response handling
- [ ] Rate limiting response
- [ ] Form validation errors
### 3. **Security Testing**
- [ ] XSS prevention
- [ ] CSRF protection
- [ ] Input sanitization
- [ ] Token format validation
### 4. **UI/UX Testing**
- [ ] Responsive design
- [ ] Loading states
- [ ] Error messages
- [ ] Accessibility compliance
---
## 🚀 DEPLOYMENT NOTES
### 1. **Environment Variables**
Set `MCP_SERVER` in Vercel project settings
### 2. **CORS Configuration**
Ensure Railway MCP server allows Vercel domain
### 3. **Build Process**
All components use client-side rendering where needed
### 4. **Performance**
- Implement lazy loading for large token lists
- Use React.memo for expensive components
- Debounce API calls if needed
---
## 📞 SUPPORT INFORMATION
**Railway MCP Server Status:** `https://mcp-server-railway-production.up.railway.app/health`
**Test Credentials:** testuser/testuser123
**Database:** Ready for token storage if needed
---
**🎯 Final Note:** This implementation follows the microservices architecture where frontend (Vercel) communicates with backend (Railway MCP Server) via REST API. The design maintains security, performance, and user experience standards while integrating seamlessly with the existing Next.js application.
---
**Author:** MiniMax Agent
**Date:** 2025-11-05
**Version:** 1.0