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 76 : const generate_line = (host?: string, line?: string): string => {
22 76 : let message = "";
23 59 : if (host && line) {
24 59 : 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 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 14 : 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 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 82 : setName(user.name);
57 :
58 82 : const bridge = cockpit.dbus(null, { bus: "internal" });
59 82 : try {
60 82 : const [reply] = await bridge.call("/LoginMessages", "cockpit.LoginMessages", "Get", []);
61 81 : const obj = JSON.parse(reply as string);
62 68 : if (obj.version == 1) {
63 68 : setLoginMessages(obj);
64 17 : } else {
65 : // empty reply is okay -- older bridges just don't send that information
66 30 : if (obj.version !== undefined)
67 17 : console.error("unknown login-messages:", reply);
68 30 : }
69 17 : } catch (error) {
70 17 : console.error("failed to fetch login messages:", error);
71 17 : }
72 :
73 81 : try {
74 76 : setLastlog((await getLastlog2(user.name))[user.name]);
75 17 : } catch (error) {
76 17 : console.log("failed to fetch lastlog2:", error);
77 17 : }
78 92 : });
79 :
80 34 : const lastLoginTime: number | undefined = messages?.['last-login-time'] ?? lastlog?.time;
81 92 : if (!lastLoginTime)
82 92 : 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 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 67 : 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 79 : icon = <UserIcon className="system-information-last-login-icon" />;
109 79 : headerText = lastLoginText;
110 79 : underlineClass += " ct-grey-text pf-v6-u-font-size-sm";
111 79 : underlineText = generate_line(lastLoginFrom, lastLoginPort);
112 79 : }
113 :
114 79 : return (
115 79 : <li className="last-login" id="page_status_last_login">
116 79 : <Flex flexWrap={{ default: 'nowrap' }}>
117 79 : <FlexItem>{icon}</FlexItem>
118 79 : <div>
119 79 : <div id="system_last_login"
120 79 : className={headerClass}
121 : >
122 79 : {headerText}
123 79 : </div>
124 79 : <div id="system_last_login_from"
125 79 : className={underlineClass}
126 : >
127 79 : {underlineText}
128 79 : </div>
129 79 : {failedLogins &&
130 17 : <div id="system_last_login_success" className="pf-v6-u-text-break-word">
131 17 : {lastLoginText}
132 17 : </div>
133 : }
134 92 : {name &&
135 79 : <Button variant="link" isInline
136 79 : className="pf-v6-u-font-size-sm"
137 1 : onClick={() => cockpit.jump("/users#/" + name)}>
138 79 : {_("View login history")}
139 79 : </Button>
140 : }
141 92 : </div>
142 92 : </Flex>
143 92 : </li>
144 : );
145 92 : };
146 :
147 92 : export default LastLogin;
|