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 <[email protected]>
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";
+               }
+       };
 
 };
 

Reply via email to