Added reset password route
This commit is contained in:
@@ -45,6 +45,10 @@ func Load() error {
|
||||
WalletAddress: getEnv("NOVATECH_BLOCKCHAIN_WALLET_ADDRESS", ""),
|
||||
WalletPrivateKey: getEnv("NOVATECH_BLOCKCHAIN_WALLET_PRIVATE_KEY", ""),
|
||||
},
|
||||
AWS: AWS {
|
||||
AccessKey: getEnv("AWS_ACCESS_KEY_ID", ""),
|
||||
SecretKey: getEnv("AWS_SECRET_ACCESS_KEY", ""),
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -6,6 +6,7 @@ type Config struct {
|
||||
AdminService Service
|
||||
Database Database
|
||||
Blockchain Blockchain
|
||||
AWS AWS
|
||||
}
|
||||
|
||||
// Service contains configuration for service
|
||||
@@ -32,3 +33,8 @@ type Database struct {
|
||||
HostName string
|
||||
Port string
|
||||
}
|
||||
|
||||
type AWS struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
113
controllers/users_controller.go
Normal file
113
controllers/users_controller.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gitlab.com/pactual1/backend/database/user"
|
||||
"gitlab.com/pactual1/backend/models"
|
||||
"gitlab.com/pactual1/backend/services/messaging"
|
||||
"gitlab.com/pactual1/backend/shared"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func ResetPassword(c *gin.Context) {
|
||||
var req models.ResetPasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
dbUser, err := user.GetUserByEmail(req.Email)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
resetToken, err := GenerateResetToken()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err = user.SaveResetTokenToDB(dbUser.ID, resetToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
subject := "Password Reset Request"
|
||||
body := "Here is your password reset link: https://pactualdev.com/setNewPassword?token=" + resetToken
|
||||
email := models.EmailNotification{Body: body, Subject: subject, Email: dbUser.Email}
|
||||
|
||||
go func(email models.EmailNotification) {
|
||||
|
||||
emailChannel := messaging.GetEmailChannel()
|
||||
emailChannel <- email
|
||||
|
||||
}(email)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Reset email sent"})
|
||||
}
|
||||
|
||||
func GenerateResetToken() (string, error) {
|
||||
// Generate 32 random bytes (256 bits)
|
||||
randomBytes := make([]byte, 32)
|
||||
_, err := rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
return "", err // return an error if there was one
|
||||
}
|
||||
|
||||
// Encode the random bytes into a URL-safe base64 string
|
||||
resetToken := base64.URLEncoding.EncodeToString(randomBytes)
|
||||
|
||||
return resetToken, nil
|
||||
}
|
||||
|
||||
func UpdatePassword(c *gin.Context) {
|
||||
var req models.UpdatePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Find the PasswordTokens entry
|
||||
var passwordToken models.PasswordTokens
|
||||
if err := shared.GetDb().Where("token = ?", req.Token).First(&passwordToken).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Invalid token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Find the associated User
|
||||
var user models.User
|
||||
if err := shared.GetDb().Where("id = ?", passwordToken.UserID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Hash the password before saving it
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update the user's password and set them as active
|
||||
user.Password = string(hashedPassword)
|
||||
user.IsActive = true
|
||||
if err := shared.GetDb().Save(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the PasswordTokens entry
|
||||
if err := shared.GetDb().Delete(&passwordToken).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"})
|
||||
}
|
||||
42
database/user/user.go
Normal file
42
database/user/user.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"gitlab.com/pactual1/backend/models"
|
||||
"gitlab.com/pactual1/backend/shared"
|
||||
)
|
||||
|
||||
func SaveResetTokenToDB(userID uint, resetToken string) error {
|
||||
// Calculate the expiry date (one month from now)
|
||||
expiryDate := time.Now().AddDate(0, 1, 0).Format(time.RFC3339)
|
||||
|
||||
// Create a new PasswordTokens instance
|
||||
passwordToken := models.PasswordTokens{
|
||||
UserID: userID,
|
||||
Token: resetToken,
|
||||
ExpiryDate: expiryDate,
|
||||
}
|
||||
|
||||
// Save the password token to the database
|
||||
if err := shared.GetDb().Create(&passwordToken).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetUserByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
|
||||
// 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, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -13,3 +13,5 @@ NOVATECH_BLOCKCHAIN_CONTRACT_ADDRESS=0x121Cb4bFEeDb55d598D8F5e9EeDF8bB14c421d96
|
||||
NOVATECH_BLOCKCHAIN_WALLET_ADDRESS=0x20eff5decaed29bd64f0c6385956363eeaaf4d3e
|
||||
NOVATECH_BLOCKCHAIN_WALLET_PRIVATE_KEY=PRIVATE_KEY
|
||||
NOVATECH_SERVICE_MAPBOX_ACCESS_TOKEN=pk.ey
|
||||
AWS_ACCESS_KEY_ID:access
|
||||
AWS_SECRET_ACCESS_KEY:secret
|
||||
8
go.mod
8
go.mod
@@ -3,6 +3,7 @@ module gitlab.com/pactual1/backend
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.46.6
|
||||
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
|
||||
@@ -66,6 +67,7 @@ require (
|
||||
github.com/holiman/uint256 v1.2.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
@@ -106,12 +108,12 @@ 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.13.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.15.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
|
||||
36
go.sum
36
go.sum
@@ -15,6 +15,8 @@ github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9Pq
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/aws/aws-sdk-go v1.34.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aws/aws-sdk-go v1.46.6 h1:6wFnNC9hETIZLMf6SOTN7IcclrOGwp/n9SLp8Pjt6E8=
|
||||
github.com/aws/aws-sdk-go v1.46.6/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -169,6 +171,10 @@ github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
|
||||
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@@ -343,6 +349,7 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yosssi/gohtml v0.0.0-20200519115854-476f5b4b8047 h1:YWaOkupKL+BRRJSWRq/uhSkWXc1K0QVIYVG36XUBGOc=
|
||||
github.com/yosssi/gohtml v0.0.0-20200519115854-476f5b4b8047/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
@@ -351,24 +358,32 @@ golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -382,26 +397,34 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
@@ -413,6 +436,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
32
main.go
32
main.go
@@ -37,22 +37,35 @@ func main() {
|
||||
Admin := admin.New(&admin.AdminConfig{DB: shared.GetDb()})
|
||||
Admin.RegisterViewPath("app/views/qor")
|
||||
fmt.Printf("Admin instance: %+v\n", Admin)
|
||||
|
||||
// Allow Admin to manage User resource
|
||||
company := Admin.AddResource(&models.Company{})
|
||||
company.Meta(&admin.Meta{Name: "Users", Config: &admin.SelectManyConfig{SelectMode: "bottom_sheet"}})
|
||||
company.Meta(&admin.Meta{Name: "Devices", Config: &admin.SelectManyConfig{SelectMode: "bottom_sheet"}})
|
||||
// company.Meta(&admin.Meta{Name: "DeviceInfos", Config: &admin.SelectManyConfig{SelectMode: "bottom_sheet"}})
|
||||
|
||||
// Add User and Device resources
|
||||
Admin.AddResource(&models.User{})
|
||||
Admin.AddResource(&models.Device{})
|
||||
Admin.AddResource(&models.Company{})
|
||||
Admin.AddResource(&models.ProductTemplate{})
|
||||
// Initialize HTTP request multiplexer
|
||||
company.Meta(&admin.Meta{Name: "Users", Config: &admin.SelectManyConfig{SelectMode: "select"}})
|
||||
company.Meta(&admin.Meta{Name: "Devices", Config: &admin.SelectManyConfig{SelectMode: "bottom_sheet"}})
|
||||
// // Initialize HTTP request multiplexe
|
||||
mux := http.NewServeMux()
|
||||
// Mount admin interface to mux
|
||||
// // Mount admin interface to mux
|
||||
Admin.MountTo("/admin", mux)
|
||||
|
||||
// // Setup Admin
|
||||
// Admin := admin.New(&admin.AdminConfig{DB: shared.GetDb()})
|
||||
// Admin.AddResource(&models.Student{})
|
||||
// school := Admin.AddResource(&models.School{})
|
||||
// school.Meta(&admin.Meta{Name: "Students", Config: &admin.SelectManyConfig{SelectMode: "select"}})
|
||||
// Admin.MountTo("/admin", mux)
|
||||
|
||||
// school.SaveHandler = func(value interface{}, ctx *qor.Context) error {
|
||||
// schoolRecord := value.(*models.School)
|
||||
// log.Printf("Executed handler %v", schoolRecord)
|
||||
// log.Printf("Executed handler %v", schoolRecord.Students)
|
||||
|
||||
// return ctx.GetDB().Save(schoolRecord).Error
|
||||
// }
|
||||
|
||||
// Start the admin server in a separate goroutine
|
||||
go func() {
|
||||
port := config.AppConfig.Service.Port
|
||||
@@ -66,10 +79,13 @@ func main() {
|
||||
|
||||
// Start services and pass the respective channels
|
||||
notificationCh := make(chan models.Notification, 100)
|
||||
// Start services and pass the respective channels
|
||||
emailCh := make(chan models.EmailNotification, 100)
|
||||
// Create a new messaging service
|
||||
messagingService := messaging.NewService(notificationCh, shared.GetDb())
|
||||
messagingService := messaging.NewService(notificationCh, emailCh, shared.GetDb())
|
||||
// Run the messaging service
|
||||
go messagingService.MessagingService()
|
||||
go messagingService.SendEmailService()
|
||||
go erp.ERPService(erpChannel)
|
||||
|
||||
// Sending messages via channels
|
||||
|
||||
@@ -22,6 +22,20 @@ type Company struct {
|
||||
Devices []Device `json:"devices"`
|
||||
}
|
||||
|
||||
type School struct {
|
||||
BaseModel
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Students []Student `json:"students" gorm:"foreignKey:SchoolID"`
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
BaseModel
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
SchoolID uint `json:"schoolId" gorm:"index"`
|
||||
}
|
||||
|
||||
type CompanyShortResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -14,6 +14,11 @@ type Notification struct {
|
||||
CompanyID int
|
||||
}
|
||||
|
||||
type EmailNotification struct {
|
||||
Subject string
|
||||
Email string
|
||||
Body string
|
||||
}
|
||||
|
||||
type GetNotificationsResponse struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
18
models/password_tokens.go
Normal file
18
models/password_tokens.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
type PasswordTokens struct {
|
||||
BaseModel
|
||||
UserID uint `json:"username"`
|
||||
Token string `json:"token"`
|
||||
ExpiryDate string `json:"expiryDate"`
|
||||
}
|
||||
|
||||
func (PasswordTokens) Update() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (PasswordTokens) Create() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (PasswordTokens) Delete() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -6,9 +6,18 @@ type User struct {
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
IsActive bool `json:"isActive" gorm:"default:false"`
|
||||
CompanyID uint `json:"companyId"`
|
||||
}
|
||||
|
||||
type ResetPasswordRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
type UpdatePasswordRequest struct {
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (User) Update() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -1459,6 +1459,186 @@
|
||||
"body": "{\n \"id\": 9,\n \"message\": \"Successfully received and saved contract\"\n}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reset password",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"someemail@gmail.com\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{URL}}/user/reset/password",
|
||||
"host": [
|
||||
"{{URL}}"
|
||||
],
|
||||
"path": [
|
||||
"user",
|
||||
"reset",
|
||||
"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}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Set password",
|
||||
"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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -48,4 +48,8 @@ func RegisterPublicRoutes(r *gin.Engine) {
|
||||
r.GET("/stats/contracts/total", controllers.GetTotalContractCount)
|
||||
r.GET("/stats/invoices", controllers.GetInvoiceCountByStatus)
|
||||
r.GET("/stats/milestones", controllers.GetContractsMatchingDeviceLocation)
|
||||
|
||||
//Users
|
||||
r.POST("/user/reset/password", controllers.ResetPassword)
|
||||
r.POST("/user/set/password", controllers.UpdatePassword)
|
||||
}
|
||||
|
||||
@@ -2,24 +2,48 @@ package messaging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ses"
|
||||
"github.com/jinzhu/gorm"
|
||||
"gitlab.com/pactual1/backend/config"
|
||||
"gitlab.com/pactual1/backend/models"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
ch chan models.Notification
|
||||
db *gorm.DB
|
||||
ech chan models.EmailNotification
|
||||
ses *ses.SES
|
||||
}
|
||||
|
||||
var MessagingChannel chan models.Notification
|
||||
var EmailChannel chan models.EmailNotification
|
||||
|
||||
func NewService(ch chan models.Notification, db *gorm.DB) service {
|
||||
func NewService(ch chan models.Notification, ech chan models.EmailNotification, db *gorm.DB) service {
|
||||
MessagingChannel = ch
|
||||
EmailChannel = ech
|
||||
log.Printf("Aws %v , %v", config.AppConfig.AWS.AccessKey, config.AppConfig.AWS.SecretKey)
|
||||
// Create a new session in the us-west-2 region.
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
Credentials: credentials.NewStaticCredentials(config.AppConfig.AWS.AccessKey, config.AppConfig.AWS.SecretKey, "")},
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating AWS session: %v\n", err)
|
||||
return service{}
|
||||
}
|
||||
// Create an SES session.
|
||||
svc := ses.New(sess)
|
||||
return service{
|
||||
ch: MessagingChannel,
|
||||
db: db,
|
||||
ech: EmailChannel,
|
||||
ses: svc,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s service) MessagingService() {
|
||||
@@ -31,8 +55,45 @@ func (s service) MessagingService() {
|
||||
fmt.Printf("Error saving notification to DB: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s service) SendEmailService() {
|
||||
for emailNotification := range s.ech {
|
||||
fmt.Println("Email Service received: ", emailNotification)
|
||||
|
||||
// Send email via SES
|
||||
input := &ses.SendEmailInput{
|
||||
Destination: &ses.Destination{
|
||||
ToAddresses: []*string{
|
||||
aws.String(emailNotification.Email),
|
||||
},
|
||||
},
|
||||
Message: &ses.Message{
|
||||
Body: &ses.Body{
|
||||
Text: &ses.Content{
|
||||
Data: aws.String(emailNotification.Body),
|
||||
},
|
||||
},
|
||||
Subject: &ses.Content{
|
||||
Data: aws.String(emailNotification.Subject),
|
||||
},
|
||||
},
|
||||
Source: aws.String("app@pactualdev.com"), // Replace with your SES verified email address
|
||||
}
|
||||
_, err := s.ses.SendEmail(input)
|
||||
if err != nil {
|
||||
fmt.Printf("Error sending email: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Email sent successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetMessagingChannel() chan models.Notification {
|
||||
return MessagingChannel
|
||||
}
|
||||
|
||||
func GetEmailChannel() chan models.EmailNotification {
|
||||
return EmailChannel
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ func Init() error {
|
||||
dbName := config.AppConfig.Database.DatabaseName
|
||||
password := config.AppConfig.Database.Password
|
||||
|
||||
|
||||
dbString := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable password=%s", host, user, dbName, password)
|
||||
// db, err = gorm.Open("postgres", "host=localhost user=postgres dbname=postgres sslmode=disable password=root")
|
||||
var err error
|
||||
@@ -27,7 +26,6 @@ func Init() error {
|
||||
// //PostgreSQL
|
||||
db, err = gorm.Open("postgres", dbString)
|
||||
|
||||
|
||||
db.LogMode(true)
|
||||
|
||||
if err != nil {
|
||||
@@ -37,7 +35,7 @@ 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.ProductTemplate{}, &models.TextTemplate{}, &models.Invoice{}, &models.InvoiceItem{}, &models.Notification{}, models.PasswordTokens{})
|
||||
|
||||
return nil
|
||||
|
||||
|
||||
Reference in New Issue
Block a user