tsk-34: Update last login of user (#37)

Closes #34

Reviewed-on: phoenix/textsender-auth#37
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
phoenix
2025-12-31 23:13:02 +00:00
committed by phoenix
parent d34cad033d
commit de171b790d
10 changed files with 101 additions and 12 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ module git.kundeng.us/phoenix/textsender-auth
go 1.25.4 go 1.25.4
require ( require (
git.kundeng.us/phoenix/textsender-models v0.0.12 git.kundeng.us/phoenix/textsender-models v0.1.5-30-6f9e112b72-556
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
+2 -2
View File
@@ -1,5 +1,5 @@
git.kundeng.us/phoenix/textsender-models v0.0.12 h1:ps9H3FS5LyCwQhAiIvg4vYyfLZZ64dex1y9ytb0o9C4= git.kundeng.us/phoenix/textsender-models v0.1.5-30-6f9e112b72-556 h1:Ispsun00898NAWwnTfTAQQxbbVz+Te0bTl8z6t9E+gs=
git.kundeng.us/phoenix/textsender-models v0.0.12/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0= git.kundeng.us/phoenix/textsender-models v0.1.5-30-6f9e112b72-556/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+3 -1
View File
@@ -3,7 +3,9 @@ package endpoint
// Endpoint for registering a user // Endpoint for registering a user
const Register = "/api/v1/register" const Register = "/api/v1/register"
const Login = "/api/v1/login" const Login = "/api/v1/login"
const UpdatePassword = "/api/v1/user/password/update"
const CreateServiceUser = "/api/v1/service/register" const CreateServiceUser = "/api/v1/service/register"
const LoginServiceUser = "/api/v1/service/login" const LoginServiceUser = "/api/v1/service/login"
const TokenRefresh = "/api/v1/token/refresh" const TokenRefresh = "/api/v1/token/refresh"
const UpdatePassword = "/api/v1/user/password/update"
+12 -3
View File
@@ -3,6 +3,7 @@ package handler
import ( import (
"log" "log"
"net/http" "net/http"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/token" "git.kundeng.us/phoenix/textsender-models/tx0/token"
"git.kundeng.us/phoenix/textsender-models/tx0/user" "git.kundeng.us/phoenix/textsender-models/tx0/user"
@@ -75,6 +76,7 @@ func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
resp.Message = err.Error() resp.Message = err.Error()
} else { } else {
if hashing.CheckPasswordHash(req.Password, user.Password) { if hashing.CheckPasswordHash(req.Password, user.Password) {
lastLogin := time.Now()
var tokGen utility.TokenGenerator var tokGen utility.TokenGenerator
secretKey := config.GetSecretKey() secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey) tokGen.SetSecretKey(secretKey)
@@ -83,9 +85,16 @@ func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = "Error generating token" resp.Message = "Error generating token"
} else { } else {
statusCode = http.StatusOK log.Println("Updating user's last login")
resp.Data = append(resp.Data, *myToken) if rowsAffected, err := l.UserStore.UpdateLastLogin(ctx, user.Id, lastLogin); err != nil {
resp.Message = "Successful" statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
log.Println("Rows updated:", rowsAffected)
statusCode = http.StatusOK
resp.Data = append(resp.Data, *myToken)
resp.Message = "Successful"
}
} }
} else { } else {
statusCode = http.StatusNotFound statusCode = http.StatusNotFound
+13 -3
View File
@@ -1,7 +1,9 @@
package handler package handler
import ( import (
"log"
"net/http" "net/http"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/token" "git.kundeng.us/phoenix/textsender-models/tx0/token"
"git.kundeng.us/phoenix/textsender-models/tx0/user" "git.kundeng.us/phoenix/textsender-models/tx0/user"
@@ -153,6 +155,7 @@ func (s *ServiceHandler) Login(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = "Not valid" resp.Message = "Not valid"
} else { } else {
lastLogin := time.Now()
var tokGen utility.TokenGenerator var tokGen utility.TokenGenerator
tokGen.SetHourOffset(8) tokGen.SetHourOffset(8)
secretKey := config.GetSecretKey() secretKey := config.GetSecretKey()
@@ -162,9 +165,16 @@ func (s *ServiceHandler) Login(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = err.Error() resp.Message = err.Error()
} else { } else {
statusCode = http.StatusOK log.Println("Updating service user's last login")
resp.Data = append(resp.Data, myToken) if rowsAffected, err := s.ServiceStore.UpdateLastLogin(ctx, serviceUser.Id, lastLogin); err != nil {
resp.Message = "Successful" statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
log.Println("Rows updated:", rowsAffected)
statusCode = http.StatusOK
resp.Data = append(resp.Data, myToken)
resp.Message = "Successful"
}
} }
} }
} }
+22
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"sync" "sync"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/user" "git.kundeng.us/phoenix/textsender-models/tx0/user"
"github.com/google/uuid" "github.com/google/uuid"
@@ -101,3 +102,24 @@ func (m *MockServiceUserStore) GetWithId(ctx context.Context, id uuid.UUID) (*us
return nil, nil return nil, nil
} }
func (m *MockServiceUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return 0, m.Error
}
serviceUser, exists := m.ServiceUsers[id]
if !exists {
return 0, errors.New("Service user not found")
}
serviceUser.LastLogin = &lastLogin
m.ServiceUsers[id] = serviceUser
m.ServiceUsersByUsername[serviceUser.Username] = serviceUser
return 1, nil
}
+21
View File
@@ -136,3 +136,24 @@ func (m *MockUserStore) UpdatePassword(ctx context.Context, id uuid.UUID, passwo
return 1, nil return 1, nil
} }
func (m *MockUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return 0, m.Error
}
user, exists := m.Users[id]
if !exists {
return 0, errors.New("User not found")
}
user.LastLogin = &lastLogin
m.Users[id] = user
m.UsersByUsername[user.Username] = user
return 1, nil
}
+12
View File
@@ -3,6 +3,7 @@ package store
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/user" "git.kundeng.us/phoenix/textsender-models/tx0/user"
"github.com/google/uuid" "github.com/google/uuid"
@@ -10,11 +11,13 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
// TODO: Rename this to ServiceUserStore
type ServiceStore interface { type ServiceStore interface {
CheckWithUsername(ctx context.Context, username string) (bool, error) CheckWithUsername(ctx context.Context, username string) (bool, error)
GetWithUsername(ctx context.Context, username string) (*user.ServiceUser, error) GetWithUsername(ctx context.Context, username string) (*user.ServiceUser, error)
GetWithId(ctx context.Context, id uuid.UUID) (*user.ServiceUser, error) GetWithId(ctx context.Context, id uuid.UUID) (*user.ServiceUser, error)
Create(ctx context.Context, serviceUser *user.ServiceUser) error Create(ctx context.Context, serviceUser *user.ServiceUser) error
UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error)
} }
type PGServiceStore struct { type PGServiceStore struct {
@@ -81,3 +84,12 @@ func (s *PGServiceStore) Create(ctx context.Context, serviceUser *user.ServiceUs
&serviceUser.Id, &serviceUser.Created, &serviceUser.Id, &serviceUser.Created,
) )
} }
func (s *PGServiceStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error) {
query := `UPDATE service_users SET last_login = $1 WHERE id = $2`
if affected, err := s.db.Exec(ctx, query, lastLogin, id); err != nil {
return 0, err
} else {
return affected.RowsAffected(), nil
}
}
+11
View File
@@ -3,6 +3,7 @@ package store
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
@@ -18,6 +19,7 @@ type UserStore interface {
GetAllUsers(ctx context.Context) ([]*user.User, error) GetAllUsers(ctx context.Context) ([]*user.User, error)
UserExists(ctx context.Context, username string) (bool, error) UserExists(ctx context.Context, username string) (bool, error)
UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error) UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error)
UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error)
} }
type PGUserStore struct { type PGUserStore struct {
@@ -123,3 +125,12 @@ func (s *PGUserStore) UpdatePassword(ctx context.Context, id uuid.UUID, password
return affected.RowsAffected(), nil return affected.RowsAffected(), nil
} }
} }
func (s *PGUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error) {
query := `UPDATE users SET last_login = $1 WHERE id = $2`
if affected, err := s.db.Exec(ctx, query, lastLogin, id); err != nil {
return 0, err
} else {
return affected.RowsAffected(), nil
}
}
+4 -2
View File
@@ -8,12 +8,14 @@ CREATE TABLE users (
phone_number TEXT NOT NULL, phone_number TEXT NOT NULL,
username TEXT NOT NULL, username TEXT NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
created timestamptz DEFAULT now() created timestamptz DEFAULT now(),
last_login timestamptz NULL
); );
CREATE TABLE service_users ( CREATE TABLE service_users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username TEXT NOT NULL, username TEXT NOT NULL,
passphrase TEXT NOT NULL, passphrase TEXT NOT NULL,
created timestamptz DEFAULT now() created timestamptz DEFAULT now(),
last_login timestamptz NULL
); );