Skip to content

Migration Plan: DDI Legacy PHP to Nuxt 3 Fullstack #1

@jpalanco

Description

@jpalanco

Description

This is a comprehensive migration plan for converting the DDI system from legacy PHP to a modern Nuxt 3 fullstack application with TypeScript, MongoDB, NATS, and modern authentication.

Current System Analysis

The legacy PHP application (drainware/ddi) currently utilizes the following architecture and patterns:

  • Manual routing via ?module=X&action=Y query parameters in ddi/index.php.
  • MD5 password hashing is used in both CloudModel and UserModel.
  • Session-based authentication with role checks is performed in index.php.
  • AMQP messaging is used for server communication.
  • IonCube license validation is active (lines 22-61 in index.php).
  • phpMoAdmin is used for MongoDB management (mo.php).

Migration Architecture

Technology Stack

  • Frontend: Nuxt 3 + Vue 3 + Tailwind CSS
  • Backend: Nuxt Nitro server routes (RESTful API)
  • Database: MongoDB via Mongoose (already in use)
  • Messaging: NATS with JetStream (replacing AMQP)
  • Authentication: nuxt-auth-utils (secure sealed sessions)
  • Excel Reports: exceljs (replacing PHP Excel libraries)

Project Structure

server/
├── api/ # REST controllers
│ ├── auth/
│ ├── users/
│ ├── groups/
│ └── reports/
├── models/ # Mongoose schemas
├── middleware/ # Role-based authorization
├── utils/ # Password hashing, NATS helpers
└── plugins/ # DB & NATS connections

Implementation Steps

Phase 1: Foundation Setup

Install dependencies:

npx nuxi@latest init ddi-nuxt
cd ddi-nuxt
npm install mongoose nuxt-auth-utils bcrypt nats exceljs
npm install -D @types/bcrypt @types/node

Database connection (server/plugins/mongoose.ts):

import mongoose from 'mongoose';

export default defineNitroPlugin(async (nitroApp) => {
const config = useRuntimeConfig();
await mongoose.connect(config.mongodbUri);
console.log('[OK] Connected to MongoDB');
});

Phase 2: Data Models

User Model (server/models/User.ts):
This maps to the existing customers collection structure.
import { Schema, model } from 'mongoose';

const UserSchema = new Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['admin', 'user'], default: 'user' },
isLegacyHash: { type: Boolean, default: false },
license: String,
company: String,
country: String
}, { timestamps: true });

export const User = model('User', UserSchema);

Phase 3: Authentication Migration

Password verification utility (server/utils/password.ts):
This handles the progressive migration from MD5 (used in CloudModel::loginCloudUser) to bcrypt.
import bcrypt from 'bcrypt';
import crypto from 'crypto';

export const verifyPassword = async (inputPassword: string, user: any) => {
if (!user.isLegacyHash) {
return await bcrypt.compare(inputPassword, user.password);
}

// Legacy MD5 verification
const inputMd5 = crypto.createHash('md5').update(inputPassword).digest('hex');

if (inputMd5 === user.password) {
// Migrate to bcrypt on successful login
user.password = await bcrypt.hash(inputPassword, 10);
user.isLegacyHash = false;
await user.save();
return true;
}
return false;
};

Login endpoint (server/api/auth/login.post.ts):
This replaces the login logic in MainController::loginAction.
import { User } from '~/server/models/User';

export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event);

const user = await User.findOne({ email });
if (!user || !user.activate) {
throw createError({ statusCode: 401, message: 'Invalid credentials' });
}

const isValid = await verifyPassword(password, user);
if (!isValid) {
throw createError({ statusCode: 401, message: 'Invalid credentials' });
}

await setUserSession(event, {
user: {
id: user._id.toString(),
email: user.email,
role: user.role,
license: user.license
}
});

return { loggedIn: true, user: { email: user.email, role: user.role } };
});

Phase 4: Role-Based Authorization Middleware

Authorization middleware (server/middleware/auth.ts):
This replaces the role-checking logic in index.php.
export default defineEventHandler(async (event) => {
const url = getRequestURL(event).pathname;

// Protected routes requiring authentication
const protectedRoutes = ['/api/users', '/api/groups', '/api/reports'];

if (protectedRoutes.some(route => url.startsWith(route))) {
const session = await getUserSession(event);

if (\!session.user) {  
  throw createError({ statusCode: 401, message: 'Not authenticated' });  
}

// Admin-only routes  
const adminOnlyRoutes \= \['/api/users/create', '/api/groups/create'\];  
  
if (adminOnlyRoutes.some(route \=\> url.startsWith(route))) {  
  if (session.user.role \!== 'admin') {  
    throw createError({ statusCode: 403, message: 'Admin access required' });  
  }  
}  

}
});

Phase 5: NATS Messaging

NATS plugin (server/plugins/nats.ts):

import { connect, StringCodec } from 'nats';

let natsConnection: any = null;

export default defineNitroPlugin(async (nitroApp) => {
natsConnection = await connect({ servers: "nats://localhost:4222" });
console.log('[OK] Connected to NATS');

nitroApp.hooks.hook('close', async () => {
await natsConnection.close();
});
});

export const useNats = () => natsConnection;

Example usage (replacing AMQP calls like in MainController::saveWireTransferAction):

// server/api/config/update.post.ts
import { StringCodec } from 'nats';

export default defineEventHandler(async (event) => {
const body = await readBody(event);
const nc = useNats();
const sc = StringCodec();

nc.publish("server.atp.update", sc.encode(JSON.stringify({
module: 'atp',
command: 'update',
args: body
})));

return { status: 'queued' };
});

Phase 6: RESTful Routing

The legacy routing system is replaced by Nuxt's file-based routing.

Legacy Route New Route
?module=main&action=login pages/login.vue
?module=main&action=show pages/dashboard.vue
?module=user&action=show pages/users/index.vue
?module=main&action=showUserAuth pages/settings/auth.vue
?module=main&action=showCredentials pages/settings/credentials.vue

Phase 7: Excel Reports

Report endpoint (server/api/reports/users.get.ts):

import ExcelJS from 'exceljs';
import { User } from '~/server/models/User';

export default defineEventHandler(async (event) => {
setResponseHeader(event, 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
setResponseHeader(event, 'Content-Disposition', 'attachment; filename="users.xlsx"');

const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('Users');

sheet.columns = [
{ header: 'Email', key: 'email', width: 30 },
{ header: 'Company', key: 'company', width: 30 },
{ header: 'Role', key: 'role', width: 15 }
];

const cursor = User.find().cursor();
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
sheet.addRow({
email: doc.email,
company: doc.company,
role: doc.role
});
}

return await workbook.xlsx.writeBuffer();
});

Phase 8: LDAP Support

LDAP utility (server/utils/ldap.ts):
This replaces the LDAP functionality in MainController::saveUserAuthAction.
import ldap from 'ldapjs';

export async function authenticateLDAP(config: any, username: string, password: string) {
const client = ldap.createClient({
url: `${config.ssl ? 'ldaps' : 'ldap'}://${config.host}:${config.port}`
});

return new Promise((resolve, reject) => {
const dn = `${config.username_attr}=${username},${config.base}`;
client.bind(dn, password, (err) => {
client.unbind();
if (err) reject(err);
else resolve(true);
});
});
}

Removals

  1. IonCube: Remove license validation logic. This is no longer needed with TypeScript.
  2. phpMoAdmin: Remove mo.php. Use MongoDB Compass instead.
  3. Session files: Replace PHP sessions with nuxt-auth-utils sealed sessions.
  4. Manual routing: Remove query parameter routing logic from the legacy index.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions