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:
@@ -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"})
|
||||
}
|
||||
|
||||
@@ -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
3
go.mod
@@ -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
2
go.sum
@@ -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
18
models/session_token.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user