add user management functionality with OTP verification and consent handling, DI introduced
This commit is contained in:
51
internal/api/auth/domain/auth_domain.go
Normal file
51
internal/api/auth/domain/auth_domain.go
Normal file
@ -0,0 +1,51 @@
|
||||
package domain
|
||||
|
||||
import "context"
|
||||
|
||||
type AuthRepository interface {
|
||||
SaveOtpRequest(ctx context.Context, uuid string, code string) error
|
||||
GetOtpRequest(ctx context.Context, uuid string) (*string, error)
|
||||
}
|
||||
|
||||
type ErrOtpNotFound struct {
|
||||
Uuid string
|
||||
}
|
||||
|
||||
func (e ErrOtpNotFound) Error() string {
|
||||
return "OTP request not found for UUID: " + e.Uuid
|
||||
}
|
||||
|
||||
type ErrUserNotFound struct {
|
||||
PhoneNumber string
|
||||
}
|
||||
|
||||
func (e ErrUserNotFound) Error() string {
|
||||
return "User not found with phone number: " + e.PhoneNumber
|
||||
}
|
||||
|
||||
type ErrOtpInvalid struct {
|
||||
Code string
|
||||
Uuid string
|
||||
}
|
||||
|
||||
func (e ErrOtpInvalid) Error() string {
|
||||
return "Invalid OTP code: " + e.Code + " for UUID: " + e.Uuid
|
||||
}
|
||||
|
||||
type ErrInvalidHydraAccept struct {
|
||||
Message string
|
||||
Uuid string
|
||||
}
|
||||
|
||||
func (e ErrInvalidHydraAccept) Error() string {
|
||||
return "Invalid Hydra accept request: " + e.Message + " for UUID: " + e.Uuid
|
||||
}
|
||||
|
||||
type ErrInvalidPhoneNumber struct {
|
||||
PhoneNumber string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrInvalidPhoneNumber) Error() string {
|
||||
return "Invalid phone number: " + e.PhoneNumber + ", error: " + e.Err.Error()
|
||||
}
|
||||
20
internal/api/auth/fx.go
Normal file
20
internal/api/auth/fx.go
Normal file
@ -0,0 +1,20 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"git.logidex.ru/fakz9/logidex-id/internal/api/auth/handler"
|
||||
"git.logidex.ru/fakz9/logidex-id/internal/api/auth/repo"
|
||||
"git.logidex.ru/fakz9/logidex-id/internal/api/auth/service"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
var Module = fx.Options(
|
||||
fx.Provide(
|
||||
repo.NewAuthRepo,
|
||||
service.NewAuthService,
|
||||
handler.NewAuthHandler,
|
||||
),
|
||||
fx.Invoke(func(handler *handler.AuthHandler, router fiber.Router) {
|
||||
handler.RegisterRoutes(router)
|
||||
}),
|
||||
)
|
||||
@ -14,6 +14,9 @@ import (
|
||||
type AcceptConsentRequest struct {
|
||||
// ConsentChallenge The consent challenge to accept
|
||||
ConsentChallenge string `json:"consent_challenge"`
|
||||
|
||||
// PhoneNumber Phone number associated with the consent
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
}
|
||||
|
||||
// AcceptConsentResponse defines model for AcceptConsentResponse.
|
||||
@ -56,6 +59,9 @@ type VerifyOTPRequest struct {
|
||||
|
||||
// VerifyOTPResponse defines model for VerifyOTPResponse.
|
||||
type VerifyOTPResponse struct {
|
||||
// Message Confirmation message
|
||||
Message string `json:"message"`
|
||||
|
||||
// Ok Status of the verification
|
||||
Ok bool `json:"ok"`
|
||||
|
||||
|
||||
@ -2,113 +2,68 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.logidex.ru/fakz9/logidex-id/internal/hydra_client"
|
||||
"git.logidex.ru/fakz9/logidex-id/internal/redis"
|
||||
|
||||
"git.logidex.ru/fakz9/logidex-id/internal/api/auth/service"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
hydraApi "github.com/ory/hydra-client-go"
|
||||
)
|
||||
|
||||
type AuthHandler struct{}
|
||||
|
||||
func (a AuthHandler) PostAuthConsentAccept(ctx context.Context, request PostAuthConsentAcceptRequestObject) (PostAuthConsentAcceptResponseObject, error) {
|
||||
hydraClient := hydra_client.GetClient()
|
||||
|
||||
hydraRequest := hydraApi.AcceptConsentRequest{}
|
||||
hydraRequest.SetGrantScope([]string{"openid"})
|
||||
hydraRequest.SetRemember(true)
|
||||
hydraRequest.SetRememberFor(3600) // 1 hour
|
||||
|
||||
hydraResponse, r, err := hydraClient.AdminApi.
|
||||
AcceptConsentRequest(ctx).
|
||||
ConsentChallenge(request.Body.ConsentChallenge).
|
||||
AcceptConsentRequest(hydraRequest).
|
||||
Execute()
|
||||
if err != nil {
|
||||
return PostAuthConsentAccept400JSONResponse{
|
||||
RedirectUrl: "",
|
||||
Ok: false,
|
||||
Message: "Failed to accept consent request",
|
||||
}, nil
|
||||
}
|
||||
fmt.Println(r)
|
||||
return PostAuthConsentAccept200JSONResponse{
|
||||
RedirectUrl: hydraResponse.RedirectTo,
|
||||
Ok: true,
|
||||
Message: "Успешно",
|
||||
}, nil
|
||||
type AuthHandler struct {
|
||||
service service.AuthService
|
||||
}
|
||||
|
||||
func (a AuthHandler) PostAuthOtpRequest(ctx context.Context, request PostAuthOtpRequestRequestObject) (PostAuthOtpRequestResponseObject, error) {
|
||||
redisClient := redis.GetClient()
|
||||
|
||||
// TODO implement OTP request logic
|
||||
|
||||
err := redisClient.Do(ctx, redisClient.B().Set().Key("otp:"+request.Body.PhoneNumber).Value("123456").Build()).Error()
|
||||
func (h AuthHandler) PostAuthOtpRequest(ctx context.Context, request PostAuthOtpRequestRequestObject) (PostAuthOtpRequestResponseObject, error) {
|
||||
err := h.service.OtpRequest(ctx, request.Body.PhoneNumber)
|
||||
if err != nil {
|
||||
return PostAuthOtpRequest400JSONResponse{
|
||||
Message: "Failed to set OTP in Redis",
|
||||
Message: err.Error(),
|
||||
Ok: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return PostAuthOtpRequest200JSONResponse{
|
||||
Message: "Код успешно отправлен",
|
||||
Message: "OTP request successful",
|
||||
Ok: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a AuthHandler) PostAuthOtpVerify(ctx context.Context, request PostAuthOtpVerifyRequestObject) (PostAuthOtpVerifyResponseObject, error) {
|
||||
redisClient := redis.GetClient()
|
||||
hydraClient := hydra_client.GetClient()
|
||||
|
||||
sentOtp, err := redisClient.Do(ctx, redisClient.B().Get().Key("otp:"+request.Body.PhoneNumber).Build()).ToString()
|
||||
func (h AuthHandler) PostAuthOtpVerify(ctx context.Context, request PostAuthOtpVerifyRequestObject) (PostAuthOtpVerifyResponseObject, error) {
|
||||
redirectUrl, err := h.service.OtpVerify(ctx, request.Body.PhoneNumber, request.Body.Otp, request.Body.LoginChallenge)
|
||||
if err != nil {
|
||||
return PostAuthOtpVerify400JSONResponse{
|
||||
RedirectUrl: "",
|
||||
Message: err.Error(),
|
||||
Ok: false,
|
||||
}, nil
|
||||
}
|
||||
if sentOtp != request.Body.Otp {
|
||||
return PostAuthOtpVerify400JSONResponse{
|
||||
RedirectUrl: "",
|
||||
Ok: false,
|
||||
}, nil
|
||||
}
|
||||
hydraRequest := hydraApi.AcceptLoginRequest{}
|
||||
// TODO read user from database by phone number
|
||||
|
||||
hydraRequest.SetSubject("some-user-id") // Replace with actual user ID
|
||||
hydraRequest.SetRemember(true)
|
||||
hydraRequest.SetRememberFor(3600) // 1 hour
|
||||
|
||||
hydraResponse, r, err := hydraClient.AdminApi.
|
||||
AcceptLoginRequest(ctx).
|
||||
LoginChallenge(request.Body.LoginChallenge).
|
||||
AcceptLoginRequest(hydraRequest).
|
||||
Execute()
|
||||
fmt.Println(r)
|
||||
if err != nil {
|
||||
return PostAuthOtpVerify400JSONResponse{
|
||||
RedirectUrl: "",
|
||||
Ok: false,
|
||||
}, nil
|
||||
}
|
||||
return PostAuthOtpVerify200JSONResponse{
|
||||
RedirectUrl: hydraResponse.RedirectTo,
|
||||
Message: "OTP verification successful",
|
||||
Ok: true,
|
||||
RedirectUrl: redirectUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h AuthHandler) PostAuthConsentAccept(ctx context.Context, request PostAuthConsentAcceptRequestObject) (PostAuthConsentAcceptResponseObject, error) {
|
||||
redirectUrl, err := h.service.AcceptConsent(ctx, request.Body.PhoneNumber, request.Body.ConsentChallenge)
|
||||
if err != nil {
|
||||
return PostAuthConsentAccept400JSONResponse{
|
||||
Message: err.Error(),
|
||||
Ok: false,
|
||||
RedirectUrl: "",
|
||||
}, nil
|
||||
}
|
||||
return PostAuthConsentAccept200JSONResponse{
|
||||
Message: "Consent accepted successfully",
|
||||
Ok: true,
|
||||
RedirectUrl: redirectUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ StrictServerInterface = (*AuthHandler)(nil)
|
||||
|
||||
func NewAuthHandler() *AuthHandler {
|
||||
return &AuthHandler{}
|
||||
func NewAuthHandler(service service.AuthService) *AuthHandler {
|
||||
return &AuthHandler{service: service}
|
||||
}
|
||||
|
||||
func RegisterApp(router fiber.Router) {
|
||||
//authGroup := router.Group("/auth")
|
||||
server := NewStrictHandler(NewAuthHandler(), nil)
|
||||
func (h AuthHandler) RegisterRoutes(router fiber.Router) {
|
||||
server := NewStrictHandler(h, nil)
|
||||
RegisterHandlers(router, server)
|
||||
|
||||
}
|
||||
|
||||
45
internal/api/auth/repo/auth_repo.go
Normal file
45
internal/api/auth/repo/auth_repo.go
Normal file
@ -0,0 +1,45 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.logidex.ru/fakz9/logidex-id/internal/api/auth/domain"
|
||||
"github.com/redis/rueidis"
|
||||
)
|
||||
|
||||
type authRepo struct {
|
||||
redisClient rueidis.Client
|
||||
}
|
||||
|
||||
func (a authRepo) GetOtpRequest(ctx context.Context, uuid string) (*string, error) {
|
||||
redisClient := a.redisClient
|
||||
|
||||
resp, err := redisClient.Do(ctx, redisClient.B().Get().Key("otp:"+uuid).Build()).ToString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp == "" {
|
||||
// create error
|
||||
return nil, &domain.ErrOtpNotFound{Uuid: uuid}
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (a authRepo) SaveOtpRequest(ctx context.Context, uuid string, code string) error {
|
||||
redisClient := a.redisClient
|
||||
|
||||
err := redisClient.Do(ctx, redisClient.B().Set().Key("otp:"+uuid).Value(code).Ex(120*time.Second).Build()).Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func NewAuthRepo(redisClient rueidis.Client) domain.AuthRepository {
|
||||
return &authRepo{redisClient: redisClient}
|
||||
}
|
||||
167
internal/api/auth/service/auth_service.go
Normal file
167
internal/api/auth/service/auth_service.go
Normal file
@ -0,0 +1,167 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"git.logidex.ru/fakz9/logidex-id/internal/api/auth/domain"
|
||||
userDomain "git.logidex.ru/fakz9/logidex-id/internal/api/user/domain"
|
||||
"git.logidex.ru/fakz9/logidex-id/internal/phoneutil"
|
||||
hydraApi "github.com/ory/hydra-client-go"
|
||||
)
|
||||
|
||||
type AuthService interface {
|
||||
OtpRequest(ctx context.Context, phoneNumber string) error
|
||||
OtpVerify(ctx context.Context, phoneNumber, code string, loggingChallenge string) (string, error)
|
||||
AcceptConsent(ctx context.Context, phoneNumber string, challenge string) (string, error)
|
||||
}
|
||||
|
||||
type authService struct {
|
||||
repo domain.AuthRepository
|
||||
userRepo userDomain.UserRepository
|
||||
hydraClient *hydraApi.APIClient
|
||||
}
|
||||
|
||||
func (a authService) AcceptConsent(ctx context.Context, phoneNumber string, challenge string) (string, error) {
|
||||
phoneNumber, err := phoneutil.ParseAndFormatPhoneNumber(phoneNumber)
|
||||
if err != nil {
|
||||
return "", domain.ErrInvalidPhoneNumber{
|
||||
PhoneNumber: phoneNumber,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
user, err := a.userRepo.GetUserByPhoneNumber(ctx, phoneNumber)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if user == nil {
|
||||
return "", domain.ErrUserNotFound{PhoneNumber: phoneNumber}
|
||||
}
|
||||
request := hydraApi.AcceptConsentRequest{}
|
||||
request.SetGrantScope([]string{"openid"})
|
||||
request.SetRemember(true)
|
||||
request.SetRememberFor(3600)
|
||||
|
||||
rsp, rawRsp, err := a.hydraClient.AdminApi.
|
||||
AcceptConsentRequest(ctx).
|
||||
ConsentChallenge(challenge).
|
||||
AcceptConsentRequest(request).
|
||||
Execute()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rawRsp.StatusCode != 200 {
|
||||
return "", domain.ErrInvalidHydraAccept{
|
||||
Message: "Hydra response is nil: " + strconv.Itoa(rawRsp.StatusCode),
|
||||
Uuid: "",
|
||||
}
|
||||
}
|
||||
redirectTo, ok := rsp.GetRedirectToOk()
|
||||
if !ok || redirectTo == nil {
|
||||
return "", domain.ErrInvalidHydraAccept{
|
||||
Message: "Hydra redirectTo is nil",
|
||||
Uuid: "",
|
||||
}
|
||||
}
|
||||
// TODO: Verify user in the database
|
||||
_, err = a.userRepo.VerifyUser(ctx, user.Uuid.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return *redirectTo, nil
|
||||
}
|
||||
|
||||
func (a authService) OtpRequest(ctx context.Context, phoneNumber string) error {
|
||||
phoneNumber, err := phoneutil.ParseAndFormatPhoneNumber(phoneNumber)
|
||||
if err != nil {
|
||||
return domain.ErrInvalidPhoneNumber{
|
||||
PhoneNumber: phoneNumber,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
user, err := a.userRepo.GetUserByPhoneNumber(ctx, phoneNumber)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
if user == nil {
|
||||
// Create a new user if it does not exist
|
||||
user, err = a.userRepo.CreateUser(ctx, phoneNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
code := "123456"
|
||||
err = a.repo.SaveOtpRequest(ctx, user.Uuid.String(), code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO implement sending OTP code via SMS
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a authService) OtpVerify(ctx context.Context, phoneNumber string, code string, loggingChallenge string) (string, error) {
|
||||
phoneNumber, err := phoneutil.ParseAndFormatPhoneNumber(phoneNumber)
|
||||
if err != nil {
|
||||
return "", domain.ErrInvalidPhoneNumber{
|
||||
PhoneNumber: phoneNumber,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
user, err := a.userRepo.GetUserByPhoneNumber(ctx, phoneNumber)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if user == nil {
|
||||
return "", domain.ErrUserNotFound{PhoneNumber: phoneNumber}
|
||||
}
|
||||
otp, err := a.repo.GetOtpRequest(ctx, user.Uuid.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if otp == nil {
|
||||
return "", domain.ErrOtpNotFound{Uuid: user.Uuid.String()}
|
||||
}
|
||||
if *otp != code {
|
||||
return "", domain.ErrOtpInvalid{Uuid: user.Uuid.String(), Code: code}
|
||||
}
|
||||
request := hydraApi.AcceptLoginRequest{}
|
||||
request.SetSubject(user.Uuid.String())
|
||||
request.SetRemember(true)
|
||||
request.SetRememberFor(3600) // 1 hour
|
||||
|
||||
rsp, rawRsp, err := a.hydraClient.AdminApi.
|
||||
AcceptLoginRequest(ctx).
|
||||
LoginChallenge(loggingChallenge).
|
||||
AcceptLoginRequest(request).
|
||||
Execute()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rawRsp.StatusCode != 200 {
|
||||
return "", domain.ErrInvalidHydraAccept{
|
||||
Message: "Hydra response is nil: " + strconv.Itoa(rawRsp.StatusCode),
|
||||
Uuid: user.Uuid.String(),
|
||||
}
|
||||
}
|
||||
|
||||
redirectTo, ok := rsp.GetRedirectToOk()
|
||||
if !ok || redirectTo == nil {
|
||||
return "", domain.ErrInvalidHydraAccept{
|
||||
Message: "Hydra redirectTo is nil",
|
||||
Uuid: user.Uuid.String(),
|
||||
}
|
||||
}
|
||||
return *redirectTo, nil
|
||||
}
|
||||
|
||||
func NewAuthService(repo domain.AuthRepository, userRepo userDomain.UserRepository, hydraClient *hydraApi.APIClient) AuthService {
|
||||
return &authService{
|
||||
repo: repo,
|
||||
userRepo: userRepo,
|
||||
hydraClient: hydraClient,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user