Update user login, form

This commit is contained in:
nguyentrungthat 2026-05-09 10:34:37 +07:00
parent 95601a02cb
commit 7ea6af3c28
6 changed files with 153 additions and 55 deletions

View File

@ -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' })

View File

@ -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

View File

@ -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) =>
`<div style="background:#f9fafb;border:1px solid #f0f1f3;border-radius:6px;padding:8px 12px;margin-bottom:6px;"><div style="font-weight:700;color:#3b82f6;font-size:13px;">${escapeHtml(String(l.FEATURE || ''))}</div><div style="font-size:10px;color:#9ca3af;">${escapeHtml(String(l.LICENSE_TYPE || ''))}${l.STATUS ? ' · ' + escapeHtml(String(l.STATUS)) : ''}</div></div>`
@ -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`
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="50%" valign="top" style="padding-right:5px;">
<table cellpadding="0" cellspacing="0" border="0" height="265px" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
<table cellpadding="0" cellspacing="0" border="0" height="265px" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate; font-size:14px;">
<tr><td style="padding:16px 20px;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:8px;">Product Info</div>
<table cellpadding="0" cellspacing="0" border="0" width="100%">
@ -2220,27 +2223,65 @@ Ports Missing/Down: ${missing.length}\n\n`
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
<tr><td style="padding:16px 20px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<!-- line -->
<tbody style="position:relative; z-index:0;">
<tr>
<td width="33%" align="center" style="padding:4px;">
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-bottom:8px;">&#10003;</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">Received</div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;">Trung Nguyen</div>
<div style="font-size:10px;color:#9ca3af;">06 May 10:30</div>
</td>
<td width="33%" align="center" style="padding:4px;">
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-bottom:8px;">&#10003;</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">Physical Check</div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;">Khanh Le</div>
<div style="font-size:10px;color:#9ca3af;">06 May 11:15</div>
</td>
<td width="34%" align="center" style="padding:4px;">
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-bottom:8px;">&#10003;</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">Software Test</div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;">Duy Pham (remote)</div>
<div style="font-size:10px;color:#9ca3af;">06 May 14:00</div>
</td>
<td colspan="3" style="padding:0 55px;">
<div style="height:2px;background:#e2e8f0;font-size:0;line-height:0; position:absolute; z-index:-1; width: 90%; top: 12px;">
&nbsp;
</div>
</td>
</tr>
</table>
<!-- steps -->
<tr>
<!-- Step 1 -->
<td width="33%" align="center" valign="top" style="padding:0 4px 4px 4px;">
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-top:-14px;margin-bottom:8px;">
&#10003;
</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">
Received
</div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;">
Trung Nguyen
</div>
<div style="font-size:10px;color:#9ca3af;">
06 May 10:30
</div>
</td>
<!-- Step 2 -->
<td width="33%" align="center" valign="top" style="padding:0 4px 4px 4px;">
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-top:-14px;margin-bottom:8px;">
&#10003;
</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">
Physical Check
</div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;">
Khanh Le
</div>
<div style="font-size:10px;color:#9ca3af;">
06 May 11:15
</div>
</td>
<!-- Step 3 -->
<td width="34%" align="center" valign="top" style="padding:0 4px 4px 4px;">
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-top:-14px;margin-bottom:8px;">
&#10003;
</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">
Software Test
</div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;">
Duy Pham (remote)
</div>
<div style="font-size:10px;color:#9ca3af;">
06 May 14:00
</div>
</td>
</tr></tbody>
</table>
</td></tr>
</table>
</td></tr>
@ -2310,11 +2351,15 @@ Ports Missing/Down: ${missing.length}\n\n`
<td width="33%" valign="top" style="padding-right:8px;">
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">Hardware Inventory</div>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="font-size:11px;">
<tr><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-weight:600;color:#5f6978;">${productPN}</td><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">${productSN}</td></tr>
<tr><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-weight:600;color:#5f6978;">PWR-C1-715WAC-P</td><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">LIT241525W1</td></tr>
<tr><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-weight:600;color:#5f6978;">C9300-NM-4G</td><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">FDO2420H0X1</td></tr>
<tr><td style="padding:2px 0;font-weight:600;color:#5f6978;">FAN-T2-GEN2 (x3)</td><td style="padding:2px 0;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">OK</td></tr>
</table>
${
this.config?.inventory?.listInventory
?.map(
(item: any) => `
<tr><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-weight:600;color:#5f6978;">${item.pid}</td><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">${item.sn}</td></tr>`
)
.join('') || ''
}
</table>
</td>
<td width="33%" valign="top" style="padding:0 4px;">
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">System &amp; License</div>
@ -2328,8 +2373,8 @@ Ports Missing/Down: ${missing.length}\n\n`
<td width="50%" style="padding:0 0 6px 3px;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:${sfpColor};">${escapeHtml(sfpText)}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">SFP+ UP</div></td></tr></table></td>
</tr>
<tr>
<td width="50%" style="padding:0 3px 0 0;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:#10b981;">${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">PoE+ Test</div></td></tr></table></td>
<td width="50%" style="padding:0 0 0 3px;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:#10b981;">${Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">Throughput</div></td></tr></table></td>
<td width="50%" style="padding:0 3px 0 0;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color: ${missingSFP.length > 0 || missingPoE.length > 0 ? '#dc2626' : '#10b981'};">${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">PoE+ Test</div></td></tr></table></td>
<td width="50%" style="padding:0 0 0 3px;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color: ${missingSFP.length > 0 || missingPoE.length > 0 ? '#dc2626' : '#10b981'};">${totalPoE + totalSFP === 0 ? 100 : Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">Throughput</div></td></tr></table></td>
</tr>
</table>
${missingDetailsHtml}
@ -2340,7 +2385,7 @@ Ports Missing/Down: ${missing.length}\n\n`
<!-- CONSOLE RAW OUTPUT -->
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-top:16px;background:#1e293b;border-radius:6px;border:1px solid #334155;border-collapse:separate;">
<tr><td style="padding:6px 12px;background:#334155;color:#94a3b8;font-size:10px;font-weight:700;letter-spacing:.5px;border-radius:6px 6px 0 0;">CONSOLE RAW OUTPUT (Boot Log snippet)</td></tr>
<tr><pre style="overflow-y: auto; max-height: 300px; padding:12px;color:#cbd5e1;font-family:Consolas,'Courier New',monospace;font-size:11px;line-height:1.6;white-space:pre-wrap;word-break:break-all;">${snapshot?.outputTestLog || 'No test log available'}</pre></tr>
<tr><td><pre style="overflow-y: auto; max-height: 300px; padding:12px;color:#cbd5e1;font-family:Consolas,'Courier New',monospace;font-size:11px;line-height:1.6;white-space:pre-wrap;word-break:break-all;">${snapshot?.outputTestLog || 'No test log available'}</pre></td></tr>
</table>
</td></tr>
</table>

View File

@ -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')
})
}
}

View File

@ -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),
{}
)
})

View File

@ -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<PropsLines[]>([]);
const [isDisabled, setIsDisabled] = useState<boolean>(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(() => {