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