mirror of
https://github.com/makayabou/asg-server.git
synced 2026-05-02 17:43:36 +02:00
[auth] allow to change password from device
This commit is contained in:
parent
a1f606734f
commit
cf6ceaef98
2
go.mod
2
go.mod
@ -4,7 +4,7 @@ go 1.22.0
|
||||
|
||||
require (
|
||||
firebase.google.com/go/v4 v4.12.1
|
||||
github.com/android-sms-gateway/client-go v1.1.0
|
||||
github.com/android-sms-gateway/client-go v1.2.0
|
||||
github.com/ansrivas/fiberprometheus/v2 v2.6.1
|
||||
github.com/capcom6/go-helpers v0.0.0-20240521035631-865ee2879fa3
|
||||
github.com/capcom6/go-infra-fx v0.2.0
|
||||
|
||||
10
go.sum
10
go.sum
@ -26,8 +26,10 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/android-sms-gateway/client-go v1.1.0 h1:mAPsueRrY/qOdQAU5yO3uLQAb7Po+3jBxB1tiqyClvg=
|
||||
github.com/android-sms-gateway/client-go v1.1.0/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/android-sms-gateway/client-go v1.1.1-0.20241130131931-31efddf9578d h1:8LYyHCkZP5y0Wsa+DhRUv5NCpS72IDwtvkF0+Qqy+SQ=
|
||||
github.com/android-sms-gateway/client-go v1.1.1-0.20241130131931-31efddf9578d/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/android-sms-gateway/client-go v1.2.0 h1:P02e/Nm2XY6gpxVQVZiaxh1ZfInVkwfOLzz8Mp/1dy0=
|
||||
github.com/android-sms-gateway/client-go v1.2.0/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/ansrivas/fiberprometheus/v2 v2.6.1 h1:wac3pXaE6BYYTF04AC6K0ktk6vCD+MnDOJZ3SK66kXM=
|
||||
@ -39,10 +41,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/capcom6/go-helpers v0.0.0-20240521035631-865ee2879fa3 h1:mq9rmBMCCzqGnZtbQqFSd+Ua3fahqUOYaTf26YFhWJc=
|
||||
github.com/capcom6/go-helpers v0.0.0-20240521035631-865ee2879fa3/go.mod h1:WDqc7HZNqHxUTisArkYIBZtqUfJBVyPWeQI+FMwEzAw=
|
||||
github.com/capcom6/go-infra-fx v0.0.2 h1:Vj2bHqCokDRa5qfHoPa4zVVKIo1QGzCPF+9EMQ9upQc=
|
||||
github.com/capcom6/go-infra-fx v0.0.2/go.mod h1:Mc7KClD8Z5wMiUAF9rxifMc39E9mMrSrylpqHzVfPM4=
|
||||
github.com/capcom6/go-infra-fx v0.1.0 h1:RZ0gxFtR2ehopDzSnXSCVJ8I2C4oBUaCz42sQQp75dM=
|
||||
github.com/capcom6/go-infra-fx v0.1.0/go.mod h1:T/DnT1EDrF9F+44eZw/lZnmsz5Dry0w/CTk0FB1Nct0=
|
||||
github.com/capcom6/go-infra-fx v0.2.0 h1:FrWtdFiG58unIK7xN7kMJn3LfOFecp20W/ZVgvN3bsM=
|
||||
github.com/capcom6/go-infra-fx v0.2.0/go.mod h1:T/DnT1EDrF9F+44eZw/lZnmsz5Dry0w/CTk0FB1Nct0=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
|
||||
@ -178,6 +178,35 @@ func (h *mobileHandler) patchMessage(device models.Device, c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary Change password
|
||||
// @Description Changes the user's password
|
||||
// @Security MobileToken
|
||||
// @Tags Device
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body smsgateway.MobileChangePasswordRequest true "Password change request"
|
||||
// @Success 204 {object} nil "Password changed successfully"
|
||||
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
|
||||
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
|
||||
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
|
||||
// @Router /mobile/v1/user/password [patch]
|
||||
//
|
||||
// Change password
|
||||
func (h *mobileHandler) changePassword(device models.Device, c *fiber.Ctx) error {
|
||||
req := smsgateway.MobileChangePasswordRequest{}
|
||||
|
||||
if err := h.BodyParserValidator(c, &req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if err := h.authSvc.ChangePassword(device.UserID, req.CurrentPassword, req.NewPassword); err != nil {
|
||||
h.Logger.Error("failed to change password", zap.Error(err))
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid current password")
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *mobileHandler) Register(router fiber.Router) {
|
||||
router = router.Group("/mobile/v1")
|
||||
|
||||
@ -226,6 +255,8 @@ func (h *mobileHandler) Register(router fiber.Router) {
|
||||
router.Get("/message", auth.WithDevice(h.getMessage))
|
||||
router.Patch("/message", auth.WithDevice(h.patchMessage))
|
||||
|
||||
router.Patch("/user/password", auth.WithDevice(h.changePassword))
|
||||
|
||||
h.webhooksCtrl.Register(router.Group("/webhooks"))
|
||||
}
|
||||
|
||||
|
||||
@ -24,3 +24,7 @@ func (r *repository) GetByLogin(login string) (models.User, error) {
|
||||
func (r *repository) Insert(user *models.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdatePassword(userID string, passwordHash string) error {
|
||||
return r.db.Model(&models.User{}).Where("id = ?", userID).Update("password_hash", passwordHash).Error
|
||||
}
|
||||
|
||||
@ -158,3 +158,32 @@ func (s *Service) AuthorizeUser(username, password string) (models.User, error)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Service) ChangePassword(userID string, currentPassword string, newPassword string) error {
|
||||
user, err := s.users.GetByLogin(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if err := crypto.CompareBCryptHash(user.PasswordHash, currentPassword); err != nil {
|
||||
return fmt.Errorf("current password is incorrect: %w", err)
|
||||
}
|
||||
|
||||
newHash, err := crypto.MakeBCryptHash(newPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash new password: %w", err)
|
||||
}
|
||||
|
||||
if err := s.users.UpdatePassword(userID, newHash); err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
}
|
||||
|
||||
// Invalidate cache
|
||||
hash := sha256.Sum256([]byte(userID + currentPassword))
|
||||
cacheKey := hex.EncodeToString(hash[:])
|
||||
if err := s.usersCache.Delete(cacheKey); err != nil {
|
||||
s.logger.Error("can't invalidate user cache", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -44,4 +44,14 @@ Content-Type: application/json
|
||||
|
||||
###
|
||||
GET {{baseUrl}}/webhooks HTTP/1.1
|
||||
Authorization: Bearer {{mobileToken}}
|
||||
Authorization: Bearer {{mobileToken}}
|
||||
|
||||
###
|
||||
PATCH {{baseUrl}}/user/password HTTP/1.1
|
||||
Authorization: Bearer {{mobileToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"currentPassword": "wsmgz1akhoo24o",
|
||||
"newPassword": "wsmgz1akhoo24o"
|
||||
}
|
||||
|
||||
@ -641,6 +641,60 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mobile/v1/user/password": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"MobileToken": []
|
||||
}
|
||||
],
|
||||
"description": "Changes the user's password",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Device"
|
||||
],
|
||||
"summary": "Change password",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Password change request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.MobileChangePasswordRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Password changed successfully"
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mobile/v1/webhooks": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -1021,6 +1075,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"smsgateway.MobileChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"currentPassword",
|
||||
"newPassword"
|
||||
],
|
||||
"properties": {
|
||||
"currentPassword": {
|
||||
"description": "Current password",
|
||||
"type": "string",
|
||||
"example": "cp2pydvxd2zwpx"
|
||||
},
|
||||
"newPassword": {
|
||||
"description": "New password, at least 14 characters",
|
||||
"type": "string",
|
||||
"minLength": 14,
|
||||
"example": "cp2pydvxd2zwpx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"smsgateway.MobileDeviceResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@ -211,6 +211,21 @@ definitions:
|
||||
- recipients
|
||||
- state
|
||||
type: object
|
||||
smsgateway.MobileChangePasswordRequest:
|
||||
properties:
|
||||
currentPassword:
|
||||
description: Current password
|
||||
example: cp2pydvxd2zwpx
|
||||
type: string
|
||||
newPassword:
|
||||
description: New password, at least 14 characters
|
||||
example: cp2pydvxd2zwpx
|
||||
minLength: 14
|
||||
type: string
|
||||
required:
|
||||
- currentPassword
|
||||
- newPassword
|
||||
type: object
|
||||
smsgateway.MobileDeviceResponse:
|
||||
properties:
|
||||
device:
|
||||
@ -790,6 +805,40 @@ paths:
|
||||
tags:
|
||||
- Device
|
||||
- Messages
|
||||
/mobile/v1/user/password:
|
||||
patch:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Changes the user's password
|
||||
parameters:
|
||||
- description: Password change request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.MobileChangePasswordRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: Password changed successfully
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
security:
|
||||
- MobileToken: []
|
||||
summary: Change password
|
||||
tags:
|
||||
- Device
|
||||
/mobile/v1/webhooks:
|
||||
get:
|
||||
description: Returns list of registered webhooks for device
|
||||
|
||||
8
pkg/types/cache/cache.go
vendored
8
pkg/types/cache/cache.go
vendored
@ -61,6 +61,14 @@ func (c *Cache[T]) Get(key string) (T, error) {
|
||||
return item.value, nil
|
||||
}
|
||||
|
||||
func (c *Cache[T]) Delete(key string) error {
|
||||
c.mux.Lock()
|
||||
delete(c.items, key)
|
||||
c.mux.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache[T]) Drain() map[string]T {
|
||||
t := time.Now()
|
||||
|
||||
|
||||
@ -1,16 +1,46 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func makeClient(baseUrl string) *resty.Client {
|
||||
return resty.New().
|
||||
SetBaseURL(baseUrl).
|
||||
SetTimeout(300 * time.Millisecond)
|
||||
var (
|
||||
publicClient = resty.New().
|
||||
SetBaseURL(PublicURL + "/mobile/v1").
|
||||
SetTimeout(300 * time.Millisecond)
|
||||
privateClient = resty.New().
|
||||
SetBaseURL(PrivateURL + "/mobile/v1").
|
||||
SetTimeout(300 * time.Millisecond)
|
||||
)
|
||||
|
||||
type mobileRegisterResponse struct {
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func mobileDeviceRegister(t *testing.T, client *resty.Client) mobileRegisterResponse {
|
||||
res, err := client.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(`{"name": "Public Device Name", "pushToken": "token"}`).
|
||||
Post("device")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !res.IsSuccess() {
|
||||
t.Fatal(res.StatusCode(), res.String())
|
||||
}
|
||||
|
||||
var resp mobileRegisterResponse
|
||||
if err := json.Unmarshal(res.Body(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func TestPublicDeviceRegister(t *testing.T) {
|
||||
@ -40,15 +70,13 @@ func TestPublicDeviceRegister(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
client := makeClient(PublicURL + "/mobile/v1/device")
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
res, err := client.R().
|
||||
res, err := publicClient.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(`{"name": "Public Device Name", "pushToken": "token"}`).
|
||||
SetHeaders(c.headers).
|
||||
Post("")
|
||||
Post("device")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -87,7 +115,7 @@ func TestPrivateDeviceRegister(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
client := makeClient(PrivateURL + "/mobile/v1/device")
|
||||
client := privateClient
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
@ -95,7 +123,76 @@ func TestPrivateDeviceRegister(t *testing.T) {
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(`{"name": "Private Device Name", "pushToken": "token"}`).
|
||||
SetHeaders(c.headers).
|
||||
Post("")
|
||||
Post("device")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res.StatusCode() != c.expectedStatusCode {
|
||||
t.Fatal(res.StatusCode(), res.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicDevicePasswordChange(t *testing.T) {
|
||||
device := mobileDeviceRegister(t, publicClient)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
headers map[string]string
|
||||
body string
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{
|
||||
name: "with invalid token",
|
||||
headers: map[string]string{
|
||||
"Authorization": "Bearer 123456789",
|
||||
},
|
||||
body: `{"currentPassword": "123456789", "newPassword": "123456789"}`,
|
||||
expectedStatusCode: 401,
|
||||
},
|
||||
{
|
||||
name: "with invalid password",
|
||||
headers: map[string]string{
|
||||
"Authorization": "Bearer " + device.Token,
|
||||
},
|
||||
body: `{"currentPassword": "123456789", "newPassword": "changemeonemoretime"}`,
|
||||
expectedStatusCode: 401,
|
||||
},
|
||||
{
|
||||
name: "short password",
|
||||
headers: map[string]string{
|
||||
"Authorization": "Bearer " + device.Token,
|
||||
},
|
||||
body: `{"currentPassword": "` + device.Password + `", "newPassword": "changeme"}`,
|
||||
expectedStatusCode: 400,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
headers: map[string]string{
|
||||
"Authorization": "Bearer " + device.Token,
|
||||
},
|
||||
body: `{"currentPassword": "` + device.Password + `", "newPassword": "changemeonemoretime"}`,
|
||||
expectedStatusCode: 204,
|
||||
},
|
||||
{
|
||||
name: "success with new password",
|
||||
headers: map[string]string{
|
||||
"Authorization": "Bearer " + device.Token,
|
||||
},
|
||||
body: `{"currentPassword": "changemeonemoretime", "newPassword": "` + device.Password + `"}`,
|
||||
expectedStatusCode: 204,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
res, err := publicClient.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(c.body).
|
||||
SetHeaders(c.headers).
|
||||
Patch("user/password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user