-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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
- IonCube: Remove license validation logic. This is no longer needed with TypeScript.
- phpMoAdmin: Remove mo.php. Use MongoDB Compass instead.
- Session files: Replace PHP sessions with nuxt-auth-utils sealed sessions.
- Manual routing: Remove query parameter routing logic from the legacy index.