You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2021/07/16 23:04:45 UTC
[trafficcontrol] branch master updated: Logic to capture/display
the last time a user successfully authenticated (#6009)
This is an automated email from the ASF dual-hosted git repository.
ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new b7ced4a Logic to capture/display the last time a user successfully authenticated (#6009)
b7ced4a is described below
commit b7ced4a8c1afe439eec8805e23a11957db822e6f
Author: Rima Shah <22...@users.noreply.github.com>
AuthorDate: Fri Jul 16 17:04:31 2021 -0600
Logic to capture/display the last time a user successfully authenticated (#6009)
* Added logic for last_authenticated in TO
* Added TP changes.
* updated CHANGELOG.md
* updated docs for users
* updated user tests
* updated sql file
* updated time
* updated based on review comments.
* updated based on review comments - 1
* updated docs
* updated logic for relativeLoginTime
---
CHANGELOG.md | 1 +
docs/source/api/v4/user_current.rst | 48 ++++++++++----------
docs/source/api/v4/users.rst | 52 +++++++++++-----------
docs/source/api/v4/users_id.rst | 48 ++++++++++----------
lib/go-tc/users.go | 38 +++++++++++-----
...070800000000_add_tm_user_last_authenticated.sql | 26 +++++++++++
traffic_ops/testing/api/v4/user_test.go | 9 ++++
traffic_ops/traffic_ops_golang/login/login.go | 24 ++++++++--
traffic_ops/traffic_ops_golang/user/current.go | 22 ++++++---
traffic_ops/traffic_ops_golang/user/user.go | 4 +-
traffic_ops/v4-client/user.go | 7 ++-
.../TableCapabilityUsersController.js | 8 ++--
.../capabilityUsers/table.capabilityUsers.tpl.html | 2 +
.../table/roleUsers/TableRoleUsersController.js | 8 ++--
.../table/roleUsers/table.roleUsers.tpl.html | 2 +
.../tenantUsers/TableTenantUsersController.js | 8 ++--
.../table/tenantUsers/table.tenantUsers.tpl.html | 2 +
.../modules/table/users/TableUsersController.js | 11 +++--
.../modules/table/users/table.users.tpl.html | 6 +--
.../app/src/common/service/utils/DateUtils.js | 22 +++++++--
20 files changed, 234 insertions(+), 114 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3e12c7..fd6ccdf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
## [unreleased]
### Added
+- [#5412](https://github.com/apache/trafficcontrol/issues/5412) Added last authenticated time to user API's (`GET /user/current, GET /users, GET /user?id=`) response payload
- [#5451](https://github.com/apache/trafficcontrol/issues/5451) Added change log count to user API's response payload and query param (username) to logs API
- Added support for CDN locks
- Added support for PostgreSQL as a Traffic Vault backend
diff --git a/docs/source/api/v4/user_current.rst b/docs/source/api/v4/user_current.rst
index 953a48c..b51de38 100644
--- a/docs/source/api/v4/user_current.rst
+++ b/docs/source/api/v4/user_current.rst
@@ -35,28 +35,29 @@ No parameters available.
Response Structure
------------------
-:addressLine1: The user's address - including street name and number
-:addressLine2: An additional address field for e.g. apartment number
-:city: The name of the city wherein the user resides
-:company: The name of the company for which the user works
-:country: The name of the country wherein the user resides
-:email: The user's email address
-:fullName: The user's full name, e.g. "John Quincy Adams"
-:gid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX group ID of the user
-:id: An integral, unique identifier for this user
-:lastUpdated: The date and time at which the user was last modified, in :ref:`non-rfc-datetime`
-:newUser: A meta field with no apparent purpose that is usually ``null`` unless explicitly set during creation or modification of a user via some API endpoint
-:phoneNumber: The user's phone number
-:postalCode: The postal code of the area in which the user resides
-:publicSshKey: The user's public key used for the SSH protocol
-:registrationSent: If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
-:role: The integral, unique identifier of the highest-privilege :term:`Role` assigned to this user
-:rolename: The name of the highest-privilege :term:`Role` assigned to this user
-:stateOrProvince: The name of the state or province where this user resides
-:tenant: The name of the :term:`Tenant` to which this user belongs
-:tenantId: The integral, unique identifier of the :term:`Tenant` to which this user belongs
-:uid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX user ID of the user
-:username: The user's username
+:addressLine1: The user's address - including street name and number
+:addressLine2: An additional address field for e.g. apartment number
+:city: The name of the city wherein the user resides
+:company: The name of the company for which the user works
+:country: The name of the country wherein the user resides
+:email: The user's email address
+:fullName: The user's full name, e.g. "John Quincy Adams"
+:gid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX group ID of the user
+:id: An integral, unique identifier for this user
+:lastAuthenticated: The date and time at which the user was last authenticated, in :rfc:`3339`
+:lastUpdated: The date and time at which the user was last modified, in :ref:`non-rfc-datetime`
+:newUser: A meta field with no apparent purpose that is usually ``null`` unless explicitly set during creation or modification of a user via some API endpoint
+:phoneNumber: The user's phone number
+:postalCode: The postal code of the area in which the user resides
+:publicSshKey: The user's public key used for the SSH protocol
+:registrationSent: If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
+:role: The integral, unique identifier of the highest-privilege :term:`Role` assigned to this user
+:rolename: The name of the highest-privilege :term:`Role` assigned to this user
+:stateOrProvince: The name of the state or province where this user resides
+:tenant: The name of the :term:`Tenant` to which this user belongs
+:tenantId: The integral, unique identifier of the :term:`Tenant` to which this user belongs
+:uid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX user ID of the user
+:username: The user's username
.. code-block:: http
:caption: Response Example
@@ -95,7 +96,8 @@ Response Structure
"tenant": "root",
"tenantId": 1,
"uid": null,
- "lastUpdated": "2018-12-12 16:26:32+00"
+ "lastUpdated": "2018-12-12 16:26:32+00",
+ "lastAuthenticated": "2021-07-09T14:44:10.371708-06:00"
}}
``PUT``
diff --git a/docs/source/api/v4/users.rst b/docs/source/api/v4/users.rst
index cbf780b..658e191 100644
--- a/docs/source/api/v4/users.rst
+++ b/docs/source/api/v4/users.rst
@@ -68,29 +68,30 @@ Request Structure
Response Structure
------------------
-:addressLine1: The user's address - including street name and number
-:addressLine2: An additional address field for e.g. apartment number
-:city: The name of the city wherein the user resides
-:company: The name of the company for which the user works
-:country: The name of the country wherein the user resides
-:email: The user's email address
-:fullName: The user's full name, e.g. "John Quincy Adams"
-:gid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX group ID of the user - now it is always ``null``
-:id: An integral, unique identifier for this user
-:lastUpdated: The date and time at which the user was last modified, in :ref:`non-rfc-datetime`
-:newUser: A meta field with no apparent purpose that is usually ``null`` unless explicitly set during creation or modification of a user via some API endpoint
-:phoneNumber: The user's phone number
-:postalCode: The postal code of the area in which the user resides
-:publicSshKey: The user's public key used for the SSH protocol
-:registrationSent: If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
-:role: The integral, unique identifier of the highest-privilege role assigned to this user
-:rolename: The name of the highest-privilege role assigned to this user
-:stateOrProvince: The name of the state or province where this user resides
-:tenant: The name of the tenant to which this user belongs
-:tenantId: The integral, unique identifier of the tenant to which this user belongs
-:uid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX user ID of the user - now it is always ``null``
-:username: The user's username
-:changeLogCount: The number of change log entries created by the user
+:addressLine1: The user's address - including street name and number
+:addressLine2: An additional address field for e.g. apartment number
+:changeLogCount: The number of change log entries created by the user
+:city: The name of the city wherein the user resides
+:company: The name of the company for which the user works
+:country: The name of the country wherein the user resides
+:email: The user's email address
+:fullName: The user's full name, e.g. "John Quincy Adams"
+:gid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX group ID of the user - now it is always ``null``
+:id: An integral, unique identifier for this user
+:lastAuthenticated: The date and time at which the user was last authenticated, in :rfc:`3339`
+:lastUpdated: The date and time at which the user was last modified, in :ref:`non-rfc-datetime`
+:newUser: A meta field with no apparent purpose that is usually ``null`` unless explicitly set during creation or modification of a user via some API endpoint
+:phoneNumber: The user's phone number
+:postalCode: The postal code of the area in which the user resides
+:publicSshKey: The user's public key used for the SSH protocol
+:registrationSent: If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
+:role: The integral, unique identifier of the highest-privilege role assigned to this user
+:rolename: The name of the highest-privilege role assigned to this user
+:stateOrProvince: The name of the state or province where this user resides
+:tenant: The name of the tenant to which this user belongs
+:tenantId: The integral, unique identifier of the tenant to which this user belongs
+:uid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX user ID of the user - now it is always ``null``
+:username: The user's username
.. code-block:: http
:caption: Response Example
@@ -130,8 +131,9 @@ Response Structure
"tenant": "root",
"tenantId": 1,
"uid": null,
- "lastUpdated": "2018-12-12 16:26:32+00"
- "changeLogCount": 20
+ "lastUpdated": "2018-12-12 16:26:32+00",
+ "changeLogCount": 20,
+ "lastAuthenticated": "2021-07-09T14:44:10.371708-06:00"
}
]}
diff --git a/docs/source/api/v4/users_id.rst b/docs/source/api/v4/users_id.rst
index 8a299a9..e4dac10 100644
--- a/docs/source/api/v4/users_id.rst
+++ b/docs/source/api/v4/users_id.rst
@@ -48,28 +48,29 @@ Request Structure
Response Structure
------------------
-:addressLine1: The user's address - including street name and number
-:addressLine2: An additional address field for e.g. apartment number
-:city: The name of the city wherein the user resides
-:company: The name of the company for which the user works
-:country: The name of the country wherein the user resides
-:email: The user's email address
-:fullName: The user's full name, e.g. "John Quincy Adams"
-:gid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX group ID of the user - now it is always ``null``
-:id: An integral, unique identifier for this user
-:lastUpdated: The date and time at which the user was last modified, in :ref:`non-rfc-datetime`
-:newUser: A meta field with no apparent purpose that is usually ``null`` unless explicitly set during creation or modification of a user via some API endpoint
-:phoneNumber: The user's phone number
-:postalCode: The postal code of the area in which the user resides
-:publicSshKey: The user's public key used for the SSH protocol
-:registrationSent: If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
-:role: The integral, unique identifier of the highest-privilege role assigned to this user
-:rolename: The name of the highest-privilege role assigned to this user
-:stateOrProvince: The name of the state or province where this user resides
-:tenant: The name of the tenant to which this user belongs
-:tenantId: The integral, unique identifier of the tenant to which this user belongs
-:uid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX user ID of the user - now it is always ``null``
-:username: The user's username
+:addressLine1: The user's address - including street name and number
+:addressLine2: An additional address field for e.g. apartment number
+:city: The name of the city wherein the user resides
+:company: The name of the company for which the user works
+:country: The name of the country wherein the user resides
+:email: The user's email address
+:fullName: The user's full name, e.g. "John Quincy Adams"
+:gid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX group ID of the user - now it is always ``null``
+:id: An integral, unique identifier for this user
+:lastAuthenticated: The date and time at which the user was last authenticated, in :rfc:`3339`
+:lastUpdated: The date and time at which the user was last modified, in :ref:`non-rfc-datetime`
+:newUser: A meta field with no apparent purpose that is usually ``null`` unless explicitly set during creation or modification of a user via some API endpoint
+:phoneNumber: The user's phone number
+:postalCode: The postal code of the area in which the user resides
+:publicSshKey: The user's public key used for the SSH protocol
+:registrationSent: If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
+:role: The integral, unique identifier of the highest-privilege role assigned to this user
+:rolename: The name of the highest-privilege role assigned to this user
+:stateOrProvince: The name of the state or province where this user resides
+:tenant: The name of the tenant to which this user belongs
+:tenantId: The integral, unique identifier of the tenant to which this user belongs
+:uid: A deprecated field only kept for legacy compatibility reasons that used to contain the UNIX user ID of the user - now it is always ``null``
+:username: The user's username
.. code-block:: http
:caption: Response Example
@@ -109,7 +110,8 @@ Response Structure
"tenant": "root",
"tenantId": 1,
"uid": null,
- "lastUpdated": "2018-12-13 17:24:23+00"
+ "lastUpdated": "2018-12-13 17:24:23+00",
+ "lastAuthenticated": "2021-07-09T14:44:10.371708-06:00"
}
]}
diff --git a/lib/go-tc/users.go b/lib/go-tc/users.go
index 773d544..1bf0388 100644
--- a/lib/go-tc/users.go
+++ b/lib/go-tc/users.go
@@ -19,16 +19,19 @@ package tc
* under the License.
*/
-import "database/sql"
-import "encoding/json"
-import "errors"
-import "fmt"
+import (
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "time"
-import "github.com/apache/trafficcontrol/lib/go-rfc"
-import "github.com/apache/trafficcontrol/lib/go-util"
+ "github.com/apache/trafficcontrol/lib/go-rfc"
+ "github.com/apache/trafficcontrol/lib/go-util"
-import "github.com/go-ozzo/ozzo-validation"
-import "github.com/go-ozzo/ozzo-validation/is"
+ "github.com/go-ozzo/ozzo-validation"
+ "github.com/go-ozzo/ozzo-validation/is"
+)
// UserCredentials contains Traffic Ops login credentials
type UserCredentials struct {
@@ -107,13 +110,14 @@ type User struct {
commonUserFields
}
-// UserV40 contains ChangeLogCount field
+// UserV40 contains ChangeLogCount field.
type UserV40 struct {
User
- ChangeLogCount *int `json:"changeLogCount" db:"change_log_count"`
+ ChangeLogCount *int `json:"changeLogCount" db:"change_log_count"`
+ LastAuthenticated *time.Time `json:"lastAuthenticated" db:"last_authenticated"`
}
-// UserCurrent represents the profile for the authenticated user
+// UserCurrent represents the profile for the authenticated user.
type UserCurrent struct {
UserName *string `json:"username"`
LocalUser *bool `json:"localUser"`
@@ -121,6 +125,12 @@ type UserCurrent struct {
commonUserFields
}
+// UserCurrentV40 contains LastAuthenticated field.
+type UserCurrentV40 struct {
+ UserCurrent
+ LastAuthenticated *time.Time `json:"lastAuthenticated" db:"last_authenticated"`
+}
+
// CurrentUserUpdateRequest differs from a regular User/UserCurrent in that many of its fields are
// *parsed* but not *unmarshaled*. This allows a handler to distinguish between "null" and
// "undefined" values.
@@ -331,6 +341,12 @@ type UserCurrentResponse struct {
Alerts
}
+// UserCurrentResponseV40 is the Traffic Ops API version 4.0 variant of UserResponse.
+type UserCurrentResponseV40 struct {
+ Response UserCurrentV40 `json:"response"`
+ Alerts
+}
+
// UserDeliveryServiceDeleteResponse can hold a Traffic Ops API response to
// a request to remove a delivery service from a user.
type UserDeliveryServiceDeleteResponse struct {
diff --git a/traffic_ops/app/db/migrations/2021070800000000_add_tm_user_last_authenticated.sql b/traffic_ops/app/db/migrations/2021070800000000_add_tm_user_last_authenticated.sql
new file mode 100644
index 0000000..1f147b9
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2021070800000000_add_tm_user_last_authenticated.sql
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+-- +goose Up
+-- SQL in section 'Up' is executed when this migration is applied
+
+ALTER TABLE tm_user ADD COLUMN last_authenticated timestamp with time zone;
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+
+ALTER TABLE tm_user DROP COLUMN last_authenticated;
diff --git a/traffic_ops/testing/api/v4/user_test.go b/traffic_ops/testing/api/v4/user_test.go
index 98b648d..af7d7a4 100644
--- a/traffic_ops/testing/api/v4/user_test.go
+++ b/traffic_ops/testing/api/v4/user_test.go
@@ -454,6 +454,12 @@ func GetTestUsers(t *testing.T) {
if err != nil {
t.Errorf("cannot get users: %v - alerts: %+v", err, resp.Alerts)
}
+ if len(resp.Response) < 1 {
+ t.Fatalf("expected a users list, got nothing")
+ }
+ if resp.Response[0].LastAuthenticated == nil {
+ t.Errorf("current user's authenticated time, expected: '%s' actual: %v", resp.Response[0].LastAuthenticated, nil)
+ }
}
func GetTestUserCurrent(t *testing.T) {
@@ -466,6 +472,9 @@ func GetTestUserCurrent(t *testing.T) {
} else if *user.Response.UserName != SessionUserName {
t.Errorf("current user expected: '%s' actual: '%s'", SessionUserName, *user.Response.UserName)
}
+ if user.Response.LastAuthenticated == nil {
+ t.Errorf("current user's authenticated time, expected: '%s' actual: %v", user.Response.LastAuthenticated, nil)
+ }
}
func UserTenancyTest(t *testing.T) {
diff --git a/traffic_ops/traffic_ops_golang/login/login.go b/traffic_ops/traffic_ops_golang/login/login.go
index 2f32ed5..5173ff6 100644
--- a/traffic_ops/traffic_ops_golang/login/login.go
+++ b/traffic_ops/traffic_ops_golang/login/login.go
@@ -62,6 +62,7 @@ WHERE name='tm.instance_name' AND
`
const userQueryByEmail = `SELECT EXISTS(SELECT * FROM tm_user WHERE email=$1)`
const setTokenQuery = `UPDATE tm_user SET token=$1 WHERE email=$2`
+const UpdateLoginTimeQuery = `UPDATE tm_user SET last_authenticated = now() WHERE username=$1`
const defaultCookieDuration = 6 * time.Hour
@@ -149,9 +150,26 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
if authenticated {
httpCookie := tocookie.GetCookie(form.Username, defaultCookieDuration, cfg.Secrets[0])
http.SetCookie(w, httpCookie)
- resp = struct {
- tc.Alerts
- }{tc.CreateAlerts(tc.SuccessLevel, "Successfully logged in.")}
+
+ //If all's well until here, then update last authenticated time
+ tx, txErr := db.Begin()
+ if txErr != nil {
+ api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("beginning transaction: %w", txErr))
+ return
+ }
+ defer tx.Commit()
+ _, dbErr := tx.Exec(UpdateLoginTimeQuery, form.Username)
+ if dbErr != nil {
+ log.Errorf("unable to update authentication time for a given user: %s\n", dbErr.Error())
+ resp = struct {
+ tc.Alerts
+ }{tc.CreateAlerts(tc.ErrorLevel, "Unable to update authentication time for a given user")}
+ } else {
+ resp = struct {
+ tc.Alerts
+ }{tc.CreateAlerts(tc.SuccessLevel, "Successfully logged in.")}
+ }
+
} else {
resp = struct {
tc.Alerts
diff --git a/traffic_ops/traffic_ops_golang/user/current.go b/traffic_ops/traffic_ops_golang/user/current.go
index 54a43e9..d12abe0 100644
--- a/traffic_ops/traffic_ops_golang/user/current.go
+++ b/traffic_ops/traffic_ops_golang/user/current.go
@@ -105,15 +105,26 @@ func Current(w http.ResponseWriter, r *http.Request) {
return
}
defer inf.Close()
+
currentUser, err := getUser(inf.Tx.Tx, inf.User.ID)
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting current user: "+err.Error()))
return
}
- api.WriteResp(w, r, currentUser)
+
+ version := inf.Version
+ if version == nil {
+ api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, fmt.Errorf("TOUsers.Read called with invalid API version"), nil)
+ return
+ }
+ if version.Major >= 4 {
+ api.WriteResp(w, r, currentUser)
+ } else {
+ api.WriteResp(w, r, currentUser.UserCurrent)
+ }
}
-func getUser(tx *sql.Tx, id int) (tc.UserCurrent, error) {
+func getUser(tx *sql.Tx, id int) (tc.UserCurrentV40, error) {
q := `
SELECT
u.address_line1,
@@ -125,6 +136,7 @@ u.email,
u.full_name,
u.id,
u.last_updated,
+u.last_authenticated,
u.local_passwd,
u.new_user,
u.phone_number,
@@ -141,10 +153,10 @@ LEFT JOIN role as r ON r.id = u.role
INNER JOIN tenant as t ON t.id = u.tenant_id
WHERE u.id=$1
`
- u := tc.UserCurrent{}
+ u := tc.UserCurrentV40{}
localPassword := sql.NullString{}
- if err := tx.QueryRow(q, id).Scan(&u.AddressLine1, &u.AddressLine2, &u.City, &u.Company, &u.Country, &u.Email, &u.FullName, &u.ID, &u.LastUpdated, &localPassword, &u.NewUser, &u.PhoneNumber, &u.PostalCode, &u.PublicSSHKey, &u.Role, &u.RoleName, &u.StateOrProvince, &u.Tenant, &u.TenantID, &u.UserName); err != nil {
- return tc.UserCurrent{}, errors.New("querying current user: " + err.Error())
+ if err := tx.QueryRow(q, id).Scan(&u.AddressLine1, &u.AddressLine2, &u.City, &u.Company, &u.Country, &u.Email, &u.FullName, &u.ID, &u.LastUpdated, &u.LastAuthenticated, &localPassword, &u.NewUser, &u.PhoneNumber, &u.PostalCode, &u.PublicSSHKey, &u.Role, &u.RoleName, &u.StateOrProvince, &u.Tenant, &u.TenantID, &u.UserName); err != nil {
+ return tc.UserCurrentV40{}, errors.New("querying current user: " + err.Error())
}
u.LocalUser = util.BoolPtr(localPassword.Valid)
return u, nil
diff --git a/traffic_ops/traffic_ops_golang/user/user.go b/traffic_ops/traffic_ops_golang/user/user.go
index 5765159..e3fd5f4 100644
--- a/traffic_ops/traffic_ops_golang/user/user.go
+++ b/traffic_ops/traffic_ops_golang/user/user.go
@@ -229,7 +229,8 @@ func (this *TOUser) Read(h http.Header, useIMS bool) ([]interface{}, error, erro
}
type UserGet40 struct {
UserGet
- ChangeLogCount *int `json:"changeLogCount" db:"change_log_count"`
+ ChangeLogCount *int `json:"changeLogCount" db:"change_log_count"`
+ LastAuthenticated *time.Time `json:"lastAuthenticated" db:"last_authenticated"`
}
user := &UserGet{}
@@ -431,6 +432,7 @@ func (user *TOUser) SelectQuery40() string {
u.tenant_id,
t.name as tenant,
u.last_updated,
+ u.last_authenticated,
(SELECT count(l.tm_user) FROM log as l WHERE l.tm_user = u.id) as change_log_count
FROM tm_user u
LEFT JOIN tenant t ON u.tenant_id = t.id
diff --git a/traffic_ops/v4-client/user.go b/traffic_ops/v4-client/user.go
index 229cd90..2c30d2d 100644
--- a/traffic_ops/v4-client/user.go
+++ b/traffic_ops/v4-client/user.go
@@ -24,6 +24,9 @@ import (
"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
)
+// UserCurrentResponseV4 is an alias to avoid client breaking changes. In-case of a minor or major version change, we replace the below alias with a new structure.
+type UserCurrentResponseV4 = tc.UserCurrentResponseV40
+
// GetUsers retrieves all (Tenant-accessible) Users stored in Traffic Ops.
func (to *Session) GetUsers(opts RequestOptions) (tc.UsersResponseV40, toclientlib.ReqInf, error) {
data := tc.UsersResponseV40{}
@@ -33,9 +36,9 @@ func (to *Session) GetUsers(opts RequestOptions) (tc.UsersResponseV40, toclientl
}
// GetUserCurrent retrieves the currently authenticated User.
-func (to *Session) GetUserCurrent(opts RequestOptions) (tc.UserCurrentResponse, toclientlib.ReqInf, error) {
+func (to *Session) GetUserCurrent(opts RequestOptions) (UserCurrentResponseV4, toclientlib.ReqInf, error) {
route := `/user/current`
- resp := tc.UserCurrentResponse{}
+ resp := UserCurrentResponseV4{}
reqInf, err := to.get(route, opts, &resp)
return resp, reqInf, err
}
diff --git a/traffic_portal/app/src/common/modules/table/capabilityUsers/TableCapabilityUsersController.js b/traffic_portal/app/src/common/modules/table/capabilityUsers/TableCapabilityUsersController.js
index 52b4b84..1fd5653 100644
--- a/traffic_portal/app/src/common/modules/table/capabilityUsers/TableCapabilityUsersController.js
+++ b/traffic_portal/app/src/common/modules/table/capabilityUsers/TableCapabilityUsersController.js
@@ -17,7 +17,7 @@
* under the License.
*/
-var TableCapabilityUsersController = function(capability, capUsers, $controller, $scope, $state, locationUtils) {
+var TableCapabilityUsersController = function(capability, capUsers, $controller, $scope, $state, dateUtils, locationUtils) {
// extends the TableUsersController to inherit common methods
angular.extend(this, $controller('TableUsersController', { users: capUsers, $scope: $scope }));
@@ -26,6 +26,8 @@ var TableCapabilityUsersController = function(capability, capUsers, $controller,
$scope.capability = capability[0];
+ $scope.relativeLoginTime = dateUtils.relativeLoginTime;
+
$scope.editUser = function(id) {
locationUtils.navigateToPath('/users/' + id);
};
@@ -48,7 +50,7 @@ var TableCapabilityUsersController = function(capability, capUsers, $controller,
"iDisplayLength": 25,
"aaSorting": [],
"columns": $scope.columns,
- "initComplete": function(settings, json) {
+ "initComplete": function() {
try {
// need to create the show/hide column checkboxes and bind to the current visibility
$scope.columns = JSON.parse(localStorage.getItem('DataTables_capUsersTable_/')).columns;
@@ -61,5 +63,5 @@ var TableCapabilityUsersController = function(capability, capUsers, $controller,
};
-TableCapabilityUsersController.$inject = ['capability', 'capUsers', '$controller', '$scope', '$state', 'locationUtils'];
+TableCapabilityUsersController.$inject = ['capability', 'capUsers', '$controller', '$scope', '$state', 'dateUtils', 'locationUtils'];
module.exports = TableCapabilityUsersController;
diff --git a/traffic_portal/app/src/common/modules/table/capabilityUsers/table.capabilityUsers.tpl.html b/traffic_portal/app/src/common/modules/table/capabilityUsers/table.capabilityUsers.tpl.html
index dea80c5..b453f2b 100644
--- a/traffic_portal/app/src/common/modules/table/capabilityUsers/table.capabilityUsers.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/capabilityUsers/table.capabilityUsers.tpl.html
@@ -53,6 +53,7 @@ under the License.
<th>Tenant</th>
<th>Role</th>
<th>Registration Sent</th>
+ <th>Last Authenticated</th>
<th>Change Log Count</th>
</tr>
</thead>
@@ -64,6 +65,7 @@ under the License.
<td data-search="^{{::u.tenant}}$">{{::u.tenant}}</td>
<td data-search="^{{::u.rolename}}$">{{::u.rolename}}</td>
<td data-search="^{{::u.registrationSent}}$">{{::u.registrationSent}}</td>
+ <td data-search="^{{::relativeLoginTime(u.lastAuthenticated)}}$">{{::relativeLoginTime(u.lastAuthenticated)}}</td>
<td data-search="^{{::u.changeLogCount}}$">{{::u.changeLogCount}}</td>
</tr>
</tbody>
diff --git a/traffic_portal/app/src/common/modules/table/roleUsers/TableRoleUsersController.js b/traffic_portal/app/src/common/modules/table/roleUsers/TableRoleUsersController.js
index c6fd168..e741f35 100644
--- a/traffic_portal/app/src/common/modules/table/roleUsers/TableRoleUsersController.js
+++ b/traffic_portal/app/src/common/modules/table/roleUsers/TableRoleUsersController.js
@@ -17,7 +17,7 @@
* under the License.
*/
-var TableRoleUsersController = function(roles, roleUsers, $controller, $scope, $state, locationUtils) {
+var TableRoleUsersController = function(roles, roleUsers, $controller, $scope, $state, dateUtils, locationUtils) {
// extends the TableUsersController to inherit common methods
angular.extend(this, $controller('TableUsersController', { users: roleUsers, $scope: $scope }));
@@ -26,6 +26,8 @@ var TableRoleUsersController = function(roles, roleUsers, $controller, $scope, $
$scope.role = roles[0];
+ $scope.relativeLoginTime = dateUtils.relativeLoginTime;
+
$scope.editUser = function(id) {
locationUtils.navigateToPath('/users/' + id);
};
@@ -48,7 +50,7 @@ var TableRoleUsersController = function(roles, roleUsers, $controller, $scope, $
"iDisplayLength": 25,
"aaSorting": [],
"columns": $scope.columns,
- "initComplete": function(settings, json) {
+ "initComplete": function() {
try {
// need to create the show/hide column checkboxes and bind to the current visibility
$scope.columns = JSON.parse(localStorage.getItem('DataTables_roleUsersTable_/')).columns;
@@ -61,5 +63,5 @@ var TableRoleUsersController = function(roles, roleUsers, $controller, $scope, $
};
-TableRoleUsersController.$inject = ['roles', 'roleUsers', '$controller', '$scope', '$state', 'locationUtils'];
+TableRoleUsersController.$inject = ['roles', 'roleUsers', '$controller', '$scope', '$state', 'dateUtils', 'locationUtils'];
module.exports = TableRoleUsersController;
diff --git a/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html b/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html
index df0eb84..39adb98 100644
--- a/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html
@@ -53,6 +53,7 @@ under the License.
<th>Tenant</th>
<th>Role</th>
<th>Registration Sent</th>
+ <th>Last Authenticated</th>
<th>Change Log Count</th>
</tr>
</thead>
@@ -64,6 +65,7 @@ under the License.
<td data-search="^{{::u.tenant}}$">{{::u.tenant}}</td>
<td data-search="^{{::u.rolename}}$">{{::u.rolename}}</td>
<td data-search="^{{::u.registrationSent}}$">{{::u.registrationSent}}</td>
+ <td data-search="^{{::relativeLoginTime(u.lastAuthenticated)}}$">{{::relativeLoginTime(u.lastAuthenticated)}}</td>
<td data-search="^{{::u.changeLogCount}}$">{{::u.changeLogCount}}</td>
</tr>
</tbody>
diff --git a/traffic_portal/app/src/common/modules/table/tenantUsers/TableTenantUsersController.js b/traffic_portal/app/src/common/modules/table/tenantUsers/TableTenantUsersController.js
index f83ca8c..8f31e37 100644
--- a/traffic_portal/app/src/common/modules/table/tenantUsers/TableTenantUsersController.js
+++ b/traffic_portal/app/src/common/modules/table/tenantUsers/TableTenantUsersController.js
@@ -17,7 +17,7 @@
* under the License.
*/
-var TableTenantUsersController = function(tenant, tenantUsers, $controller, $scope, $state, locationUtils) {
+var TableTenantUsersController = function(tenant, tenantUsers, $controller, $scope, $state, dateUtils, locationUtils) {
// extends the TableUsersController to inherit common methods
angular.extend(this, $controller('TableUsersController', { users: tenantUsers, $scope: $scope }));
@@ -26,6 +26,8 @@ var TableTenantUsersController = function(tenant, tenantUsers, $controller, $sco
$scope.tenant = tenant;
+ $scope.relativeLoginTime = dateUtils.relativeLoginTime;
+
$scope.editUser = function(id) {
locationUtils.navigateToPath('/users/' + id);
};
@@ -48,7 +50,7 @@ var TableTenantUsersController = function(tenant, tenantUsers, $controller, $sco
"iDisplayLength": 25,
"aaSorting": [],
"columns": $scope.columns,
- "initComplete": function(settings, json) {
+ "initComplete": function() {
try {
// need to create the show/hide column checkboxes and bind to the current visibility
$scope.columns = JSON.parse(localStorage.getItem('DataTables_tenantUsersTable_/')).columns;
@@ -61,5 +63,5 @@ var TableTenantUsersController = function(tenant, tenantUsers, $controller, $sco
};
-TableTenantUsersController.$inject = ['tenant', 'tenantUsers', '$controller', '$scope', '$state', 'locationUtils'];
+TableTenantUsersController.$inject = ['tenant', 'tenantUsers', '$controller', '$scope', '$state', 'dateUtils', 'locationUtils'];
module.exports = TableTenantUsersController;
diff --git a/traffic_portal/app/src/common/modules/table/tenantUsers/table.tenantUsers.tpl.html b/traffic_portal/app/src/common/modules/table/tenantUsers/table.tenantUsers.tpl.html
index 4e83725..168cfc5 100644
--- a/traffic_portal/app/src/common/modules/table/tenantUsers/table.tenantUsers.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/tenantUsers/table.tenantUsers.tpl.html
@@ -53,6 +53,7 @@ under the License.
<th>Tenant</th>
<th>Role</th>
<th>Registration Sent</th>
+ <th>Last Authenticated</th>
<th>Change Log Count</th>
</tr>
</thead>
@@ -64,6 +65,7 @@ under the License.
<td data-search="^{{::u.tenant}}$">{{::u.tenant}}</td>
<td data-search="^{{::u.rolename}}$">{{::u.rolename}}</td>
<td data-search="^{{::u.registrationSent}}$">{{::u.registrationSent}}</td>
+ <td data-search="^{{::relativeLoginTime(u.lastAuthenticated)}}$">{{::relativeLoginTime(u.lastAuthenticated)}}</td>
<td data-search="^{{::u.changeLogCount}}$">{{::u.changeLogCount}}</td>
</tr>
</tbody>
diff --git a/traffic_portal/app/src/common/modules/table/users/TableUsersController.js b/traffic_portal/app/src/common/modules/table/users/TableUsersController.js
index c32fbc5..8ea177b 100644
--- a/traffic_portal/app/src/common/modules/table/users/TableUsersController.js
+++ b/traffic_portal/app/src/common/modules/table/users/TableUsersController.js
@@ -17,12 +17,14 @@
* under the License.
*/
-var TableUsersController = function(users, $scope, $state, locationUtils) {
+var TableUsersController = function(users, $scope, $state, dateUtils, locationUtils) {
let usersTable;
$scope.users = users;
+ $scope.relativeLoginTime = dateUtils.relativeLoginTime;
+
$scope.columns = [
{ "name": "Full Name", "visible": true, "searchable": true },
{ "name": "Username", "visible": true, "searchable": true },
@@ -30,7 +32,8 @@ var TableUsersController = function(users, $scope, $state, locationUtils) {
{ "name": "Tenant", "visible": true, "searchable": true },
{ "name": "Role", "visible": true, "searchable": true },
{ "name": "Registration Sent", "visible": false, "searchable": true },
- { "name": "Change Log Count", "visible": false, "searchable": false }
+ { "name": "Last Authenticated", "visible": false, "searchable": false },
+ { "name": "Change Log Count", "visible": false, "searchable": false },
];
$scope.editUser = function(id) {
@@ -61,7 +64,7 @@ var TableUsersController = function(users, $scope, $state, locationUtils) {
"iDisplayLength": 25,
"aaSorting": [],
"columns": $scope.columns,
- "initComplete": function(settings, json) {
+ "initComplete": function() {
try {
// need to create the show/hide column checkboxes and bind to the current visibility
$scope.columns = JSON.parse(localStorage.getItem('DataTables_usersTable_/')).columns;
@@ -74,5 +77,5 @@ var TableUsersController = function(users, $scope, $state, locationUtils) {
};
-TableUsersController.$inject = ['users', '$scope', '$state', 'locationUtils'];
+TableUsersController.$inject = ['users', '$scope', '$state', 'dateUtils', 'locationUtils'];
module.exports = TableUsersController;
diff --git a/traffic_portal/app/src/common/modules/table/users/table.users.tpl.html b/traffic_portal/app/src/common/modules/table/users/table.users.tpl.html
index c933abe..48bf88e 100644
--- a/traffic_portal/app/src/common/modules/table/users/table.users.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/users/table.users.tpl.html
@@ -53,6 +53,7 @@ under the License.
<th>Tenant</th>
<th>Role</th>
<th>Registration Sent</th>
+ <th>Last Authenticated</th>
<th>Change Log Count</th>
</tr>
</thead>
@@ -64,6 +65,7 @@ under the License.
<td data-search="^{{::u.tenant}}$">{{::u.tenant}}</td>
<td data-search="^{{::u.rolename}}$">{{::u.rolename}}</td>
<td data-search="^{{::u.registrationSent}}$">{{::u.registrationSent}}</td>
+ <td data-search="^{{::relativeLoginTime(u.lastAuthenticated)}}$">{{::relativeLoginTime(u.lastAuthenticated)}}</td>
<td data-search="^{{::u.changeLogCount}}$">{{::u.changeLogCount}}</td>
</tr>
</tbody>
@@ -71,7 +73,3 @@ under the License.
</div>
</div>
-
-
-
-
diff --git a/traffic_portal/app/src/common/service/utils/DateUtils.js b/traffic_portal/app/src/common/service/utils/DateUtils.js
index 40098d7..433e58d 100644
--- a/traffic_portal/app/src/common/service/utils/DateUtils.js
+++ b/traffic_portal/app/src/common/service/utils/DateUtils.js
@@ -38,7 +38,7 @@ var DateUtils = function() {
var dF = this.dateFormat;
// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
- if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
+ if (arguments.length === 1 && Object.prototype.toString.call(date) === "[object String]" && !/\d/.test(date)) {
mask = date;
date = undefined;
}
@@ -50,7 +50,7 @@ var DateUtils = function() {
mask = String(dF.masks[mask] || mask || dF.masks["default"]);
// Allow setting the utc argument via the mask
- if (mask.slice(0, 4) == "UTC:") {
+ if (mask.slice(0, 4) === "UTC:") {
mask = mask.slice(4);
utc = true;
}
@@ -92,7 +92,7 @@ var DateUtils = function() {
TT: H < 12 ? "AM" : "PM",
Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
- S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
+ S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 !== 10) * d % 10]
};
return mask.replace(token, function ($0) {
@@ -142,7 +142,21 @@ var DateUtils = function() {
*/
this.getRelativeTime = function(date) {
return moment(date).fromNow();
- }
+ };
+
+ /**
+ * Converts a date into a string that tells how much time is between the current time and the given date and whether
+ * the user has logged into the system.
+ * @param {Date | string} date Either a Date object or a string that can be parsed by momentjs.
+ * @returns {string} A human readable description of how much time is between now and `date`.
+ */
+ this.relativeLoginTime = function(date) {
+ if (date) {
+ return moment(date).fromNow();
+ } else {
+ return "Never logged in";
+ }
+ };
};