Merge branch 'user' into 'main'

Added login and logout

See merge request ukacorp/mesari/backend!22
This commit was merged in pull request #22.
This commit is contained in:
2023-11-07 06:58:48 +00:00
9 changed files with 359 additions and 11 deletions

View File

@@ -4,9 +4,11 @@ import (
"crypto/rand"
"encoding/base64"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"gitlab.com/pactual1/backend/database/user"
usr "gitlab.com/pactual1/backend/database/user"
"gitlab.com/pactual1/backend/models"
"gitlab.com/pactual1/backend/services/messaging"
"gitlab.com/pactual1/backend/shared"
@@ -111,3 +113,59 @@ func UpdatePassword(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"})
}
func Login(c *gin.Context) {
var req models.User
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"})
return
}
user, err := usr.GetUserByEmail(req.Email)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
if usr.CheckPassword(user.Password, req.Password) {
if user.IsActive && user.LoginAttempts < 10 {
// Proceed with creating JWT token and resetting login attempts
token, err := usr.CreateSessionToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create JWT token"})
return
}
usr.ResetLoginAttempts(*user)
c.JSON(http.StatusOK, gin.H{"token": token})
} else {
c.JSON(http.StatusForbidden, gin.H{"error": "Account locked or too many attempts"})
}
} else {
// Wrong password, increment login attempts
usr.IncrementLoginAttempts(*user)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
}
}
func Logout(c *gin.Context) {
// Extract the token from the request, typically from the Authorization header
tokenString := c.GetHeader("Authorization")
// If using a Bearer token, strip the 'Bearer ' prefix
if len(tokenString) > 7 && strings.ToUpper(tokenString[0:7]) == "BEARER " {
tokenString = tokenString[7:]
}
// Invalidate the session token
err := usr.InvalidateSessionToken(tokenString)
if err != nil {
// Handle error, could be not found or database error
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to logout"})
return
}
// Respond with success
c.JSON(http.StatusOK, gin.H{"message": "Successfully logged out"})
}

View File

@@ -1,11 +1,15 @@
package user
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt"
"github.com/jinzhu/gorm"
"gitlab.com/pactual1/backend/models"
"gitlab.com/pactual1/backend/shared"
"golang.org/x/crypto/bcrypt"
)
func SaveResetTokenToDB(userID uint, resetToken string) error {
@@ -33,10 +37,82 @@ func GetUserByEmail(email string) (*models.User, error) {
// Query the database for a user with the specified email
if err := shared.GetDb().Where("email = ?", email).First(&user).Error; err != nil {
if gorm.IsRecordNotFoundError(err) {
return nil, nil
return nil, nil
}
return nil, err
return nil, err
}
return &user, nil
}
}
func CheckPassword(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
func CreateSessionToken(userID uint) (string, error) {
// Generate JWT token
tokenString, err := CreateJWTToken(userID)
if err != nil {
return "", err
}
// Create and save the session token in the database
sessionToken := models.SessionToken{
UserID: userID,
Token: tokenString,
IsActive: true,
}
if result := shared.GetDb().Create(&sessionToken); result.Error != nil {
return "", result.Error
}
return tokenString, nil
}
func ResetLoginAttempts(user models.User) {
user.LoginAttempts = 0
user.IsActive = true
shared.GetDb().Save(&user)
}
func IncrementLoginAttempts(user models.User) {
user.LoginAttempts++
if user.LoginAttempts >= 10 {
user.IsActive = false
}
shared.GetDb().Save(&user)
}
var jwtKey = []byte("MDQsCiJwYWNrZXRWZXJzaW9uIjogMSwKImhhcm")
func CreateJWTToken(userID uint) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &jwt.StandardClaims{
Subject: fmt.Sprint(userID),
ExpiresAt: expirationTime.Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
func InvalidateSessionToken(tokenString string) error {
// Find the session token in the database
var sessionToken models.SessionToken
result := shared.GetDb().Where("token = ?", tokenString).First(&sessionToken)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// If token is not found, you may choose to ignore or handle it as an error
return nil // or return result.Error for strict handling
}
return result.Error
}
return shared.GetDb().Delete(&sessionToken).Error
}

3
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/ethereum/go-ethereum v1.13.1
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/jinzhu/gorm v1.9.16
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.8.0
@@ -15,6 +16,7 @@ require (
github.com/qor/admin v1.2.0
github.com/stretchr/testify v1.8.4
github.com/valyala/fasthttp v1.40.0
golang.org/x/crypto v0.14.0
)
replace gitlab.com/pactual1/backend => ./
@@ -108,7 +110,6 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.17.0 // indirect

2
go.sum
View File

@@ -119,6 +119,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=

18
models/session_token.go Normal file
View File

@@ -0,0 +1,18 @@
package models
type SessionToken struct {
BaseModel
UserID uint `json:"userId"`
Token string `json:"token"`
IsActive bool `json:"isActive"`
}
func (SessionToken) Update() (bool, error) {
return false, nil
}
func (SessionToken) Create() (bool, error) {
return false, nil
}
func (SessionToken) Delete() (bool, error) {
return false, nil
}

View File

@@ -2,12 +2,13 @@ package models
type User struct {
BaseModel
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Avatar string `json:"avatar"`
IsActive bool `json:"isActive" gorm:"default:false"`
CompanyID uint `json:"companyId"`
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Avatar string `json:"avatar"`
IsActive bool `json:"isActive" gorm:"default:false"`
CompanyID uint `json:"companyId"`
LoginAttempts int `gorm:"default:0"`
}
type ResetPasswordRequest struct {

View File

@@ -1639,6 +1639,195 @@
"body": "{\n \"id\": 9,\n \"message\": \"Successfully received and saved contract\"\n}"
}
]
},
{
"name": "Login",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTkzNTIwOTYsInN1YiI6IjEifQ.DV5AEHzgKCr42lWc7ZoniYkaSWiWWdC4tBmyUqN_iTc",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{URL}}/user/logout",
"host": [
"{{URL}}"
],
"path": [
"user",
"logout"
]
}
},
"response": [
{
"name": "Create Contracts",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"sellerId\": 1,\n \"buyerId\": 2,\n \"description\": \"This is a sample contract.\",\n \"productId\": 3,\n \"minTemp\": -20.0,\n \"maxTemp\": 40.0,\n \"arrivalDate\": 1674019200,\n \"penaltyType\": \"AMOUNT\",\n \"penaltyValue\": 100,\n \"penaltyRec\": \"DAILY\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{URL}}/contracts/create",
"host": [
"{{URL}}"
],
"path": [
"contracts",
"create"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Access-Control-Allow-Credentials",
"value": "true"
},
{
"key": "Access-Control-Allow-Headers",
"value": "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With"
},
{
"key": "Access-Control-Allow-Methods",
"value": "POST, OPTIONS, GET, PUT"
},
{
"key": "Access-Control-Allow-Origin",
"value": "*"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Date",
"value": "Fri, 06 Oct 2023 08:40:15 GMT"
},
{
"key": "Content-Length",
"value": "61"
}
],
"cookie": [],
"body": "{\n \"id\": 9,\n \"message\": \"Successfully received and saved contract\"\n}"
}
]
},
{
"name": "Logout",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"password\": \"someemail@gmail.com\",\n \"token\": \"sb6qxahXINNsg52dH0Q7u7iR6yaPLRRQ4OnbUWlxEo0=\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{URL}}/user/set/password",
"host": [
"{{URL}}"
],
"path": [
"user",
"set",
"password"
]
}
},
"response": [
{
"name": "Create Contracts",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"sellerId\": 1,\n \"buyerId\": 2,\n \"description\": \"This is a sample contract.\",\n \"productId\": 3,\n \"minTemp\": -20.0,\n \"maxTemp\": 40.0,\n \"arrivalDate\": 1674019200,\n \"penaltyType\": \"AMOUNT\",\n \"penaltyValue\": 100,\n \"penaltyRec\": \"DAILY\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{URL}}/contracts/create",
"host": [
"{{URL}}"
],
"path": [
"contracts",
"create"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Access-Control-Allow-Credentials",
"value": "true"
},
{
"key": "Access-Control-Allow-Headers",
"value": "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With"
},
{
"key": "Access-Control-Allow-Methods",
"value": "POST, OPTIONS, GET, PUT"
},
{
"key": "Access-Control-Allow-Origin",
"value": "*"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Date",
"value": "Fri, 06 Oct 2023 08:40:15 GMT"
},
{
"key": "Content-Length",
"value": "61"
}
],
"cookie": [],
"body": "{\n \"id\": 9,\n \"message\": \"Successfully received and saved contract\"\n}"
}
]
}
]
}

View File

@@ -52,4 +52,6 @@ func RegisterPublicRoutes(r *gin.Engine) {
//Users
r.POST("/user/reset/password", controllers.ResetPassword)
r.POST("/user/set/password", controllers.UpdatePassword)
r.POST("/user/login", controllers.Login)
r.POST("/user/logout", controllers.Logout)
}

View File

@@ -35,7 +35,8 @@ func Init() error {
//TODO AUTOMIGRATE models once we have them
db.AutoMigrate(&models.User{}, &models.Company{}, &models.Device{}, &models.DeviceInfo{},
&models.Contract{}, &models.ContractInfo{},
&models.ProductTemplate{}, &models.TextTemplate{}, &models.Invoice{}, &models.InvoiceItem{}, &models.Notification{}, models.PasswordTokens{})
&models.ProductTemplate{}, &models.TextTemplate{}, &models.Invoice{}, &models.InvoiceItem{},
&models.Notification{}, models.PasswordTokens{}, models.SessionToken{})
return nil