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 12:03:54

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2021 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5           93 : 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           93 : import cockpit from "cockpit";
      15              : 
      16              : import './lastLogin.scss';
      17              : 
      18           93 : const _ = cockpit.gettext;
      19              : 
      20              : // Do the full combinatorial thing to improve translatability
      21           76 : const generate_line = (host?: string, line?: string): string => {
      22           76 :     let message = "";
      23           58 :     if (host && line) {
      24           58 :         message = cockpit.format(_("from <host> on <terminal>", "from $0 on $1"), host, line);
      25           13 :     } else if (host) {
      26           31 :         message = cockpit.format(_("from <host>", "from $0"), host);
      27           13 :     } else if (line) {
      28           13 :         message = cockpit.format(_("on <terminal>", "on $0"), line);
      29           13 :     }
      30           76 :     return message;
      31           76 : };
      32              : 
      33           76 : const getFormattedDateTime = (time: number): string => {
      34           76 :     const now = new Date();
      35           76 :     const date = new Date(time);
      36           76 :     if (date.getFullYear() == now.getFullYear()) {
      37           76 :         return timeformat.dateTimeNoYear(date);
      38           76 :     }
      39           13 :     return timeformat.dateTime(date);
      40           76 : };
      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           81 :         setName(user.name);
      57              : 
      58           81 :         const bridge = cockpit.dbus(null, { bus: "internal" });
      59           81 :         try {
      60           81 :             const [reply] = await bridge.call("/LoginMessages", "cockpit.LoginMessages", "Get", []);
      61           80 :             const obj = JSON.parse(reply as string);
      62           66 :             if (obj.version == 1) {
      63           66 :                 setLoginMessages(obj);
      64           16 :             } else {
      65              :                 // empty reply is okay -- older bridges just don't send that information
      66           30 :                 if (obj.version !== undefined)
      67           16 :                     console.error("unknown login-messages:", reply);
      68           30 :             }
      69           16 :         } catch (error) {
      70           16 :             console.error("failed to fetch login messages:", error);
      71           16 :         }
      72              : 
      73           80 :         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           93 :     });
      79              : 
      80           34 :     const lastLoginTime: number | undefined = messages?.['last-login-time'] ?? lastlog?.time;
      81           93 :     if (!lastLoginTime)
      82           93 :         return null;
      83              : 
      84           34 :     const lastLoginFrom = messages?.['last-login-host'] || lastlog?.host;
      85           34 :     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           66 :     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           78 :         icon = <UserIcon className="system-information-last-login-icon" />;
     109           78 :         headerText = lastLoginText;
     110           78 :         underlineClass += " ct-grey-text pf-v6-u-font-size-sm";
     111           78 :         underlineText = generate_line(lastLoginFrom, lastLoginPort);
     112           78 :     }
     113              : 
     114           78 :     return (
     115           78 :         <li className="last-login" id="page_status_last_login">
     116           78 :             <Flex flexWrap={{ default: 'nowrap' }}>
     117           78 :                 <FlexItem>{icon}</FlexItem>
     118           78 :                 <div>
     119           78 :                     <div id="system_last_login"
     120           78 :                             className={headerClass}
     121              :                     >
     122           78 :                         {headerText}
     123           78 :                     </div>
     124           78 :                     <div id="system_last_login_from"
     125           78 :                               className={underlineClass}
     126              :                     >
     127           78 :                         {underlineText}
     128           78 :                     </div>
     129           78 :                     {failedLogins &&
     130           16 :                     <div id="system_last_login_success" className="pf-v6-u-text-break-word">
     131           16 :                         {lastLoginText}
     132           16 :                     </div>
     133              :                     }
     134           93 :                     {name &&
     135           78 :                         <Button variant="link" isInline
     136           78 :                                 className="pf-v6-u-font-size-sm"
     137            1 :                                 onClick={() => cockpit.jump("/users#/" + name)}>
     138           78 :                             {_("View login history")}
     139           78 :                         </Button>
     140              :                     }
     141           93 :                 </div>
     142           93 :             </Flex>
     143           93 :         </li>
     144              :     );
     145           93 : };
     146              : 
     147           93 : export default LastLogin;
        

Generated by: LCOV version 2.0-1