# MonacoUSA Portal - Complete Implementation Guide

## Overview

This document provides step-by-step instructions to create a complete portal foundation for MonacoUSA using the same proven tech stack as the Port Nimara client portal. The portal will feature Keycloak authentication, responsive design, PWA capabilities, and a modular structure for adding custom tools.

## Project Specifications

- **Project Name**: monacousa-portal
- **Domain**: monacousa.org (configurable)
- **Primary Color**: #a31515 (MonacoUSA red)
- **Secondary Color**: #ffffff (white)
- **Framework**: Nuxt 3 with Vue 3
- **UI Library**: Vuetify 3
- **Authentication**: Keycloak (OAuth2/OIDC)
- **Database**: NocoDB
- **File Storage**: MinIO (S3-compatible)
- **Features**: PWA, Mobile-responsive, Dashboard layout

## Prerequisites

Before starting, ensure you have:
- Node.js 18+ installed
- Git installed
- Access to a Keycloak server
- NocoDB instance (or ability to set one up)
- MinIO instance (or ability to set one up)

## Phase 1: Project Initialization

### 1.1 Create New Repository

```bash
mkdir monacousa-portal
cd monacousa-portal
git init
```

### 1.2 Initialize Nuxt 3 Project

```bash
npx nuxi@latest init .
```

### 1.3 Install Core Dependencies

```bash
npm install @nuxt/ui@^3.2.0 vuetify-nuxt-module@^0.18.3 @vite-pwa/nuxt@^0.10.6 motion-v@^1.6.1
```

### 1.4 Install Additional Dependencies

```bash
npm install @types/node formidable@^3.5.4 mime-types@^3.0.1 minio@^8.0.5 sharp@^0.34.2
npm install -D @types/formidable@^3.4.5 @types/mime-types@^3.0.1
```

## Phase 2: Project Structure Setup

### 2.1 Create Directory Structure

```bash
mkdir -p components composables layouts middleware pages/auth pages/dashboard server/api server/utils server/plugins utils docs public/icons
```

### 2.2 Create package.json

```json
{
  "name": "monacousa-portal",
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "@nuxt/ui": "^3.2.0",
    "@vite-pwa/nuxt": "^0.10.6",
    "formidable": "^3.5.4",
    "mime-types": "^3.0.1",
    "minio": "^8.0.5",
    "motion-v": "^1.6.1",
    "nuxt": "^3.15.4",
    "sharp": "^0.34.2",
    "vue": "latest",
    "vue-router": "latest",
    "vuetify-nuxt-module": "^0.18.3"
  },
  "devDependencies": {
    "@types/formidable": "^3.4.5",
    "@types/mime-types": "^3.0.1"
  }
}
```

## Phase 3: Core Configuration

### 3.1 Create nuxt.config.ts

```typescript
export default defineNuxtConfig({
  ssr: false,
  compatibilityDate: "2024-11-01",
  devtools: { enabled: true },
  modules: ["vuetify-nuxt-module", "@vite-pwa/nuxt", "motion-v/nuxt"],
  app: {
    head: {
      titleTemplate: "%s • MonacoUSA Portal",
      title: "MonacoUSA Portal",
      meta: [
        { property: "og:title", content: "MonacoUSA Portal" },
        { property: "og:image", content: "/og-image.png" },
        { name: "twitter:card", content: "summary_large_image" },
        { name: "viewport", content: "width=device-width, initial-scale=1" },
        { name: "apple-mobile-web-app-capable", content: "yes" },
        { name: "apple-mobile-web-app-status-bar-style", content: "default" },
        { name: "apple-mobile-web-app-title", content: "MonacoUSA Portal" },
      ],
      htmlAttrs: {
        lang: "en",
      },
    },
  },
  pwa: {
    registerType: 'autoUpdate',
    manifest: {
      name: 'MonacoUSA Portal',
      short_name: 'MonacoUSA',
      description: 'MonacoUSA Portal - Unified dashboard for tools and services',
      theme_color: '#a31515',
      background_color: '#ffffff',
      display: 'standalone',
      orientation: 'portrait',
      start_url: '/',
      scope: '/',
      icons: [
        {
          src: '/icons/icon-72x72.png',
          sizes: '72x72',
          type: 'image/png'
        },
        {
          src: '/icons/icon-96x96.png',
          sizes: '96x96',
          type: 'image/png'
        },
        {
          src: '/icons/icon-128x128.png',
          sizes: '128x128',
          type: 'image/png'
        },
        {
          src: '/icons/icon-144x144.png',
          sizes: '144x144',
          type: 'image/png'
        },
        {
          src: '/icons/icon-152x152.png',
          sizes: '152x152',
          type: 'image/png'
        },
        {
          src: '/icons/icon-192x192.png',
          sizes: '192x192',
          type: 'image/png'
        },
        {
          src: '/icons/icon-384x384.png',
          sizes: '384x384',
          type: 'image/png'
        },
        {
          src: '/icons/icon-512x512.png',
          sizes: '512x512',
          type: 'image/png'
        }
      ]
    },
    workbox: {
      navigateFallback: '/',
      globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico}'],
      navigateFallbackDenylist: [/^\/api\//],
      runtimeCaching: [
        {
          urlPattern: /^https:\/\/.*\.monacousa\.org\/.*/i,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'api-cache',
            expiration: {
              maxEntries: 10,
              maxAgeSeconds: 60 * 60 * 24 // 24 hours
            },
            cacheableResponse: {
              statuses: [0, 200]
            }
          }
        }
      ],
      skipWaiting: true,
      clientsClaim: true
    },
    client: {
      installPrompt: true,
      periodicSyncForUpdates: 20
    },
    devOptions: {
      enabled: true,
      type: 'module'
    }
  },
  nitro: {
    experimental: {
      wasm: true
    }
  },
  runtimeConfig: {
    // Server-side configuration
    keycloak: {
      issuer: process.env.NUXT_KEYCLOAK_ISSUER || "",
      clientId: process.env.NUXT_KEYCLOAK_CLIENT_ID || "monacousa-portal",
      clientSecret: process.env.NUXT_KEYCLOAK_CLIENT_SECRET || "",
      callbackUrl: process.env.NUXT_KEYCLOAK_CALLBACK_URL || "https://monacousa.org/auth/callback",
    },
    nocodb: {
      url: process.env.NUXT_NOCODB_URL || "",
      token: process.env.NUXT_NOCODB_TOKEN || "",
      baseId: process.env.NUXT_NOCODB_BASE_ID || "",
    },
    minio: {
      endPoint: process.env.NUXT_MINIO_ENDPOINT || "s3.monacousa.org",
      port: parseInt(process.env.NUXT_MINIO_PORT || "443"),
      useSSL: process.env.NUXT_MINIO_USE_SSL !== "false",
      accessKey: process.env.NUXT_MINIO_ACCESS_KEY || "",
      secretKey: process.env.NUXT_MINIO_SECRET_KEY || "",
      bucketName: process.env.NUXT_MINIO_BUCKET_NAME || "monacousa-portal",
    },
    sessionSecret: process.env.NUXT_SESSION_SECRET || "",
    encryptionKey: process.env.NUXT_ENCRYPTION_KEY || "",
    public: {
      // Client-side configuration
      appName: "MonacoUSA Portal",
      domain: process.env.NUXT_PUBLIC_DOMAIN || "monacousa.org",
    },
  },
  vuetify: {
    vuetifyOptions: {
      theme: {
        defaultTheme: "monacousa",
        themes: {
          monacousa: {
            colors: {
              primary: "#a31515",
              secondary: "#ffffff",
              accent: "#f5f5f5",
              error: "#ff5252",
              warning: "#ff9800",
              info: "#2196f3",
              success: "#4caf50",
            },
          },
        },
      },
    },
  },
});
```

### 3.2 Create Environment Configuration (.env.example)

```env
# Keycloak Configuration
NUXT_KEYCLOAK_ISSUER=https://auth.monacousa.org/realms/monacousa-portal
NUXT_KEYCLOAK_CLIENT_ID=monacousa-portal
NUXT_KEYCLOAK_CLIENT_SECRET=your-keycloak-client-secret
NUXT_KEYCLOAK_CALLBACK_URL=https://monacousa.org/auth/callback

# NocoDB Configuration
NUXT_NOCODB_URL=https://db.monacousa.org
NUXT_NOCODB_TOKEN=your-nocodb-token
NUXT_NOCODB_BASE_ID=your-nocodb-base-id

# MinIO Configuration
NUXT_MINIO_ENDPOINT=s3.monacousa.org
NUXT_MINIO_PORT=443
NUXT_MINIO_USE_SSL=true
NUXT_MINIO_ACCESS_KEY=your-minio-access-key
NUXT_MINIO_SECRET_KEY=your-minio-secret-key
NUXT_MINIO_BUCKET_NAME=monacousa-portal

# Security Configuration
NUXT_SESSION_SECRET=your-48-character-session-secret-key-here
NUXT_ENCRYPTION_KEY=your-32-character-encryption-key-here

# Public Configuration
NUXT_PUBLIC_DOMAIN=monacousa.org
```

## Phase 4: Authentication Implementation

### 4.1 Create Keycloak Utility (server/utils/keycloak.ts)

```typescript
interface KeycloakConfig {
  issuer: string;
  clientId: string;
  clientSecret: string;
  callbackUrl: string;
}

interface TokenResponse {
  access_token: string;
  refresh_token: string;
  id_token: string;
  token_type: string;
  expires_in: number;
}

interface UserInfo {
  sub: string;
  email: string;
  given_name?: string;
  family_name?: string;
  name?: string;
  groups?: string[];
  tier?: string;
}

export class KeycloakClient {
  private config: KeycloakConfig;

  constructor(config: KeycloakConfig) {
    this.config = config;
  }

  getAuthUrl(state: string): string {
    const params = new URLSearchParams({
      client_id: this.config.clientId,
      redirect_uri: this.config.callbackUrl,
      response_type: 'code',
      scope: 'openid email profile',
      state,
    });

    return `${this.config.issuer}/protocol/openid-connect/auth?${params}`;
  }

  async exchangeCodeForTokens(code: string): Promise<TokenResponse> {
    const response = await fetch(`${this.config.issuer}/protocol/openid-connect/token`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: this.config.clientId,
        client_secret: this.config.clientSecret,
        code,
        redirect_uri: this.config.callbackUrl,
      }),
    });

    if (!response.ok) {
      throw new Error(`Token exchange failed: ${response.statusText}`);
    }

    return response.json();
  }

  async getUserInfo(accessToken: string): Promise<UserInfo> {
    const response = await fetch(`${this.config.issuer}/protocol/openid-connect/userinfo`, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    if (!response.ok) {
      throw new Error(`Failed to get user info: ${response.statusText}`);
    }

    return response.json();
  }

  async refreshToken(refreshToken: string): Promise<TokenResponse> {
    const response = await fetch(`${this.config.issuer}/protocol/openid-connect/token`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: this.config.clientId,
        client_secret: this.config.clientSecret,
        refresh_token: refreshToken,
      }),
    });

    if (!response.ok) {
      throw new Error(`Token refresh failed: ${response.statusText}`);
    }

    return response.json();
  }
}

export function createKeycloakClient(): KeycloakClient {
  const config = useRuntimeConfig();
  return new KeycloakClient(config.keycloak);
}
```

### 4.2 Create Session Management (server/utils/session.ts)

```typescript
import { serialize, parse } from 'cookie';
import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'crypto';

interface SessionData {
  user: {
    id: string;
    email: string;
    name: string;
    groups?: string[];
    tier?: string;
  };
  tokens: {
    accessToken: string;
    refreshToken: string;
    expiresAt: number;
  };
  createdAt: number;
  lastActivity: number;
}

export class SessionManager {
  private encryptionKey: Buffer;
  private cookieName = 'monacousa-session';

  constructor(encryptionKey: string) {
    this.encryptionKey = Buffer.from(encryptionKey, 'hex');
  }

  private encrypt(data: string): string {
    const iv = randomBytes(16);
    const cipher = createCipheriv('aes-256-cbc', this.encryptionKey, iv);
    let encrypted = cipher.update(data, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    return iv.toString('hex') + ':' + encrypted;
  }

  private decrypt(encryptedData: string): string {
    const [ivHex, encrypted] = encryptedData.split(':');
    const iv = Buffer.from(ivHex, 'hex');
    const decipher = createDecipheriv('aes-256-cbc', this.encryptionKey, iv);
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  }

  createSession(sessionData: SessionData): string {
    const data = JSON.stringify(sessionData);
    const encrypted = this.encrypt(data);
    
    return serialize(this.cookieName, encrypted, {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      maxAge: 60 * 60 * 24 * 7, // 7 days
      path: '/',
    });
  }

  getSession(cookieHeader?: string): SessionData | null {
    if (!cookieHeader) return null;

    const cookies = parse(cookieHeader);
    const sessionCookie = cookies[this.cookieName];

    if (!sessionCookie) return null;

    try {
      const decrypted = this.decrypt(sessionCookie);
      const sessionData = JSON.parse(decrypted) as SessionData;

      // Check if session is expired
      if (Date.now() > sessionData.tokens.expiresAt) {
        return null;
      }

      return sessionData;
    } catch (error) {
      console.error('Failed to decrypt session:', error);
      return null;
    }
  }

  destroySession(): string {
    return serialize(this.cookieName, '', {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      maxAge: 0,
      path: '/',
    });
  }
}

export function createSessionManager(): SessionManager {
  const config = useRuntimeConfig();
  return new SessionManager(config.encryptionKey);
}
```

### 4.3 Create Authentication Middleware (middleware/auth.ts)

```typescript
export default defineNuxtRouteMiddleware((to) => {
  // Skip auth for public pages
  if (to.meta.auth === false) {
    return;
  }

  // Check if user is authenticated
  const authState = useState('auth.state', () => ({
    authenticated: false,
    user: null,
    groups: [],
  }));

  if (!authState.value.authenticated) {
    return navigateTo('/login');
  }
});
```

### 4.4 Create Auth API Routes

#### server/api/auth/login.get.ts

```typescript
export default defineEventHandler(async (event) => {
  const keycloak = createKeycloakClient();
  const state = randomBytes(32).toString('hex');
  
  // Store state in session for verification
  setCookie(event, 'oauth-state', state, {
    httpOnly: true,
    secure: true,
    maxAge: 600, // 10 minutes
  });

  const authUrl = keycloak.getAuthUrl(state);
  
  return sendRedirect(event, authUrl);
});
```

#### server/api/auth/callback.get.ts

```typescript
export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const { code, state } = query;

  if (!code || !state) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Missing authorization code or state',
    });
  }

  // Verify state
  const storedState = getCookie(event, 'oauth-state');
  if (state !== storedState) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid state parameter',
    });
  }

  try {
    const keycloak = createKeycloakClient();
    const sessionManager = createSessionManager();

    // Exchange code for tokens
    const tokens = await keycloak.exchangeCodeForTokens(code as string);
    
    // Get user info
    const userInfo = await keycloak.getUserInfo(tokens.access_token);

    // Create session
    const sessionData = {
      user: {
        id: userInfo.sub,
        email: userInfo.email,
        name: userInfo.name || `${userInfo.given_name} ${userInfo.family_name}`.trim(),
        groups: userInfo.groups || [],
        tier: userInfo.tier,
      },
      tokens: {
        accessToken: tokens.access_token,
        refreshToken: tokens.refresh_token,
        expiresAt: Date.now() + (tokens.expires_in * 1000),
      },
      createdAt: Date.now(),
      lastActivity: Date.now(),
    };

    const sessionCookie = sessionManager.createSession(sessionData);
    
    // Set session cookie
    setHeader(event, 'Set-Cookie', sessionCookie);
    
    // Clear state cookie
    deleteCookie(event, 'oauth-state');

    return sendRedirect(event, '/dashboard');
  } catch (error) {
    console.error('Auth callback error:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Authentication failed',
    });
  }
});
```

#### server/api/auth/session.get.ts

```typescript
export default defineEventHandler(async (event) => {
  const sessionManager = createSessionManager();
  const cookieHeader = getHeader(event, 'cookie');
  const session = sessionManager.getSession(cookieHeader);

  if (!session) {
    return {
      authenticated: false,
      user: null,
      groups: [],
    };
  }

  return {
    authenticated: true,
    user: session.user,
    groups: session.user.groups || [],
  };
});
```

#### server/api/auth/logout.post.ts

```typescript
export default defineEventHandler(async (event) => {
  const sessionManager = createSessionManager();
  const destroyCookie = sessionManager.destroySession();
  
  setHeader(event, 'Set-Cookie', destroyCookie);
  
  return { success: true };
});
```

## Phase 5: Database Integration (NocoDB)

### 5.1 Create NocoDB Utility (server/utils/nocodb.ts)

```typescript
interface NocoDBConfig {
  url: string;
  token: string;
  baseId: string;
}

export class NocoDBClient {
  private config: NocoDBConfig;
  private baseUrl: string;

  constructor(config: NocoDBConfig) {
    this.config = config;
    this.baseUrl = `${config.url}/api/v2/tables`;
  }

  private async request(endpoint: string, options: RequestInit = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    
    const response = await fetch(url, {
      ...options,
      headers: {
        'xc-token': this.config.token,
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    if (!response.ok) {
      throw new Error(`NocoDB request failed: ${response.statusText}`);
    }

    return response.json();
  }

  async findAll(tableName: string, params: Record<string, any> = {}) {
    const queryString = new URLSearchParams(params).toString();
    const endpoint = `/${tableName}/records${queryString ? `?${queryString}` : ''}`;
    return this.request(endpoint);
  }

  async findOne(tableName: string, id: string) {
    return this.request(`/${tableName}/records/${id}`);
  }

  async create(tableName: string, data: Record<string, any>) {
    return this.request(`/${tableName}/records`, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async update(tableName: string, id: string, data: Record<string, any>) {
    return this.request(`/${tableName}/records/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }

  async delete(tableName: string, id: string) {
    return this.request(`/${tableName}/records/${id}`, {
      method: 'DELETE',
    });
  }
}

export function createNocoDBClient(): NocoDBClient {
  const config = useRuntimeConfig();
  return new NocoDBClient(config.nocodb);
}
```

### 5.2 Create Database API Template (server/api/data/[table]/[...params].ts)

```typescript
export default defineEventHandler(async (event) => {
  const method = getMethod(event);
  const params = getRouterParams(event);
  const table = params.table;
  const additionalParams = params.params?.split('/') || [];

  const nocodb = createNocoDBClient();

  try {
    switch (method) {
      case 'GET':
        if (additionalParams.length > 0) {
          // Get single record
          const id = additionalParams[0];
          return await nocodb.findOne(table, id);
        } else {
          // Get all records
          const query = getQuery(event);
          return await nocodb.findAll(table, query);
        }

      case 'POST':
        const createData = await readBody(event);
        return await nocodb.create(table, createData);

      case 'PATCH':
        if (additionalParams.length === 0) {
          throw createError({
            statusCode: 400,
            statusMessage: 'Record ID required for update',
          });
        }
        const updateId = additionalParams[0];
        const updateData = await readBody(event);
        return await nocodb.update(table, updateId, updateData);

      case 'DELETE':
        if (additionalParams.length === 0) {
          throw createError({
            statusCode: 400,
            statusMessage: 'Record ID required for delete',
          });
        }
        const deleteId = additionalParams[0];
        return await nocodb.delete(table, deleteId);

      default:
        throw createError({
          statusCode: 405,
          statusMessage: 'Method not allowed',
        });
    }
  } catch (error) {
    console.error('Database operation error:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'Database operation failed',
    });
  }
});
```

## Phase 6: File Storage Integration (MinIO)

### 6.1 Create MinIO Utility (server/utils/minio.ts)

```typescript
import { Client } from 'minio';

interface MinIOConfig {
  endPoint: string;
  port: number;
  useSSL: boolean;
  accessKey: string;
  secretKey: string;
  bucketName: string;
}

export class MinIOClient {
  private client: Client;
  private bucketName: string;

  constructor(config: MinIOConfig) {
    this.client = new Client({
      endPoint: config.endPoint,
      port: config.port,
      useSSL: config.useSSL,
      accessKey: config.accessKey,
      secretKey: config.secretKey,
    });
    this.bucketName = config.bucketName;
  }

  async ensureBucket(): Promise<void> {
    const exists = await this.client.bucketExists(this.bucketName);
    if (!exists) {
      await this.client.makeBucket(this.bucketName);
    }
  }

  async uploadFile(fileName: string, buffer: Buffer, contentType?: string): Promise<string> {
    await this.ensureBucket();
    
    const metadata = contentType ? { 'Content-Type': contentType } : {};
    
    await this.client.putObject(this.bucketName, fileName, buffer, buffer.length, metadata);
    
    return fileName;
  }

  async getFile(fileName: string): Promise<Buffer> {
    const stream = await this.client.getObject(this.bucketName, fileName);
    const chunks: Buffer[] = [];
    
    return new Promise((resolve, reject) => {
      stream.on('data', (chunk) => chunks.push(chunk));
      stream.on('end', () => resolve(Buffer.concat(chunks)));
      stream.on('error', reject);
    });
  }

  async deleteFile(fileName: string): Promise<void> {
    await this.client.removeObject(this.bucketName, fileName);
  }

  async listFiles(prefix?: string): Promise<string[]> {
    const objects: string[] = [];
    const stream = this.client.listObjects(this.bucketName, prefix);
    
    return new Promise((resolve, reject) => {
      stream.on('data', (obj) => objects.push(obj.name!));
      stream.on('end', () => resolve(objects));
      stream.on('error', reject);
    });
  }

  getPresignedUrl(fileName: string, expiry: number = 3600): Promise<string> {
    return this.client.presignedGetObject(this.bucketName, fileName, expiry);
  }
}

export function createMinIOClient(): MinIOClient {
  const config = useRuntimeConfig();
  return new MinIOClient(config.minio);
}
```

### 6.2 Create File Upload API (server/api/files/upload.post.ts)

```typescript
import formidable from 'formidable';
import { readFileSync } from 'fs';
import { lookup } from 'mime-types';

export default defineEventHandler(async (event) => {
  const form = formidable({
    maxFileSize: 10 * 1024 * 1024, // 10MB
    allowEmptyFiles: false,
  });

  try {
    const [fields, files] = await form.parse(event.node.req);
    const minio = createMinIOClient();

    const uploadedFiles = [];

    for (const [fieldName, fileArray] of Object.entries(files)) {
      const file = Array.isArray(fileArray) ? fileArray[0] : fileArray;
      
      if (file && file.filepath) {
        const buffer = readFileSync(file.filepath);
        const contentType = lookup(file.originalFilename || '') || 'application/octet-stream';
        const fileName = `${Date.now()}-${file.originalFilename}`;
        
        await minio.uploadFile(fileName, buffer, contentType);
        
        uploadedFiles.push({
          fieldName,
          fileName,
          originalName: file.originalFilename,
          size: file.size,
          contentType,
        });
      }
    }

    return { success: true, files: uploadedFiles };
  } catch (error) {
    console.error('File upload error:', error);
    throw createError({
      statusCode: 500,
      statusMessage: 'File upload failed',
    });
  }
});
```

### 6.3 Create File Download API (server/api/files/[filename].get.ts)

```typescript
export default defineEventHandler(async (event) => {
  const filename = getRouterParam(event, 'filename');
  
  if (!filename) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Filename required',
    });
  }

  try {
    const minio = createMinIOClient();
    const buffer = await minio.getFile(filename);
    
    // Set appropriate headers
    setHeader(event, 'Content-Type', 'application/octet-stream');
    setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`);
    
    return buffer;
  } catch (error) {
    console.error('File download error:', error);
    throw createError({
      statusCode: 404,
      statusMessage: 'File not found',
    });
  }
});
```

## Phase 7: UI Components and Layout

### 7.1 Create Authentication Composable (composables/useAuth.ts)

```typescript
interface User {
  id: string;
  email: string;
  name: string;
  groups?: string[];
  tier?: string;
}

interface AuthState {
  authenticated: boolean;
  user: User | null;
  groups: string[];
}

export const useAuth = () => {
  const authState = useState<AuthState>('auth.state', () => ({
    authenticated: false,
    user: null,
    groups: [],
  }));

  const login = () => {
    return navigateTo('/api/auth/login');
  };

  const logout = async () => {
    try {
      await $fetch('/api/auth/logout', { method: 'POST' });
      authState.value = {
        authenticated: false,
        user: null,
        groups: [],
      };
      await navigateTo('/login');
    } catch (error) {
      console.error('Logout error:', error);
      await navigateTo('/login');
    }
  };

  const checkAuth = async () => {
    try {
      const response = await $fetch<AuthState>('/api/auth/session');
      authState.value = response;
      return response.authenticated;
    } catch (error) {
      console.error('Auth check error:', error);
      authState.value = {
        authenticated: false,
        user: null,
        groups: [],
      };
      return false;
    }
  };

  const isAdmin = computed(() => {
    return authState.value.groups?.includes('admin') || false;
  });

  const hasRole = (role: string) => {
    return authState.value.groups?.includes(role) || false;
  };

  return {
    authState: readonly(authState),
    user: computed(() => authState.value.user),
    authenticated: computed(() => authState.value.authenticated),
    groups: computed(() => authState.value.groups),
    isAdmin,
    hasRole,
    login,
    logout,
    checkAuth,
  };
};
```

### 7.2 Create Login Page (pages/login.vue)

```vue
<template>
  <v-app>
    <v-main class="d-flex align-center justify-center min-h-screen bg-grey-lighten-4">
      <v-container>
        <v-row justify="center">
          <v-col cols="12" sm="8" md="6" lg="4">
            <v-card class="elevation-8 rounded-lg">
              <v-card-text class="pa-8">
                <div class="text-center mb-6">
                  <v-img
                    src="/logo.png"
                    alt="MonacoUSA"
                    max-width="120"
                    class="mx-auto mb-4"
                  />
                  <h1 class="text-h4 font-weight-bold text-primary mb-2">
                    MonacoUSA Portal
                  </h1>
                  <p class="text-body-1 text-grey-600">
                    Sign in to access your dashboard
                  </p>
                </div>

                <v-btn
                  @click="handleLogin"
                  :loading="loading"
                  color="primary"
                  size="large"
                  block
                  class="mb-4"
                  prepend-icon="mdi-login"
                >
                  Sign In with SSO
                </v-btn>

                <div class="text-center">
                  <p class="text-caption text-grey-600">
                    Secure authentication powered by Keycloak
                  </p>
                </div>
              </v-card-text>
            </v-card>
          </v-col>
        </v-row>
      </v-container>
    </v-main>
  </v-app>
</template>

<script setup lang="ts">
definePageMeta({
  auth: false,
  layout: false,
});

const { login } = useAuth();
const loading = ref(false);

const handleLogin = async () => {
  loading.value = true;
  try {
    await login();
  } catch (error) {
    console.error('Login error:', error);
  } finally {
    loading.value = false;
  }
};
</script>
```

### 7.3 Create Auth Callback Page (pages/auth/callback.vue)

```vue
<template>
  <v-app>
    <v-main class="d-flex align-center justify-center min-h-screen">
      <v-container>
        <v-row justify="center">
          <v-col cols="12" sm="6" md="4">
            <v-card class="text-center pa-8">
              <v-progress-circular
                indeterminate
                color="primary"
                size="64"
                class="mb-4"
              />
              <h2 class="text-h5 mb-2">Signing you in...</h2>
              <p class="text-body-2 text-grey-600">
                Please wait while we complete your authentication.
              </p>
            </v-card>
          </v-col>
        </v-row>
      </v-container>
    </v-main>
  </v-app>
</template>

<script setup lang="ts">
definePageMeta({
  auth: false,
  layout: false,
});

// The actual authentication is handled by the server-side callback API
// This page just shows a loading state while the redirect happens
</script>
```

### 7.4 Create Dashboard Layout (layouts/dashboard.vue)

```vue
<template>
  <v-app>
    <v-navigation-drawer
      v-model="drawer"
      :rail="rail && !mdAndDown"
      permanent
      color="white"
      class="elevation-2"
    >
      <!-- Logo and Title -->
      <v-list>
        <v-list-item
          class="px-3 py-3"
          :class="{ 'cursor-pointer': !mdAndDown }"
          @click="!mdAndDown ? (rail = !rail) : undefined"
        >
          <template v-slot:prepend>
            <v-avatar size="40" class="me-3">
              <v-img src="/logo.png" alt="MonacoUSA" />
            </v-avatar>
          </template>
          <v-list-item-title v-if="!rail || mdAndDown" class="text-subtitle-1 font-weight-medium">
            MonacoUSA Portal
          </v-list-item-title>
        </v-list-item>
      </v-list>

      <v-divider></v-divider>

      <!-- Navigation Items -->
      <v-list density="compact" nav>
        <v-list-item
          v-for="item in navigationItems"
          :key="item.to"
          :prepend-icon="item.icon"
          :title="item.label"
          :value="item.to"
          :to="item.to"
          color="primary"
          rounded="xl"
          class="mx-1"
        ></v-list-item>

        <!-- Admin Section -->
        <template v-if="isAdmin">
          <v-divider class="my-2"></v-divider>
          <v-list-subheader class="text-caption text-grey-600 px-3">
            Administration
          </v-list-subheader>
          <v-list-item
            v-for="item in adminItems"
            :key="item.to"
            :prepend-icon="item.icon"
            :title="item.label"
            :value="item.to"
            :to="item.to"
            color="primary"
            rounded="xl"
            class="mx-1"
          ></v-list-item>
        </template>
      </v-list>

      <template v-slot:append>
        <v-divider></v-divider>
        
        <!-- User Info Section -->
        <div class="pa-2">
          <v-list v-if="user">
            <v-list-item
              :prepend-avatar="`https://ui-avatars.com/api/?name=${encodeURIComponent(user.name || user.email)}&background=a31515&color=fff`"
              :title="user.name || user.email"
              :subtitle="user.email"
              class="px-2"
            >
              <template v-slot:append v-if="!rail && groups.length">
                <div>
                  <v-chip 
                    v-if="groups.includes('admin')" 
                    size="small" 
                    color="primary"
                    variant="tonal"
                  >
                    Admin
                  </v-chip>
                  <v-chip 
                    v-else-if="groups.includes('manager')" 
                    size="small" 
                    color="success"
                    variant="tonal"
                  >
                    Manager
                  </v-chip>
                </div>
              </template>
            </v-list-item>
            
            <v-list-item
              @click="handleLogout"
              prepend-icon="mdi-logout"
              title="Logout"
              class="px-2 mt-1"
              base-color="error"
              rounded="xl"
            ></v-list-item>
          </v-list>
        </div>
      </template>
    </v-navigation-drawer>

    <v-app-bar 
      flat
      color="white"
      class="border-b"
    >
      <v-app-bar-nav-icon
        @click="drawer = !drawer"
        class="d-lg-none"
      ></v-app-bar-nav-icon>

      <v-toolbar-title class="text-h6">
        {{ pageTitle }}
      </v-toolbar-title>

      <v-spacer></v-spacer>

      <!-- PWA Install Button -->
      <v-btn
        v-if="canInstall"
        @click="install"
        icon="mdi-download"
        variant="text"
        class="me-2"
      >
        <v-icon>mdi-download</v-icon>
        <v-tooltip activator="parent" location="bottom">
          Install App
        </v-tooltip>
      </v-btn>
    </v-app-bar>

    <v-main class="bg-grey-lighten-4">
      <slot />
    </v-main>
  </v-app>
</template>

<script setup lang="ts">
import { useDisplay } from 'vuetify';

const route = useRoute();
const { mdAndDown } = useDisplay();
const { user, groups, isAdmin, logout } = useAuth();

// Sidebar state
const drawer = ref(true);
const rail = ref(false);

// PWA install
const canInstall = ref(false);
let deferredPrompt: any = null;

// Page title based on current route
const pageTitle = computed(() => {
  const routeName = route.name as string;
  const pageTitles: Record<string, string> = {
    'dashboard': 'Dashboard',
    'dashboard-index': 'Dashboard',
    'dashboard-tools': 'Tools',
    'dashboard-settings': 'Settings',
    'dashboard-admin': 'Administration',
  };
  
  return pageTitles[routeName] || 'Dashboard';
});

// Main navigation items
const navigationItems = computed(() => {
  return [
    {
      label: 'Dashboard',
      icon: 'mdi-view-dashboard',
      to: '/dashboard',
    },
    {
      label: 'Tools',
      icon: 'mdi-tools',
      to: '/dashboard/tools',
    },
    {
      label: 'Settings',
      icon: 'mdi-cog',
      to: '/dashboard/settings',
    },
  ];
});

// Admin navigation items
const adminItems = computed(() => {
  return [
    {
      label: 'Admin Panel',
      icon: 'mdi-shield-crown',
      to: '/dashboard/admin',
    },
  ];
});

// Logout handler
const handleLogout = async () => {
  await logout();
};

// PWA install handler
const install = async () => {
  if (deferredPrompt) {
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    if (outcome === 'accepted') {
      canInstall.value = false;
    }
    deferredPrompt = null;
  }
};

// Initialize drawer state on mobile
onMounted(() => {
  if (mdAndDown.value) {
    drawer.value = false;
  }

  // PWA install prompt
  window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;
    canInstall.value = true;
  });

  window.addEventListener('appinstalled', () => {
    canInstall.value = false;
    deferredPrompt = null;
  });
});
</script>

<style scoped>
.border-b {
  border-bottom: 1px solid rgba(0, 0, 0, 0.12) !important;
}

.cursor-pointer {
  cursor: pointer;
}

/* Improve rail mode appearance */
.v-navigation-drawer--rail {
  width: 72px !important;
}

.v-navigation-drawer--rail .v-list-item {
  padding-inline-start: 16px !important;
  padding-inline-end: 16px !important;
  justify-content: center;
  min-height: 48px;
}

.v-navigation-drawer--rail .v-list-item__prepend {
  margin-inline-end: 0 !important;
  justify-content: center;
}

.v-navigation-drawer--rail .v-list-item__content {
  display: none;
}

.v-navigation-drawer--rail .v-list-item__append {
  display: none;
}

/* Mobile responsiveness */
@media (max-width: 960px) {
  .v-navigation-drawer {
    position: fixed !important;
    z-index: 1004 !important;
  }
  
  .v-main {
    padding-left: 0 !important;
  }
}

/* PWA optimizations */
@media (display-mode: standalone) {
  .v-app-bar {
    padding-top: env(safe-area-inset-top);
  }
  
  .v-navigation-drawer {
    padding-bottom: env(safe-area-inset-bottom);
  }
}
</style>
```

### 7.5 Create Dashboard Index Page (pages/dashboard/index.vue)

```vue
<template>
  <v-container fluid class="pa-6">
    <v-row>
      <v-col cols="12">
        <div class="d-flex align-center mb-6">
          <div>
            <h1 class="text-h4 font-weight-bold text-primary mb-2">
              Welcome back, {{ user?.name || 'User' }}!
            </h1>
            <p class="text-body-1 text-grey-600">
              Here's what's happening with your MonacoUSA Portal today.
            </p>
          </div>
        </div>
      </v-col>
    </v-row>

    <v-row>
      <!-- Quick Stats -->
      <v-col cols="12" sm="6" md="3">
        <v-card class="pa-4 text-center" color="primary" variant="tonal">
          <v-icon size="48" class="mb-2">mdi-account-group</v-icon>
          <div class="text-h5 font-weight-bold">{{ stats.users }}</div>
          <div class="text-body-2">Active Users</div>
        </v-card>
      </v-col>

      <v-col cols="12" sm="6" md="3">
        <v-card class="pa-4 text-center" color="success" variant="tonal">
          <v-icon size="48" class="mb-2">mdi-tools</v-icon>
          <div class="text-h5 font-weight-bold">{{ stats.tools }}</div>
          <div class="text-body-2">Available Tools</div>
        </v-card>
      </v-col>

      <v-col cols="12" sm="6" md="3">
        <v-card class="pa-4 text-center" color="info" variant="tonal">
          <v-icon size="48" class="mb-2">mdi-file-document</v-icon>
          <div class="text-h5 font-weight-bold">{{ stats.documents }}</div>
          <div class="text-body-2">Documents</div>
        </v-card>
      </v-col>

      <v-col cols="12" sm="6" md="3">
        <v-card class="pa-4 text-center" color="warning" variant="tonal">
          <v-icon size="48" class="mb-2">mdi-chart-line</v-icon>
          <div class="text-h5 font-weight-bold">{{ stats.activity }}</div>
          <div class="text-body-2">Recent Activity</div>
        </v-card>
      </v-col>
    </v-row>

    <v-row>
      <!-- Recent Activity -->
      <v-col cols="12" md="8">
        <v-card>
          <v-card-title class="d-flex align-center">
            <v-icon class="me-2">mdi-history</v-icon>
            Recent Activity
          </v-card-title>
          <v-card-text>
            <v-list>
              <v-list-item
                v-for="activity in recentActivity"
                :key="activity.id"
                :prepend-icon="activity.icon"
                :title="activity.title"
                :subtitle="activity.description"
              >
                <template v-slot:append>
                  <div class="text-caption text-grey-600">
                    {{ formatDate(activity.timestamp) }}
                  </div>
                </template>
              </v-list-item>
            </v-list>
          </v-card-text>
        </v-card>
      </v-col>

      <!-- Quick Actions -->
      <v-col cols="12" md="4">
        <v-card>
          <v-card-title class="d-flex align-center">
            <v-icon class="me-2">mdi-lightning-bolt</v-icon>
            Quick Actions
          </v-card-title>
          <v-card-text>
            <v-list>
              <v-list-item
                v-for="action in quickActions"
                :key="action.title"
                :prepend-icon="action.icon"
                :title="action.title"
                :subtitle="action.description"
                :to="action.to"
                class="rounded"
              />
            </v-list>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script setup lang="ts">
definePageMeta({
  layout: 'dashboard',
});

const { user } = useAuth();

// Mock data - replace with real API calls
const stats = ref({
  users: 42,
  tools: 8,
  documents: 156,
  activity: 23,
});

const recentActivity = ref([
  {
    id: 1,
    icon: 'mdi-account-plus',
    title: 'New user registered',
    description: 'John Doe joined the portal',
    timestamp: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago
  },
  {
    id: 2,
    icon: 'mdi-file-upload',
    title: 'Document uploaded',
    description: 'Annual report uploaded to documents',
    timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
  },
  {
    id: 3,
    icon: 'mdi-cog',
    title: 'Settings updated',
    description: 'Portal configuration modified',
    timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago
  },
]);

const quickActions = ref([
  {
    icon: 'mdi-tools',
    title: 'Browse Tools',
    description: 'Access available tools',
    to: '/dashboard/tools',
  },
  {
    icon: 'mdi-file-upload',
    title: 'Upload Document',
    description: 'Add new files',
    to: '/dashboard/files',
  },
  {
    icon: 'mdi-cog',
    title: 'Settings',
    description: 'Configure your account',
    to: '/dashboard/settings',
  },
]);

const formatDate = (date: Date) => {
  const now = new Date();
  const diff = now.getTime() - date.getTime();
  const minutes = Math.floor(diff / (1000 * 60));
  const hours = Math.floor(diff / (1000 * 60 * 60));
  const days = Math.floor(diff / (1000 * 60 * 60 * 24));

  if (minutes < 60) {
    return `${minutes}m ago`;
  } else if (hours < 24) {
    return `${hours}h ago`;
  } else {
    return `${days}d ago`;
  }
};
</script>
```

### 7.6 Create Tools Page Template (pages/dashboard/tools.vue)

```vue
<template>
  <v-container fluid class="pa-6">
    <v-row>
      <v-col cols="12">
        <div class="d-flex align-center justify-space-between mb-6">
          <div>
            <h1 class="text-h4 font-weight-bold text-primary mb-2">
              Tools & Services
            </h1>
            <p class="text-body-1 text-grey-600">
              Access your available tools and services
            </p>
          </div>
        </div>
      </v-col>
    </v-row>

    <v-row>
      <v-col
        v-for="tool in tools"
        :key="tool.id"
        cols="12"
        sm="6"
        md="4"
        lg="3"
      >
        <v-card
          class="h-100 d-flex flex-column"
          :class="{ 'elevation-8': tool.featured }"
          hover
        >
          <v-card-text class="flex-grow-1 d-flex flex-column">
            <div class="text-center mb-4">
              <v-avatar
                :color="tool.color"
                size="64"
                class="mb-3"
              >
                <v-icon size="32" color="white">{{ tool.icon }}</v-icon>
              </v-avatar>
              <h3 class="text-h6 font-weight-bold mb-2">{{ tool.name }}</h3>
              <p class="text-body-2 text-grey-600">{{ tool.description }}</p>
            </div>

            <v-spacer />

            <div class="d-flex align-center justify-space-between">
              <v-chip
                :color="tool.status === 'active' ? 'success' : 'warning'"
                size="small"
                variant="tonal"
              >
                {{ tool.status }}
              </v-chip>
              <v-btn
                :to="tool.link"
                color="primary"
                variant="tonal"
                size="small"
              >
                Open
              </v-btn>
            </div>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>

    <!-- Coming Soon Section -->
    <v-row v-if="comingSoonTools.length > 0">
      <v-col cols="12">
        <h2 class="text-h5 font-weight-bold text-primary mb-4 mt-6">
          Coming Soon
        </h2>
      </v-col>
      <v-col
        v-for="tool in comingSoonTools"
        :key="tool.id"
        cols="12"
        sm="6"
        md="4"
        lg="3"
      >
        <v-card class="h-100" disabled>
          <v-card-text class="text-center">
            <v-avatar
              color="grey-lighten-2"
              size="64"
              class="mb-3"
            >
              <v-icon size="32" color="grey">{{ tool.icon }}</v-icon>
            </v-avatar>
            <h3 class="text-h6 font-weight-bold mb-2 text-grey">{{ tool.name }}</h3>
            <p class="text-body-2 text-grey-600">{{ tool.description }}</p>
            <v-chip color="grey" size="small" variant="tonal" class="mt-2">
              Coming Soon
            </v-chip>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script setup lang="ts">
definePageMeta({
  layout: 'dashboard',
});

// Available tools - customize this based on your needs
const tools = ref([
  {
    id: 1,
    name: 'File Manager',
    description: 'Upload, organize, and manage your files',
    icon: 'mdi-folder-multiple',
    color: 'blue',
    status: 'active',
    featured: true,
    link: '/dashboard/files',
  },
  {
    id: 2,
    name: 'User Management',
    description: 'Manage users and permissions',
    icon: 'mdi-account-group',
    color: 'green',
    status: 'active',
    featured: false,
    link: '/dashboard/users',
  },
  {
    id: 3,
    name: 'Analytics',
    description: 'View detailed analytics and reports',
    icon: 'mdi-chart-bar',
    color: 'purple',
    status: 'active',
    featured: false,
    link: '/dashboard/analytics',
  },
]);

// Coming soon tools
const comingSoonTools = ref([
  {
    id: 4,
    name: 'API Manager',
    description: 'Manage API keys and integrations',
    icon: 'mdi-api',
  },
  {
    id: 5,
    name: 'Notifications',
    description: 'Configure alerts and notifications',
    icon: 'mdi-bell',
  },
]);
</script>
```

## Phase 8: Health Check and Startup

### 8.1 Create Health Check API (server/api/health.get.ts)

```typescript
export default defineEventHandler(async (event) => {
  const checks = {
    server: 'ok',
    database: 'unknown',
    storage: 'unknown',
    auth: 'unknown',
  };

  try {
    // Test NocoDB connection
    const nocodb = createNocoDBClient();
    await nocodb.findAll('test', { limit: 1 });
    checks.database = 'ok';
  } catch (error) {
    checks.database = 'error';
  }

  try {
    // Test MinIO connection
    const minio = createMinIOClient();
    await minio.ensureBucket();
    checks.storage = 'ok';
  } catch (error) {
    checks.storage = 'error';
  }

  try {
    // Test Keycloak connection
    const keycloak = createKeycloakClient();
    // Simple connectivity test - you might want to implement a proper health check
    checks.auth = 'ok';
  } catch (error) {
    checks.auth = 'error';
  }

  const allHealthy = Object.values(checks).every(status => status === 'ok');

  return {
    status: allHealthy ? 'healthy' : 'degraded',
    timestamp: new Date().toISOString(),
    checks,
  };
});
```

### 8.2 Create Startup Plugin (plugins/01.auth-check.client.ts)

```typescript
export default defineNuxtPlugin(async () => {
  const { checkAuth } = useAuth();
  
  // Check authentication status on app startup
  await checkAuth();
});
```

## Phase 9: TypeScript Types

### 9.1 Create Type Definitions (utils/types.ts)

```typescript
export interface User {
  id: string;
  email: string;
  name: string;
  groups?: string[];
  tier?: string;
}

export interface AuthState {
  authenticated: boolean;
  user: User | null;
  groups: string[];
}

export interface ApiResponse<T = any> {
  success: boolean;
  data?: T;
  error?: string;
  message?: string;
}

export interface FileUpload {
  fieldName: string;
  fileName: string;
  originalName: string;
  size: number;
  contentType: string;
}

export interface DatabaseRecord {
  id: string;
  created_at: string;
  updated_at: string;
  [key: string]: any;
}

export interface HealthCheck {
  status: 'healthy' | 'degraded' | 'unhealthy';
  timestamp: string;
  checks: {
    server: string;
    database: string;
    storage: string;
    auth: string;
  };
}
```

## Phase 10: Final Setup and Testing

### 10.1 Create Main App File (app.vue)

```vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

<script setup lang="ts">
// Global app setup
useHead({
  titleTemplate: (titleChunk) => {
    return titleChunk ? `${titleChunk} • MonacoUSA Portal` : 'MonacoUSA Portal';
  },
});
</script>
```

### 10.2 Create Index Page (pages/index.vue)

```vue
<template>
  <div>
    <!-- Redirect to dashboard if authenticated, otherwise to login -->
  </div>
</template>

<script setup lang="ts">
const { authenticated } = useAuth();

// Redirect based on authentication status
await navigateTo(authenticated.value ? '/dashboard' : '/login');
</script>
```

### 10.3 Create README.md

```markdown
# MonacoUSA Portal

A modern, responsive portal built with Nuxt 3, Vuetify, and Keycloak authentication.

## Features

- 🔐 **Keycloak Authentication** - Secure OAuth2/OIDC authentication
- 📱 **Mobile Responsive** - Works perfectly on all devices
- 🚀 **PWA Support** - Installable progressive web app
- 🎨 **Modern UI** - Beautiful Vuetify 3 interface with MonacoUSA branding
- 📁 **File Storage** - MinIO S3-compatible file storage
- 🗄️ **Database** - NocoDB for flexible data management
- 🔧 **Modular** - Easy to extend with new tools and features

## Tech Stack

- **Framework**: Nuxt 3 with Vue 3
- **UI Library**: Vuetify 3
- **Authentication**: Keycloak (OAuth2/OIDC)
- **Database**: NocoDB
- **File Storage**: MinIO (S3-compatible)
- **PWA**: Vite PWA plugin
- **TypeScript**: Full TypeScript support

## Quick Start

1. **Clone and Install**
   ```bash
   git clone <repository-url>
   cd monacousa-portal
   npm install
   ```

2. **Environment Setup**
   ```bash
   cp .env.example .env
   # Edit .env with your configuration
   ```

3. **Development**
   ```bash
   npm run dev
   ```

4. **Production Build**
   ```bash
   npm run build
   npm run preview
   ```

## Configuration

### Environment Variables

See `.env.example` for all required environment variables:

- **Keycloak**: Authentication server configuration
- **NocoDB**: Database connection settings
- **MinIO**: File storage configuration
- **Security**: Encryption keys and session secrets

### Keycloak Setup

1. Create a new client in your Keycloak realm
2. Set client type to "Confidential"
3. Configure redirect URIs:
   - `https://monacousa.org/auth/callback`
   - `http://localhost:3000/auth/callback` (development)
4. Enable "Standard Flow" authentication
5. Set up user attributes and groups as needed

### NocoDB Setup

1. Set up your NocoDB instance
2. Create a new base/project
3. Generate an API token
4. Configure tables as needed for your tools

### MinIO Setup

1. Set up MinIO server
2. Create access keys
3. Configure bucket policies
4. Set CORS policies for web access

## Project Structure

```
monacousa-portal/
├── components/          # Vue components
├── composables/         # Vue composables
├── layouts/            # Nuxt layouts
├── middleware/         # Route middleware
├── pages/              # Application pages
│   ├── auth/           # Authentication pages
│   └── dashboard/      # Dashboard pages
├── plugins/            # Nuxt plugins
├── public/             # Static assets
├── server/             # Server-side code
│   ├── api/            # API routes
│   ├── utils/          # Server utilities
│   └── plugins/        # Server plugins
├── utils/              # Shared utilities
└── docs/               # Documentation
```

## Development

### Adding New Tools

1. Create a new page in `pages/dashboard/`
2. Add navigation item to dashboard layout
3. Implement API routes in `server/api/` if needed
4. Add database tables in NocoDB if required

### API Usage

The portal provides RESTful APIs for data operations:

```typescript
// Get all records from a table
const data = await $fetch('/api/data/users');

// Get single record
const user = await $fetch('/api/data/users/123');

// Create new record
const newUser = await $fetch('/api/data/users', {
  method: 'POST',
  body: { name: 'John Doe', email: 'john@example.com' }
});

// Update record
const updatedUser = await $fetch('/api/data/users/123', {
  method: 'PATCH',
  body: { name: 'Jane Doe' }
});

// Delete record
await $fetch('/api/data/users/123', { method: 'DELETE' });
```

### File Upload

```typescript
// Upload files
const formData = new FormData();
formData.append('file', file);

const result = await $fetch('/api/files/upload', {
  method: 'POST',
  body: formData
});
```

## Deployment

### Docker Deployment

```dockerfile
FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

EXPOSE 3000
CMD ["npm", "run", "preview"]
```

### Environment Variables for Production

Ensure all environment variables are properly set:
- Use strong encryption keys
- Configure proper domain names
- Set up SSL certificates
- Configure firewall rules

## Health Checks

The portal includes health check endpoints:

- `GET /api/health` - Overall system health
- Check database connectivity
- Check file storage connectivity
- Check authentication service

## Security

- All sessions are encrypted
- HTTPS required in production
- CSRF protection enabled
- Secure cookie settings
- Input validation on all APIs

## Support

For issues and questions:
1. Check the documentation
2. Review environment configuration
3. Check health endpoints
4. Review server logs

## License

[Your License Here]
```

### 10.4 Create TypeScript Configuration (tsconfig.json)

```json
{
  "extends": "./.nuxt/tsconfig.json"
}
```

### 10.5 Create Git Ignore (.gitignore)

```gitignore
# Nuxt dev/build outputs
.output
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
*.log*

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
```

## Phase 11: Testing and Verification

### 11.1 Development Testing

After setting up the project, test the following:

1. **Authentication Flow**
   ```bash
   npm run dev
   # Visit http://localhost:3000
   # Test login/logout flow
   ```

2. **API Endpoints**
   ```bash
   # Test health check
   curl http://localhost:3000/api/health
   
   # Test authentication session
   curl http://localhost:3000/api/auth/session
   ```

3. **File Upload**
   - Test file upload functionality
   - Verify MinIO storage
   - Check file download

4. **Database Operations**
   - Test CRUD operations
   - Verify NocoDB integration
   - Check data persistence

### 11.2 Production Checklist

Before deploying to production:

- [ ] All environment variables configured
- [ ] SSL certificates installed
- [ ] Keycloak client properly configured
- [ ] NocoDB accessible and secured
- [ ] MinIO bucket policies configured
- [ ] Health checks passing
- [ ] PWA manifest and icons in place
- [ ] Error handling tested
- [ ] Mobile responsiveness verified

## Phase 12: Deployment Guide

### 12.1 Server Requirements

- Node.js 18+
- SSL certificate
- Reverse proxy (nginx/Apache)
- Firewall configuration

### 12.2 Environment Setup

1. **Production Environment Variables**
   ```bash
   # Copy and configure
   cp .env.example .env.production
   ```

2. **Build Application**
   ```bash
   npm run build
   ```

3. **Start Production Server**
   ```bash
   npm run preview
   # or use PM2 for process management
   pm2 start ecosystem.config.js
   ```

### 12.3 Nginx Configuration Example

```nginx
server {
    listen 443 ssl http2;
    server_name monacousa.org;

    ssl_certificate /path/to/certificate.crt;
    ssl_certificate_key /path/to/private.key;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}
```

## Conclusion

This implementation guide provides a complete foundation for the MonacoUSA Portal. The portal includes:

✅ **Complete Authentication System** with Keycloak integration
✅ **Responsive Dashboard** that works on all devices
✅ **File Storage System** with MinIO integration
✅ **Database Integration** with NocoDB
✅ **PWA Support** for mobile installation
✅ **Modern UI** with Vuetify and MonacoUSA branding
✅ **Modular Architecture** for easy extension
✅ **Production-Ready** configuration and deployment guide

The foundation is ready for you to build your custom tools and features on top of this solid, proven architecture.
