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