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-03 07:31:16

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

Generated by: LCOV version 2.0-1