LCOV - code coverage report
Current view: top level - pkg/systemd/overview-cards - lastLogin.tsx Coverage Total Hit
Test: cockpit Lines: 99.0 % 105 104
Test Date: 2026-07-02 14:11:36

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

Generated by: LCOV version 2.0-1