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