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;
|