From 7ea6af3c28e6a63d12baade618c249cdd46cac77 Mon Sep 17 00:00:00 2001
From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com>
Date: Sat, 9 May 2026 10:34:37 +0700
Subject: [PATCH] Update user login, form
---
BACKEND/app/controllers/auth_controller.ts | 27 +++-
BACKEND/app/models/user.ts | 6 +
BACKEND/app/services/line_connection.ts | 131 ++++++++++++------
...add_first_name_last_name_to_users_table.ts | 19 +++
BACKEND/providers/socket_io_provider.ts | 4 +-
.../Modal/ModalConfirmRunPhysicalTest.tsx | 21 ++-
6 files changed, 153 insertions(+), 55 deletions(-)
create mode 100644 BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts
diff --git a/BACKEND/app/controllers/auth_controller.ts b/BACKEND/app/controllers/auth_controller.ts
index b423cc5..22cabea 100644
--- a/BACKEND/app/controllers/auth_controller.ts
+++ b/BACKEND/app/controllers/auth_controller.ts
@@ -6,7 +6,7 @@ export default class AuthController {
// Đăng ký
async register({ request, response }: HttpContext) {
try {
- const data = request.only(['email', 'password', 'user_name'])
+ const data = request.only(['email', 'password', 'user_name', 'first_name', 'last_name'])
const user = await User.query().where('user_name', data.user_name).first()
@@ -23,7 +23,12 @@ export default class AuthController {
// Đăng nhập
async login({ request, auth, response }: HttpContext) {
- const { user_name: userName, password } = request.only(['user_name', 'password'])
+ const { user_name: userName, password } = request.only([
+ 'user_name',
+ 'password',
+ 'first_name',
+ 'last_name',
+ ])
const user = await User.query().where('user_name', userName).first()
if (!user) {
@@ -47,10 +52,18 @@ export default class AuthController {
email: remoteUser.userEmail,
userName: userName,
password: password,
+ firstName: remoteUser?.firstName || null,
+ lastName: remoteUser?.lastName || null,
})
return response.json({
message: 'Login successful',
- user: { id: newUser.id, email: newUser.email, userName: newUser.userName },
+ user: {
+ id: newUser.id,
+ email: newUser.email,
+ userName: newUser.userName,
+ firstName: newUser.firstName,
+ lastName: newUser.lastName,
+ },
})
}
@@ -62,7 +75,13 @@ export default class AuthController {
return response.json({
message: 'Login successful',
- user: { id: user.id, email: user.email, userName: user.userName },
+ user: {
+ id: user.id,
+ email: user.email,
+ userName: user.userName,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ },
})
} catch {
return response.status(401).json({ message: 'Invalid credentials' })
diff --git a/BACKEND/app/models/user.ts b/BACKEND/app/models/user.ts
index e5068e2..ce897bf 100644
--- a/BACKEND/app/models/user.ts
+++ b/BACKEND/app/models/user.ts
@@ -14,6 +14,12 @@ export default class User extends BaseModel {
@column()
declare password: string
+ @column()
+ declare firstName: string | null
+
+ @column()
+ declare lastName: string | null
+
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts
index 1aad6c9..ed5011a 100644
--- a/BACKEND/app/services/line_connection.ts
+++ b/BACKEND/app/services/line_connection.ts
@@ -545,10 +545,11 @@ export default class LineConnection {
if (
['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)
) {
- const dataInventory = JSON.parse(item.textfsm)[0]
+ const listInventory = JSON.parse(item.textfsm)
+ const dataInventory = listInventory[0]
this.config.inventory = this.config.inventory
- ? { ...this.config.inventory, ...dataInventory }
- : dataInventory
+ ? { ...this.config.inventory, ...dataInventory, listInventory }
+ : { ...dataInventory, listInventory }
pid = dataInventory?.pid || ''
this.addHistory(this.config.stationId, this.config.id, {
id: this.config.id,
@@ -807,9 +808,10 @@ export default class LineConnection {
data.forEach((item) => {
if (item?.textfsm && isValidJson(item?.textfsm)) {
if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) {
- const dataInventory = JSON.parse(item.textfsm)[0]
+ const listInventory = JSON.parse(item.textfsm)
+ const dataInventory = listInventory[0]
this.config.inventory = this.config.inventory
- ? { ...this.config.inventory, ...dataInventory }
+ ? { ...this.config.inventory, ...dataInventory, listInventory }
: dataInventory
}
item.textfsm = JSON.parse(item.textfsm)
@@ -1164,7 +1166,7 @@ Ports Missing/Down: ${missing.length}\n\n`
/**
* Starting physical test (PoE ports testing)
*/
- async runPhysicalTest() {
+ async runPhysicalTest(userName?: string) {
if (this.config.runningPhysical) {
console.log('Running physical test')
return
@@ -1964,13 +1966,13 @@ Ports Missing/Down: ${missing.length}\n\n`
: '—'
// ---- Template-fallback values (use file's hardcoded content when no real data) ----
- const productName = 'Cisco Catalyst 9300-48P-A'
- const productPN = escapeHtml(String(config?.inventory?.pid || 'C9300-48P-A'))
- const productSN = escapeHtml(String(config?.inventory?.sn || 'FCW2425L0KP'))
- const productVid = escapeHtml(String(config?.inventory?.vid || 'V02'))
- const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || '17.09.04a'))
- const memDisplay = escapeHtml(memText !== '—' ? memText : '8 GB')
- const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '16 GB')
+ const productName = escapeHtml(String(config?.inventory?.name || ''))
+ const productPN = escapeHtml(String(config?.inventory?.pid || ''))
+ const productSN = escapeHtml(String(config?.inventory?.sn || ''))
+ const productVid = escapeHtml(String(config?.inventory?.vid || ''))
+ const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || ''))
+ const memDisplay = escapeHtml(memText !== '—' ? memText : '')
+ const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '')
// AI issue rows (one per real AI issue, fall back to file's hardcoded row when none)
const aiIssueRowsHtml =
@@ -1988,6 +1990,7 @@ Ports Missing/Down: ${missing.length}\n\n`
const licenseBoxesHtml =
dataShowLic && dataShowLic.length > 0
? dataShowLic
+ .filter((l) => l.LICENSE_TYPE && l.FEATURE)
.map(
(l: any) =>
`
${escapeHtml(String(l.FEATURE || ''))}
${escapeHtml(String(l.LICENSE_TYPE || ''))}${l.STATUS ? ' · ' + escapeHtml(String(l.STATUS)) : ''}
`
@@ -1997,8 +2000,8 @@ Ports Missing/Down: ${missing.length}\n\n`
// Port stat values (real numbers if any port data, else file's defaults)
const hasPortData = portPhysical.length > 0
- const poeText = hasPortData ? `${testedPoE.length}/${totalPoE}` : '48/48'
- const sfpText = hasPortData ? `${testedSFP.length}/${totalSFP}` : '4/4'
+ const poeText = hasPortData ? `${testedPoE.length}/${totalPoE}` : '0/0'
+ const sfpText = hasPortData ? `${testedSFP.length}/${totalSFP}` : '0/0'
const poeColor =
!hasPortData || (totalPoE > 0 && testedPoE.length === totalPoE) ? '#10b981' : '#f59e0b'
const sfpColor =
@@ -2104,7 +2107,7 @@ Ports Missing/Down: ${missing.length}\n\n`
-
+
|
Product Info
@@ -2220,27 +2223,65 @@ Ports Missing/Down: ${missing.length}\n\n`
+
+
- |
- ✓
- Received
- Trung Nguyen
- 06 May 10:30
- |
-
- ✓
- Physical Check
- Khanh Le
- 06 May 11:15
- |
-
- ✓
- Software Test
- Duy Pham (remote)
- 06 May 14:00
- |
+
+
+
+
+ |
-
+
+ |
+
+ |
+
+ ✓
+
+
+ Received
+
+
+ Trung Nguyen
+
+
+ 06 May 10:30
+
+ |
+
+
+
+ ✓
+
+
+
+ Physical Check
+
+
+ Khanh Le
+
+
+ 06 May 11:15
+
+ |
+
+
+
+ ✓
+
+
+ Software Test
+
+
+ Duy Pham (remote)
+
+
+ 06 May 14:00
+
+ |
+
+
|
@@ -2310,11 +2351,15 @@ Ports Missing/Down: ${missing.length}\n\n`
Hardware Inventory
- | ${productPN} | ${productSN} |
- | PWR-C1-715WAC-P | LIT241525W1 |
- | C9300-NM-4G | FDO2420H0X1 |
- | FAN-T2-GEN2 (x3) | OK |
-
+ ${
+ this.config?.inventory?.listInventory
+ ?.map(
+ (item: any) => `
+ | | ${item.pid} | ${item.sn} | `
+ )
+ .join('') || ''
+ }
+
System & License
@@ -2328,8 +2373,8 @@ Ports Missing/Down: ${missing.length}\n\n`
| ${escapeHtml(sfpText)} SFP+ UP |
|
- ${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'} PoE+ Test |
|
- ${Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}% Throughput |
|
+ ${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'} PoE+ Test |
|
+ ${totalPoE + totalSFP === 0 ? 100 : Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}% Throughput |
|
${missingDetailsHtml}
@@ -2340,7 +2385,7 @@ Ports Missing/Down: ${missing.length}\n\n`
| CONSOLE RAW OUTPUT (Boot Log snippet) |
- ${snapshot?.outputTestLog || 'No test log available'}
+ ${snapshot?.outputTestLog || 'No test log available'} |
|
diff --git a/BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts b/BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts
new file mode 100644
index 0000000..800c368
--- /dev/null
+++ b/BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts
@@ -0,0 +1,19 @@
+import { BaseSchema } from '@adonisjs/lucid/schema'
+
+export default class extends BaseSchema {
+ protected tableName = 'users'
+
+ async up() {
+ this.schema.alterTable(this.tableName, (table) => {
+ table.string('first_name').nullable().after('user_name')
+ table.string('last_name').nullable().after('first_name')
+ })
+ }
+
+ async down() {
+ this.schema.alterTable(this.tableName, (table) => {
+ table.dropColumn('first_name')
+ table.dropColumn('last_name')
+ })
+ }
+}
diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts
index 89c936d..b540061 100644
--- a/BACKEND/providers/socket_io_provider.ts
+++ b/BACKEND/providers/socket_io_provider.ts
@@ -728,7 +728,7 @@ export class WebSocketIo {
})
socket.on('run_physical_test', async (data) => {
- const { stationId, lineId } = data
+ const { stationId, lineId, userName: name } = data
// Check station is active
const activeStation = await checkStationActive(stationId)
if (!activeStation) return
@@ -737,7 +737,7 @@ export class WebSocketIo {
io,
stationId,
[lineId],
- async (lineCon) => lineCon.runPhysicalTest(),
+ async (lineCon) => lineCon.runPhysicalTest(name),
{}
)
})
diff --git a/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx b/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx
index ffb7a69..6bb5f6d 100644
--- a/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx
+++ b/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import {
Modal,
Box,
@@ -33,6 +33,12 @@ export default function ModalConfirmRunPhysical({
socket,
station,
}: Props) {
+ const user = useMemo(() => {
+ return localStorage.getItem("user") &&
+ typeof localStorage.getItem("user") === "string"
+ ? JSON.parse(localStorage.getItem("user") || "")
+ : null;
+ }, []);
const [dataLines, setDataLines] = useState([]);
const [isDisabled, setIsDisabled] = useState(false);
@@ -48,7 +54,7 @@ export default function ModalConfirmRunPhysical({
sn: line?.inventory?.sn,
vid: line?.inventory?.vid,
checked: true,
- }))
+ })),
);
}
}, [listLines]);
@@ -100,8 +106,8 @@ export default function ModalConfirmRunPhysical({
...el,
checked: e.target.checked,
}
- : el
- )
+ : el,
+ ),
)
}
/>
@@ -133,13 +139,16 @@ export default function ModalConfirmRunPhysical({
socket?.emit("run_physical_test", {
lineId: line?.id,
stationId: Number(station?.id),
+ userName: user?.firstName
+ ? `${user.firstName} ${user.lastName || ""}`
+ : user?.userName || "Unknown User",
});
});
setDataLines(listNotChecked);
setListLines(
listLines.filter((el) =>
- listNotChecked.find((li) => li.id === el.id)
- )
+ listNotChecked.find((li) => li.id === el.id),
+ ),
);
setIsDisabled(true);
setTimeout(() => {