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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions