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 07:33:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2021 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5           92 : 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           92 : import cockpit from "cockpit";
      15              : 
      16              : import './lastLogin.scss';
      17              : 
      18           92 : 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           61 :     if (host && line) {
      24           61 :         message = cockpit.format(_("from <host> on <terminal>", "from $0 on $1"), host, line);
      25           14 :     } else if (host) {
      26           31 :         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           92 : const LastLogin = () => {
      50           92 :     const [messages, setLoginMessages] = useState<LoginMessages | null>(null);
      51           92 :     const [lastlog, setLastlog] = useState<LastlogEntry | null>(null);
      52           92 :     const [name, setName] = useState<string | null>(null);
      53              : 
      54           92 :     useInit(async () => {
      55           92 :         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           69 :             if (obj.version == 1) {
      63           69 :                 setLoginMessages(obj);
      64           16 :             } else {
      65              :                 // empty reply is okay -- older bridges just don't send that information
      66           29 :                 if (obj.version !== undefined)
      67           16 :                     console.error("unknown login-messages:", reply);
      68           29 :             }
      69           16 :         } catch (error) {
      70           16 :             console.error("failed to fetch login messages:", error);
      71           16 :         }
      72              : 
      73           82 :         try {
      74           75 :             setLastlog((await getLastlog2(user.name))[user.name]);
      75           16 :         } catch (error) {
      76           16 :             console.log("failed to fetch lastlog2:", error);
      77           16 :         }
      78           92 :     });
      79              : 
      80           33 :     const lastLoginTime: number | undefined = messages?.['last-login-time'] ?? lastlog?.time;
      81           92 :     if (!lastLoginTime)
      82           92 :         return null;
      83              : 
      84           33 :     const lastLoginFrom = messages?.['last-login-host'] || lastlog?.host;
      85           33 :     const lastLoginPort = messages?.['last-login-line'] || lastlog?.tty;
      86              : 
      87           92 :     let icon = null;
      88           92 :     let headerText = null;
      89           92 :     let underlineText = null;
      90           92 :     let headerClass = "pf-v6-u-text-break-word";
      91           92 :     let underlineClass = "pf-v6-u-text-break-word";
      92           92 :     const lastLoginText = _("Last successful login:") + " " + getFormattedDateTime(lastLoginTime * 1000);
      93           68 :     const failedLogins = messages?.['fail-count'];
      94              : 
      95           16 :     if (failedLogins) {
      96           16 :         let iconClass = "system-information-failed-login-warning-icon";
      97           16 :         if (failedLogins > 5) {
      98           16 :             iconClass = "system-information-failed-login-danger-icon";
      99           16 :             headerClass += " system-information-failed-login-danger";
     100           16 :         } else {
     101           16 :             headerClass += " system-information-failed-login-warning";
     102           16 :         }
     103              : 
     104           16 :         icon = <ExclamationTriangleIcon className={iconClass} />;
     105           16 :         headerText = cockpit.format(cockpit.ngettext("$0 failed login attempt", "$0 failed login attempts", failedLogins), failedLogins);
     106           16 :         underlineText = getFormattedDateTime(lastLoginTime * 1000) + " " + generate_line(lastLoginFrom, lastLoginPort);
     107           16 :     } 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           16 :                     <div id="system_last_login_success" className="pf-v6-u-text-break-word">
     131           16 :                         {lastLoginText}
     132           16 :                     </div>
     133              :                     }
     134           92 :                     {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           92 :                 </div>
     142           92 :             </Flex>
     143           92 :         </li>
     144              :     );
     145           92 : };
     146              : 
     147           92 : export default LastLogin;
        

Generated by: LCOV version 2.0-1