LCOV - code coverage report
Current view: top level - pkg/systemd/overview-cards - lastLogin.tsx Coverage Total Hit
Test: cockpit Lines: 100.0 % 105 105
Test Date: 2026-07-17 14:32:59

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2021 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5           94 : import React, { useState } from 'react';
       6              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
       7              : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
       8              : import { ExclamationTriangleIcon, UserIcon } from "@patternfly/react-icons";
       9              : 
      10              : import * as timeformat from "timeformat";
      11              : import { useInit } from "hooks";
      12              : import { LastlogEntry, getLastlog2 } from "logins";
      13              : 
      14           94 : import cockpit from "cockpit";
      15              : 
      16              : import './lastLogin.scss';
      17              : 
      18           94 : const _ = cockpit.gettext;
      19              : 
      20              : // Do the full combinatorial thing to improve translatability
      21           78 : const generate_line = (host?: string, line?: string): string => {
      22           78 :     let message = "";
      23           60 :     if (host && line) {
      24           60 :         message = cockpit.format(_("from <host> on <terminal>", "from $0 on $1"), host, line);
      25           14 :     } else if (host) {
      26           32 :         message = cockpit.format(_("from <host>", "from $0"), host);
      27           14 :     } else if (line) {
      28           14 :         message = cockpit.format(_("on <terminal>", "on $0"), line);
      29           14 :     }
      30           78 :     return message;
      31           78 : };
      32              : 
      33           78 : const getFormattedDateTime = (time: number): string => {
      34           78 :     const now = new Date();
      35           78 :     const date = new Date(time);
      36           78 :     if (date.getFullYear() == now.getFullYear()) {
      37           78 :         return timeformat.dateTimeNoYear(date);
      38           78 :     }
      39           14 :     return timeformat.dateTime(date);
      40           78 : };
      41              : 
      42              : type LoginMessages = {
      43              :     "last-login-time": number,
      44              :     "last-login-host": string,
      45              :     "last-login-line": string,
      46              :     "fail-count"?: number,
      47              : };
      48              : 
      49           93 : const LastLogin = () => {
      50           93 :     const [messages, setLoginMessages] = useState<LoginMessages | null>(null);
      51           93 :     const [lastlog, setLastlog] = useState<LastlogEntry | null>(null);
      52           93 :     const [name, setName] = useState<string | null>(null);
      53              : 
      54           93 :     useInit(async () => {
      55           93 :         const user = await cockpit.user();
      56           84 :         setName(user.name);
      57              : 
      58           84 :         const bridge = cockpit.dbus(null, { bus: "internal" });
      59           84 :         try {
      60           84 :             const [reply] = await bridge.call("/LoginMessages", "cockpit.LoginMessages", "Get", []);
      61           82 :             const obj = JSON.parse(reply as string);
      62           68 :             if (obj.version == 1) {
      63           68 :                 setLoginMessages(obj);
      64           17 :             } else {
      65              :                 // empty reply is okay -- older bridges just don't send that information
      66           31 :                 if (obj.version !== undefined)
      67           17 :                     console.error("unknown login-messages:", reply);
      68           31 :             }
      69           17 :         } catch (error) {
      70           17 :             console.error("failed to fetch login messages:", error);
      71           17 :         }
      72              : 
      73           82 :         try {
      74           76 :             setLastlog((await getLastlog2(user.name))[user.name]);
      75           17 :         } catch (error) {
      76           17 :             console.log("failed to fetch lastlog2:", error);
      77           17 :         }
      78           93 :     });
      79              : 
      80           35 :     const lastLoginTime: number | undefined = messages?.['last-login-time'] ?? lastlog?.time;
      81           93 :     if (!lastLoginTime)
      82           93 :         return null;
      83              : 
      84           35 :     const lastLoginFrom = messages?.['last-login-host'] || lastlog?.host;
      85           35 :     const lastLoginPort = messages?.['last-login-line'] || lastlog?.tty;
      86              : 
      87           93 :     let icon = null;
      88           93 :     let headerText = null;
      89           93 :     let underlineText = null;
      90           93 :     let headerClass = "pf-v6-u-text-break-word";
      91           93 :     let underlineClass = "pf-v6-u-text-break-word";
      92           93 :     const lastLoginText = _("Last successful login:") + " " + getFormattedDateTime(lastLoginTime * 1000);
      93           68 :     const failedLogins = messages?.['fail-count'];
      94              : 
      95           17 :     if (failedLogins) {
      96           17 :         let iconClass = "system-information-failed-login-warning-icon";
      97           17 :         if (failedLogins > 5) {
      98           17 :             iconClass = "system-information-failed-login-danger-icon";
      99           17 :             headerClass += " system-information-failed-login-danger";
     100           17 :         } else {
     101           17 :             headerClass += " system-information-failed-login-warning";
     102           17 :         }
     103              : 
     104           17 :         icon = <ExclamationTriangleIcon className={iconClass} />;
     105           17 :         headerText = cockpit.format(cockpit.ngettext("$0 failed login attempt", "$0 failed login attempts", failedLogins), failedLogins);
     106           17 :         underlineText = getFormattedDateTime(lastLoginTime * 1000) + " " + generate_line(lastLoginFrom, lastLoginPort);
     107           17 :     } else {
     108           80 :         icon = <UserIcon className="system-information-last-login-icon" />;
     109           80 :         headerText = lastLoginText;
     110           80 :         underlineClass += " ct-grey-text pf-v6-u-font-size-sm";
     111           80 :         underlineText = generate_line(lastLoginFrom, lastLoginPort);
     112           80 :     }
     113              : 
     114           80 :     return (
     115           80 :         <li className="last-login" id="page_status_last_login">
     116           80 :             <Flex flexWrap={{ default: 'nowrap' }}>
     117           80 :                 <FlexItem>{icon}</FlexItem>
     118           80 :                 <div>
     119           80 :                     <div id="system_last_login"
     120           80 :                             className={headerClass}
     121              :                     >
     122           80 :                         {headerText}
     123           80 :                     </div>
     124           80 :                     <div id="system_last_login_from"
     125           80 :                               className={underlineClass}
     126              :                     >
     127           80 :                         {underlineText}
     128           80 :                     </div>
     129           80 :                     {failedLogins &&
     130           17 :                     <div id="system_last_login_success" className="pf-v6-u-text-break-word">
     131           17 :                         {lastLoginText}
     132           17 :                     </div>
     133              :                     }
     134           93 :                     {name &&
     135           80 :                         <Button variant="link" isInline
     136           80 :                                 className="pf-v6-u-font-size-sm"
     137            1 :                                 onClick={() => cockpit.jump("/users#/" + name)}>
     138           80 :                             {_("View login history")}
     139           80 :                         </Button>
     140              :                     }
     141           93 :                 </div>
     142           93 :             </Flex>
     143           93 :         </li>
     144              :     );
     145           93 : };
     146              : 
     147           94 : export default LastLogin;
        

Generated by: LCOV version 2.0-1