diff --git a/api/auth/api.yaml b/api/auth/api.yaml index 4600833..bfdc9a5 100644 --- a/api/auth/api.yaml +++ b/api/auth/api.yaml @@ -137,9 +137,14 @@ components: type: boolean description: Status of the verification example: true + message: + type: string + description: Confirmation message + example: "OTP verified successfully" required: - redirect_url - ok + - message AcceptConsentRequest: type: object properties: @@ -147,8 +152,14 @@ components: type: string description: The consent challenge to accept example: "challenge123" + phone_number: + type: string + description: Phone number associated with the consent + example: "+79999999999" + maxLength: 15 required: - consent_challenge + - phone_number AcceptConsentResponse: type: object properties: diff --git a/api/user/api.yaml b/api/user/api.yaml index 4f87291..6132469 100644 --- a/api/user/api.yaml +++ b/api/user/api.yaml @@ -5,35 +5,11 @@ info: servers: - url: https://api.example.com/v1 paths: - /users: - get: - summary: Get all users - responses: - '200': - description: A list of users - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/User' - post: - summary: Create a new user - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreate' - responses: - '201': - description: User created - content: - application/json: - schema: - $ref: '#/components/schemas/User' /users/{userId}: get: + tags: + - users + operationId: getUserById summary: Get a user by ID parameters: - name: userId @@ -47,92 +23,65 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + type: object + properties: + user: + $ref: '#/components/schemas/User' + required: + - user + + + '404': description: User not found - put: - summary: Update a user by ID - parameters: - - name: userId - in: path - required: true - schema: - type: string + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "User not found" + required: + - message + /users: + post: + tags: + - users + operationId: createUser + summary: "Create a new user with phone number" requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/UserUpdate' + $ref: '#/components/schemas/UserCreate' responses: '200': - description: User updated + description: User created successfully content: application/json: schema: $ref: '#/components/schemas/User' - '404': - description: User not found - delete: - summary: Delete a user by ID - parameters: - - name: userId - in: path - required: true - schema: - type: string - responses: - '204': - description: User deleted successfully - '404': - description: User not found - components: schemas: User: type: object properties: - id: + uuid: type: string example: "123" - username: + phone_number: type: string example: "johndoe" - email: - type: string - format: email - example: "johndoe@example.com" required: - - id - - username - - email + - uuid + - phone_number UserCreate: type: object properties: - username: + phone_number: type: string - example: "johndoe" - email: - type: string - format: email - example: "johndoe@example.com" - password: - type: string - format: password + required: - - username - - email - - password - UserUpdate: - type: object - properties: - username: - type: string - example: "john_doe_updated" - email: - type: string - format: email - example: "johnupdated@example.com" - required: - - username - - email + - phone_number diff --git a/cmd/api/main.go b/cmd/api/main.go index 3e2a68b..2935cb5 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,28 +1,57 @@ package main import ( - authApi "git.logidex.ru/fakz9/logidex-id/internal/api/auth/handler" + "context" + "log" + "strconv" + + "git.logidex.ru/fakz9/logidex-id/internal/api/auth" + "git.logidex.ru/fakz9/logidex-id/internal/api/user" "git.logidex.ru/fakz9/logidex-id/internal/config" + "git.logidex.ru/fakz9/logidex-id/internal/db" "git.logidex.ru/fakz9/logidex-id/internal/hydra_client" "git.logidex.ru/fakz9/logidex-id/internal/redis" "github.com/gofiber/fiber/v2" - "strconv" + "go.uber.org/fx" ) -func main() { - config.Init() - err := redis.Init() - if err != nil { - panic(err) - } - hydra_client.InitClient() - +func NewFiberApp(cfg config.Config) *fiber.App { app := fiber.New() - api := app.Group("/api") - authApi.RegisterApp(api) - - err = app.Listen(":" + strconv.Itoa(config.Cfg.App.Port)) - if err != nil { - panic(err) - } + return app +} + +func StartFiberApp(lifecycle fx.Lifecycle, app *fiber.App, cfg config.Config) { + lifecycle.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + go func() { + if err := app.Listen(":" + strconv.Itoa(cfg.App.Port)); err != nil { + log.Fatal(err) + } + }() + + return nil + }, + OnStop: func(ctx context.Context) error { + return app.Shutdown() + }, + }) +} +func NewFiberRouter(app *fiber.App) fiber.Router { + return app.Group("/api") +} + +func main() { + fx.New( + fx.Provide( + config.NewConfig, + redis.NewRedisClient, + hydra_client.NewHydraClient, + NewFiberApp, + NewFiberRouter, + ), + db.Module, + user.Module, + auth.Module, + fx.Invoke(StartFiberApp), + ).Run() } diff --git a/combine.sh b/combine.sh new file mode 100755 index 0000000..541ca91 --- /dev/null +++ b/combine.sh @@ -0,0 +1 @@ +swagger-cli bundle openapi.yaml --outfile combined.yaml --type yaml \ No newline at end of file diff --git a/config.yaml b/config.yaml index 7b8a4f2..a9b5dee 100644 --- a/config.yaml +++ b/config.yaml @@ -7,4 +7,11 @@ redis: db: 0 hydra: - host: https://oauth2.logidex.ru/admin \ No newline at end of file + host: https://oauth2.logidex.ru/admin + +db: + host: localhost + port: 5432 + user: postgres + password: postgres + dbname: logidex-id diff --git a/go.mod b/go.mod index 0e82ea2..a3c9a3b 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.5 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -37,6 +38,7 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/nyaruka/phonenumbers v1.6.4 // indirect github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect @@ -74,6 +76,8 @@ require ( github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect go.uber.org/atomic v1.11.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.40.0 // indirect diff --git a/go.sum b/go.sum index 95224e5..7b7cd45 100644 --- a/go.sum +++ b/go.sum @@ -157,6 +157,8 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -187,6 +189,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nyaruka/phonenumbers v1.6.4 h1:GFAa844VqRKJvO7oboosM1q3gFVgYvyNe0O6CCbg33A= +github.com/nyaruka/phonenumbers v1.6.4/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU= github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w= github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= @@ -296,6 +300,10 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= diff --git a/internal/api/auth/domain/auth_domain.go b/internal/api/auth/domain/auth_domain.go new file mode 100644 index 0000000..d0eaf00 --- /dev/null +++ b/internal/api/auth/domain/auth_domain.go @@ -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() +} diff --git a/internal/api/auth/fx.go b/internal/api/auth/fx.go new file mode 100644 index 0000000..ce87edd --- /dev/null +++ b/internal/api/auth/fx.go @@ -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) + }), +) diff --git a/internal/api/auth/handler/gen.go b/internal/api/auth/handler/gen.go index c37d853..aad6973 100644 --- a/internal/api/auth/handler/gen.go +++ b/internal/api/auth/handler/gen.go @@ -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"` diff --git a/internal/api/auth/handler/impl.go b/internal/api/auth/handler/impl.go index 09aeb45..a7195bd 100644 --- a/internal/api/auth/handler/impl.go +++ b/internal/api/auth/handler/impl.go @@ -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) - } diff --git a/internal/api/auth/repo/auth_repo.go b/internal/api/auth/repo/auth_repo.go new file mode 100644 index 0000000..a5500e2 --- /dev/null +++ b/internal/api/auth/repo/auth_repo.go @@ -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} +} diff --git a/internal/api/auth/service/auth_service.go b/internal/api/auth/service/auth_service.go new file mode 100644 index 0000000..57bff14 --- /dev/null +++ b/internal/api/auth/service/auth_service.go @@ -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, + } +} diff --git a/internal/api/user/domain/user_domain.go b/internal/api/user/domain/user_domain.go new file mode 100644 index 0000000..2cfdd03 --- /dev/null +++ b/internal/api/user/domain/user_domain.go @@ -0,0 +1,27 @@ +package domain + +import ( + "context" + + db "git.logidex.ru/fakz9/logidex-id/internal/db/generated" +) + +type User struct { + Uuid string `json:"uuid"` + PhoneNumber string `json:"phone_number"` +} + +type UserRepository interface { + GetUserByPhoneNumber(ctx context.Context, phoneNumber string) (*db.User, error) + GetUserByUuid(ctx context.Context, uuid string) (*db.User, error) + CreateUser(ctx context.Context, phoneNumber string) (*db.User, error) + VerifyUser(ctx context.Context, uuid string) (*db.User, error) +} + +type ErrUserNotFound struct { + PhoneNumber string +} + +func (e ErrUserNotFound) Error() string { + return "User not found with phone number: " + e.PhoneNumber +} diff --git a/internal/api/user/fx.go b/internal/api/user/fx.go new file mode 100644 index 0000000..dcce6b9 --- /dev/null +++ b/internal/api/user/fx.go @@ -0,0 +1,20 @@ +package user + +import ( + "git.logidex.ru/fakz9/logidex-id/internal/api/user/handler" + "git.logidex.ru/fakz9/logidex-id/internal/api/user/repo" + "git.logidex.ru/fakz9/logidex-id/internal/api/user/service" + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" +) + +var Module = fx.Options( + fx.Provide( + repo.NewUserRepo, + service.NewUserService, + handler.NewUserHandler, + ), + fx.Invoke(func(handler *handler.UserHandler, router fiber.Router) { + handler.RegisterRoutes(router) + }), +) diff --git a/internal/api/user/handler/gen.go b/internal/api/user/handler/gen.go index dff8a44..941ab67 100644 --- a/internal/api/user/handler/gen.go +++ b/internal/api/user/handler/gen.go @@ -9,52 +9,30 @@ import ( "github.com/gofiber/fiber/v2" "github.com/oapi-codegen/runtime" - openapi_types "github.com/oapi-codegen/runtime/types" ) // User defines model for User. type User struct { - Email openapi_types.Email `json:"email"` - Id string `json:"id"` - Username string `json:"username"` + PhoneNumber string `json:"phone_number"` + Uuid string `json:"uuid"` } // UserCreate defines model for UserCreate. type UserCreate struct { - Email openapi_types.Email `json:"email"` - Password string `json:"password"` - Username string `json:"username"` + PhoneNumber string `json:"phone_number"` } -// UserUpdate defines model for UserUpdate. -type UserUpdate struct { - Email openapi_types.Email `json:"email"` - Username string `json:"username"` -} - -// PostUsersJSONRequestBody defines body for PostUsers for application/json ContentType. -type PostUsersJSONRequestBody = UserCreate - -// PutUsersUserIdJSONRequestBody defines body for PutUsersUserId for application/json ContentType. -type PutUsersUserIdJSONRequestBody = UserUpdate +// CreateUserJSONRequestBody defines body for CreateUser for application/json ContentType. +type CreateUserJSONRequestBody = UserCreate // ServerInterface represents all server handlers. type ServerInterface interface { - // Get all users - // (GET /users) - GetUsers(c *fiber.Ctx) error - // Create a new user + // Create a new user with phone number // (POST /users) - PostUsers(c *fiber.Ctx) error - // Delete a user by ID - // (DELETE /users/{userId}) - DeleteUsersUserId(c *fiber.Ctx, userId string) error + CreateUser(c *fiber.Ctx) error // Get a user by ID // (GET /users/{userId}) - GetUsersUserId(c *fiber.Ctx, userId string) error - // Update a user by ID - // (PUT /users/{userId}) - PutUsersUserId(c *fiber.Ctx, userId string) error + GetUserById(c *fiber.Ctx, userId string) error } // ServerInterfaceWrapper converts contexts to parameters. @@ -64,20 +42,14 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc fiber.Handler -// GetUsers operation middleware -func (siw *ServerInterfaceWrapper) GetUsers(c *fiber.Ctx) error { +// CreateUser operation middleware +func (siw *ServerInterfaceWrapper) CreateUser(c *fiber.Ctx) error { - return siw.Handler.GetUsers(c) + return siw.Handler.CreateUser(c) } -// PostUsers operation middleware -func (siw *ServerInterfaceWrapper) PostUsers(c *fiber.Ctx) error { - - return siw.Handler.PostUsers(c) -} - -// DeleteUsersUserId operation middleware -func (siw *ServerInterfaceWrapper) DeleteUsersUserId(c *fiber.Ctx) error { +// GetUserById operation middleware +func (siw *ServerInterfaceWrapper) GetUserById(c *fiber.Ctx) error { var err error @@ -89,39 +61,7 @@ func (siw *ServerInterfaceWrapper) DeleteUsersUserId(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter userId: %w", err).Error()) } - return siw.Handler.DeleteUsersUserId(c, userId) -} - -// GetUsersUserId operation middleware -func (siw *ServerInterfaceWrapper) GetUsersUserId(c *fiber.Ctx) error { - - var err error - - // ------------- Path parameter "userId" ------------- - var userId string - - err = runtime.BindStyledParameterWithOptions("simple", "userId", c.Params("userId"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter userId: %w", err).Error()) - } - - return siw.Handler.GetUsersUserId(c, userId) -} - -// PutUsersUserId operation middleware -func (siw *ServerInterfaceWrapper) PutUsersUserId(c *fiber.Ctx) error { - - var err error - - // ------------- Path parameter "userId" ------------- - var userId string - - err = runtime.BindStyledParameterWithOptions("simple", "userId", c.Params("userId"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter userId: %w", err).Error()) - } - - return siw.Handler.PutUsersUserId(c, userId) + return siw.Handler.GetUserById(c, userId) } // FiberServerOptions provides options for the Fiber server. @@ -145,143 +85,67 @@ func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, option router.Use(fiber.Handler(m)) } - router.Get(options.BaseURL+"/users", wrapper.GetUsers) + router.Post(options.BaseURL+"/users", wrapper.CreateUser) - router.Post(options.BaseURL+"/users", wrapper.PostUsers) - - router.Delete(options.BaseURL+"/users/:userId", wrapper.DeleteUsersUserId) - - router.Get(options.BaseURL+"/users/:userId", wrapper.GetUsersUserId) - - router.Put(options.BaseURL+"/users/:userId", wrapper.PutUsersUserId) + router.Get(options.BaseURL+"/users/:userId", wrapper.GetUserById) } -type GetUsersRequestObject struct { +type CreateUserRequestObject struct { + Body *CreateUserJSONRequestBody } -type GetUsersResponseObject interface { - VisitGetUsersResponse(ctx *fiber.Ctx) error +type CreateUserResponseObject interface { + VisitCreateUserResponse(ctx *fiber.Ctx) error } -type GetUsers200JSONResponse []User +type CreateUser200JSONResponse User -func (response GetUsers200JSONResponse) VisitGetUsersResponse(ctx *fiber.Ctx) error { +func (response CreateUser200JSONResponse) VisitCreateUserResponse(ctx *fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type PostUsersRequestObject struct { - Body *PostUsersJSONRequestBody -} - -type PostUsersResponseObject interface { - VisitPostUsersResponse(ctx *fiber.Ctx) error -} - -type PostUsers201JSONResponse User - -func (response PostUsers201JSONResponse) VisitPostUsersResponse(ctx *fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(201) - - return ctx.JSON(&response) -} - -type DeleteUsersUserIdRequestObject struct { +type GetUserByIdRequestObject struct { UserId string `json:"userId"` } -type DeleteUsersUserIdResponseObject interface { - VisitDeleteUsersUserIdResponse(ctx *fiber.Ctx) error +type GetUserByIdResponseObject interface { + VisitGetUserByIdResponse(ctx *fiber.Ctx) error } -type DeleteUsersUserId204Response struct { +type GetUserById200JSONResponse struct { + User User `json:"user"` } -func (response DeleteUsersUserId204Response) VisitDeleteUsersUserIdResponse(ctx *fiber.Ctx) error { - ctx.Status(204) - return nil -} - -type DeleteUsersUserId404Response struct { -} - -func (response DeleteUsersUserId404Response) VisitDeleteUsersUserIdResponse(ctx *fiber.Ctx) error { - ctx.Status(404) - return nil -} - -type GetUsersUserIdRequestObject struct { - UserId string `json:"userId"` -} - -type GetUsersUserIdResponseObject interface { - VisitGetUsersUserIdResponse(ctx *fiber.Ctx) error -} - -type GetUsersUserId200JSONResponse User - -func (response GetUsersUserId200JSONResponse) VisitGetUsersUserIdResponse(ctx *fiber.Ctx) error { +func (response GetUserById200JSONResponse) VisitGetUserByIdResponse(ctx *fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type GetUsersUserId404Response struct { +type GetUserById404JSONResponse struct { + Message string `json:"message"` } -func (response GetUsersUserId404Response) VisitGetUsersUserIdResponse(ctx *fiber.Ctx) error { - ctx.Status(404) - return nil -} - -type PutUsersUserIdRequestObject struct { - UserId string `json:"userId"` - Body *PutUsersUserIdJSONRequestBody -} - -type PutUsersUserIdResponseObject interface { - VisitPutUsersUserIdResponse(ctx *fiber.Ctx) error -} - -type PutUsersUserId200JSONResponse User - -func (response PutUsersUserId200JSONResponse) VisitPutUsersUserIdResponse(ctx *fiber.Ctx) error { +func (response GetUserById404JSONResponse) VisitGetUserByIdResponse(ctx *fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) + ctx.Status(404) return ctx.JSON(&response) } -type PutUsersUserId404Response struct { -} - -func (response PutUsersUserId404Response) VisitPutUsersUserIdResponse(ctx *fiber.Ctx) error { - ctx.Status(404) - return nil -} - // StrictServerInterface represents all server handlers. type StrictServerInterface interface { - // Get all users - // (GET /users) - GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error) - // Create a new user + // Create a new user with phone number // (POST /users) - PostUsers(ctx context.Context, request PostUsersRequestObject) (PostUsersResponseObject, error) - // Delete a user by ID - // (DELETE /users/{userId}) - DeleteUsersUserId(ctx context.Context, request DeleteUsersUserIdRequestObject) (DeleteUsersUserIdResponseObject, error) + CreateUser(ctx context.Context, request CreateUserRequestObject) (CreateUserResponseObject, error) // Get a user by ID // (GET /users/{userId}) - GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) - // Update a user by ID - // (PUT /users/{userId}) - PutUsersUserId(ctx context.Context, request PutUsersUserIdRequestObject) (PutUsersUserIdResponseObject, error) + GetUserById(ctx context.Context, request GetUserByIdRequestObject) (GetUserByIdResponseObject, error) } type StrictHandlerFunc func(ctx *fiber.Ctx, args interface{}) (interface{}, error) @@ -297,54 +161,29 @@ type strictHandler struct { middlewares []StrictMiddlewareFunc } -// GetUsers operation middleware -func (sh *strictHandler) GetUsers(ctx *fiber.Ctx) error { - var request GetUsersRequestObject +// CreateUser operation middleware +func (sh *strictHandler) CreateUser(ctx *fiber.Ctx) error { + var request CreateUserRequestObject - handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetUsers(ctx.UserContext(), request.(GetUsersRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUsers") - } - - response, err := handler(ctx, request) - - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if validResponse, ok := response.(GetUsersResponseObject); ok { - if err := validResponse.VisitGetUsersResponse(ctx); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// PostUsers operation middleware -func (sh *strictHandler) PostUsers(ctx *fiber.Ctx) error { - var request PostUsersRequestObject - - var body PostUsersJSONRequestBody + var body CreateUserJSONRequestBody if err := ctx.BodyParser(&body); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } request.Body = &body handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.PostUsers(ctx.UserContext(), request.(PostUsersRequestObject)) + return sh.ssi.CreateUser(ctx.UserContext(), request.(CreateUserRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostUsers") + handler = middleware(handler, "CreateUser") } response, err := handler(ctx, request) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if validResponse, ok := response.(PostUsersResponseObject); ok { - if err := validResponse.VisitPostUsersResponse(ctx); err != nil { + } else if validResponse, ok := response.(CreateUserResponseObject); ok { + if err := validResponse.VisitCreateUserResponse(ctx); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } } else if response != nil { @@ -353,85 +192,25 @@ func (sh *strictHandler) PostUsers(ctx *fiber.Ctx) error { return nil } -// DeleteUsersUserId operation middleware -func (sh *strictHandler) DeleteUsersUserId(ctx *fiber.Ctx, userId string) error { - var request DeleteUsersUserIdRequestObject +// GetUserById operation middleware +func (sh *strictHandler) GetUserById(ctx *fiber.Ctx, userId string) error { + var request GetUserByIdRequestObject request.UserId = userId handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.DeleteUsersUserId(ctx.UserContext(), request.(DeleteUsersUserIdRequestObject)) + return sh.ssi.GetUserById(ctx.UserContext(), request.(GetUserByIdRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "DeleteUsersUserId") + handler = middleware(handler, "GetUserById") } response, err := handler(ctx, request) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if validResponse, ok := response.(DeleteUsersUserIdResponseObject); ok { - if err := validResponse.VisitDeleteUsersUserIdResponse(ctx); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// GetUsersUserId operation middleware -func (sh *strictHandler) GetUsersUserId(ctx *fiber.Ctx, userId string) error { - var request GetUsersUserIdRequestObject - - request.UserId = userId - - handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetUsersUserId(ctx.UserContext(), request.(GetUsersUserIdRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUsersUserId") - } - - response, err := handler(ctx, request) - - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if validResponse, ok := response.(GetUsersUserIdResponseObject); ok { - if err := validResponse.VisitGetUsersUserIdResponse(ctx); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// PutUsersUserId operation middleware -func (sh *strictHandler) PutUsersUserId(ctx *fiber.Ctx, userId string) error { - var request PutUsersUserIdRequestObject - - request.UserId = userId - - var body PutUsersUserIdJSONRequestBody - if err := ctx.BodyParser(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body - - handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.PutUsersUserId(ctx.UserContext(), request.(PutUsersUserIdRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "PutUsersUserId") - } - - response, err := handler(ctx, request) - - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if validResponse, ok := response.(PutUsersUserIdResponseObject); ok { - if err := validResponse.VisitPutUsersUserIdResponse(ctx); err != nil { + } else if validResponse, ok := response.(GetUserByIdResponseObject); ok { + if err := validResponse.VisitGetUserByIdResponse(ctx); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } } else if response != nil { diff --git a/internal/api/user/handler/generate.go b/internal/api/user/handler/generate.go index 448a883..602f2ba 100644 --- a/internal/api/user/handler/generate.go +++ b/internal/api/user/handler/generate.go @@ -1,3 +1,3 @@ package handler -//go:generate go tool oapi-codegen -config ../../../../api/user/cfg.yaml ../../../../api/user/user.yaml +//go:generate go tool oapi-codegen -config ../../../../api/user/cfg.yaml ../../../../api/user/api.yaml diff --git a/internal/api/user/handler/impl.go b/internal/api/user/handler/impl.go index a6bfd45..c69e9c5 100644 --- a/internal/api/user/handler/impl.go +++ b/internal/api/user/handler/impl.go @@ -2,46 +2,52 @@ package handler import ( "context" + + "git.logidex.ru/fakz9/logidex-id/internal/api/user/service" "github.com/gofiber/fiber/v2" + "github.com/jinzhu/copier" ) type UserHandler struct { + service service.UserService +} + +func (h UserHandler) CreateUser(ctx context.Context, request CreateUserRequestObject) (CreateUserResponseObject, error) { + //TODO implement me + panic("implement me") +} + +func (h UserHandler) GetUserById(ctx context.Context, request GetUserByIdRequestObject) (GetUserByIdResponseObject, error) { + user, err := h.service.GetUserByUuid(ctx, request.UserId) + if err != nil { + return GetUserById404JSONResponse{ + Message: err.Error(), + }, nil + } + var responseUser User + err = copier.Copy(responseUser, user) + if err != nil { + return GetUserById404JSONResponse{Message: err.Error()}, nil + + } + return GetUserById200JSONResponse{User: responseUser}, nil } var _ StrictServerInterface = (*UserHandler)(nil) -func (u UserHandler) GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error) { - var response = make([]User, 0) - - return GetUsers200JSONResponse(response), nil +func NewUserHandler( + service service.UserService, +) *UserHandler { + return &UserHandler{service: service} } -func (u UserHandler) PostUsers(ctx context.Context, request PostUsersRequestObject) (PostUsersResponseObject, error) { - //TODO implement me - panic("implement me") -} - -func (u UserHandler) DeleteUsersUserId(ctx context.Context, request DeleteUsersUserIdRequestObject) (DeleteUsersUserIdResponseObject, error) { - //TODO implement me - panic("implement me") -} - -func (u UserHandler) GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) { - //TODO implement me - panic("implement me") -} - -func (u UserHandler) PutUsersUserId(ctx context.Context, request PutUsersUserIdRequestObject) (PutUsersUserIdResponseObject, error) { - //TODO implement me - panic("implement me") -} - -func NewUserHandler() *UserHandler { - return &UserHandler{} -} - -func RegisterApp(router fiber.Router) { - server := NewStrictHandler(NewUserHandler(), nil) +func (h UserHandler) RegisterRoutes(router fiber.Router) { + server := NewStrictHandler(h, nil) RegisterHandlers(router, server) - } + +//func RegisterUserHandler(router fiber.Router) { +// server := NewStrictHandler(NewUserHandler(), nil) +// RegisterHandlers(router, server) +// +//} diff --git a/internal/api/user/repo/user_repo.go b/internal/api/user/repo/user_repo.go new file mode 100644 index 0000000..079b89d --- /dev/null +++ b/internal/api/user/repo/user_repo.go @@ -0,0 +1,70 @@ +package repo + +import ( + "context" + + "git.logidex.ru/fakz9/logidex-id/internal/api/user/domain" + db "git.logidex.ru/fakz9/logidex-id/internal/db/generated" + "github.com/google/uuid" +) + +type userRepo struct { + db db.DBTX +} + +func (u userRepo) VerifyUser(ctx context.Context, requestUuid string) (*db.User, error) { + //TODO implement me + + queries := db.New(u.db) + uuidParsed, err := uuid.Parse(requestUuid) + if err != nil { + return nil, err + } + dbUser, err := queries.UpdateUserVerified(ctx, uuidParsed) + if err != nil { + return nil, err + } + return &dbUser, nil +} + +func (u userRepo) GetUserByUuid(ctx context.Context, requestUuid string) (*db.User, error) { + queries := db.New(u.db) + uuidParsed, err := uuid.Parse(requestUuid) + if err != nil { + return nil, err + } + dbUser, err := queries.GetUserByUUID(ctx, uuidParsed) + if err != nil { + return nil, err + } + return &dbUser, nil +} + +func userFromDbToDomain(dbUser db.User) *domain.User { + return &domain.User{ + PhoneNumber: dbUser.PhoneNumber, + Uuid: dbUser.Uuid.String(), + } +} + +func (u userRepo) CreateUser(ctx context.Context, phoneNumber string) (*db.User, error) { + queries := db.New(u.db) + user, err := queries.CreateUser(ctx, phoneNumber) + if err != nil { + return nil, err + } + return &user, nil +} + +func (u userRepo) GetUserByPhoneNumber(ctx context.Context, phoneNumber string) (*db.User, error) { + queries := db.New(u.db) + dbUser, err := queries.GetByPhoneNumber(ctx, phoneNumber) + if err != nil { + return nil, err + } + return &dbUser, nil +} + +func NewUserRepo(db db.DBTX) domain.UserRepository { + return &userRepo{db: db} +} diff --git a/internal/api/user/repository/base.go b/internal/api/user/repository/base.go deleted file mode 100644 index abdfea6..0000000 --- a/internal/api/user/repository/base.go +++ /dev/null @@ -1,4 +0,0 @@ -package repository - -type UserRepo interface { -} diff --git a/internal/api/user/service/user_service.go b/internal/api/user/service/user_service.go index 2c02358..243daa7 100644 --- a/internal/api/user/service/user_service.go +++ b/internal/api/user/service/user_service.go @@ -1,13 +1,47 @@ package service import ( - "git.logidex.ru/fakz9/logidex-id/internal/api/user/repository" + "context" + + "git.logidex.ru/fakz9/logidex-id/internal/api/user/domain" ) -type UserService struct { - repo *repository.UserRepo +type UserService interface { + GetUserByPhoneNumber(ctx context.Context, phoneNumber string) (*domain.User, error) + GetUserByUuid(ctx context.Context, phoneNumber string) (*domain.User, error) +} +type userService struct { + repo domain.UserRepository } -func NewUserService(repo *repository.UserRepo) *UserService { - return &UserService{repo: repo} +func (u userService) GetUserByUuid(ctx context.Context, uuid string) (*domain.User, error) { + dbUser, err := u.repo.GetUserByUuid(ctx, uuid) + if err != nil { + return nil, err + } + if dbUser == nil { + return nil, domain.ErrUserNotFound{PhoneNumber: uuid} + } + return &domain.User{ + Uuid: dbUser.Uuid.String(), + PhoneNumber: dbUser.PhoneNumber, + }, nil +} + +func (u userService) GetUserByPhoneNumber(ctx context.Context, phoneNumber string) (*domain.User, error) { + dbUser, err := u.repo.GetUserByPhoneNumber(ctx, phoneNumber) + if err != nil { + return nil, err + } + if dbUser == nil { + return nil, domain.ErrUserNotFound{PhoneNumber: phoneNumber} + } + return &domain.User{ + Uuid: dbUser.Uuid.String(), + PhoneNumber: dbUser.PhoneNumber, + }, nil +} + +func NewUserService(repo domain.UserRepository) UserService { + return &userService{repo: repo} } diff --git a/internal/config/config.go b/internal/config/config.go index 00d155f..fca22e4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,9 +1,10 @@ package config import ( + "log" + "github.com/joho/godotenv" "github.com/spf13/viper" - "log" ) type Config struct { @@ -20,11 +21,16 @@ type Config struct { Host string Password string } + DB struct { + Host string + Port int + User string + Password string + Dbname string + } } -var Cfg *Config - -func Init() { +func NewConfig() Config { err := godotenv.Load() if err != nil { log.Println("Error loading .env file") @@ -48,5 +54,5 @@ func Init() { if err != nil { log.Fatalf("Unable to decode config into struct, %v", err) } - Cfg = &config + return config } diff --git a/internal/db/database.go b/internal/db/database.go new file mode 100644 index 0000000..6e3c97c --- /dev/null +++ b/internal/db/database.go @@ -0,0 +1,24 @@ +package db + +import ( + "context" + "strconv" + + "git.logidex.ru/fakz9/logidex-id/internal/config" + "github.com/jackc/pgx/v5/pgxpool" +) + +func NewDatabasePool(cfg config.Config) *pgxpool.Pool { + ctx := context.Background() + connUrl := "postgresql://" + cfg.DB.User + ":" + cfg.DB.Password + "@" + cfg.DB.Host + ":" + strconv.Itoa(cfg.DB.Port) + "/" + cfg.DB.Dbname + + pool, err := pgxpool.New(ctx, connUrl) + if err != nil { + panic("Failed to connect to database: " + err.Error()) + } + err = pool.Ping(ctx) + if err != nil { + panic("Failed to ping database: " + err.Error()) + } + return pool +} diff --git a/internal/db/fx.go b/internal/db/fx.go new file mode 100644 index 0000000..76f7ffd --- /dev/null +++ b/internal/db/fx.go @@ -0,0 +1,15 @@ +package db + +import ( + sqlcdb "git.logidex.ru/fakz9/logidex-id/internal/db/generated" + "go.uber.org/fx" +) + +var Module = fx.Options( + fx.Provide( + fx.Annotate( + NewDatabasePool, + fx.As(new(sqlcdb.DBTX)), + ), + ), +) diff --git a/internal/db/migrations/001_init.sql b/internal/db/migrations/001_init.sql new file mode 100644 index 0000000..ff55d76 --- /dev/null +++ b/internal/db/migrations/001_init.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS users +( + uuid UUID PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + phone_number VARCHAR(20) NOT NULL CHECK (phone_number ~ '^\+[0-9]{10,15}$'), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + verified BOOLEAN DEFAULT FALSE NOT NULL, + verified_at TIMESTAMP NULL + +); \ No newline at end of file diff --git a/internal/db/queries/users.sql b/internal/db/queries/users.sql new file mode 100644 index 0000000..641d512 --- /dev/null +++ b/internal/db/queries/users.sql @@ -0,0 +1,23 @@ +-- name: GetByPhoneNumber :one +SELECT * +FROM users +WHERE phone_number = $1 +LIMIT 1; + +-- name: GetUserByUUID :one +SELECT * +FROM users +WHERE uuid = $1 +LIMIT 1; + +-- name: CreateUser :one +INSERT INTO users (phone_number) +VALUES ($1) +RETURNING *; + +-- name: UpdateUserVerified :one +UPDATE users +SET verified = TRUE, + verified_at = CURRENT_TIMESTAMP +WHERE uuid = $1 +RETURNING *; diff --git a/internal/hydra_client/hydra_client.go b/internal/hydra_client/hydra_client.go index 09253ad..f6f3aa8 100644 --- a/internal/hydra_client/hydra_client.go +++ b/internal/hydra_client/hydra_client.go @@ -3,26 +3,15 @@ package hydra_client import ( "git.logidex.ru/fakz9/logidex-id/internal/config" hydraApi "github.com/ory/hydra-client-go" - "sync" ) -var ( - client *hydraApi.APIClient - initClient sync.Once -) - -func InitClient() { +func NewHydraClient(appConfig config.Config) *hydraApi.APIClient { cfg := hydraApi.NewConfiguration() - cfg.AddDefaultHeader("X-Secret", config.Cfg.Hydra.Password) + cfg.AddDefaultHeader("X-Secret", appConfig.Hydra.Password) cfg.Servers = []hydraApi.ServerConfiguration{ { - URL: config.Cfg.Hydra.Host, + URL: appConfig.Hydra.Host, }, } - client = hydraApi.NewAPIClient(cfg) -} - -func GetClient() *hydraApi.APIClient { - initClient.Do(InitClient) - return client + return hydraApi.NewAPIClient(cfg) } diff --git a/internal/phoneutil/phoneutil.go b/internal/phoneutil/phoneutil.go new file mode 100644 index 0000000..1e3618b --- /dev/null +++ b/internal/phoneutil/phoneutil.go @@ -0,0 +1,19 @@ +package phoneutil + +import ( + "errors" + + "github.com/nyaruka/phonenumbers" +) + +func ParseAndFormatPhoneNumber(phoneNumber string) (string, error) { + parsedNumber, err := phonenumbers.Parse(phoneNumber, "RU") + if err != nil { + return "", err + } + result := phonenumbers.Format(parsedNumber, phonenumbers.E164) + if result == "" { + return "", errors.New("failed to format phone number") + } + return result, nil +} diff --git a/internal/redis/client.go b/internal/redis/client.go index dc0f75a..b8f16ec 100644 --- a/internal/redis/client.go +++ b/internal/redis/client.go @@ -1,26 +1,21 @@ package redis import ( + "strconv" + "git.logidex.ru/fakz9/logidex-id/internal/config" "github.com/redis/rueidis" - "strconv" ) -var client rueidis.Client - -func Init() error { +func NewRedisClient(cfg config.Config) rueidis.Client { var err error - client, err = rueidis.NewClient(rueidis.ClientOption{ + client, err := rueidis.NewClient(rueidis.ClientOption{ // Set the address of your Redis server - InitAddress: []string{config.Cfg.Redis.Host + ":" + strconv.Itoa(config.Cfg.Redis.Port)}, - Password: config.Cfg.Redis.Password, + InitAddress: []string{cfg.Redis.Host + ":" + strconv.Itoa(cfg.Redis.Port)}, + Password: cfg.Redis.Password, }) if err != nil { - return err + return nil } - return nil -} - -func GetClient() rueidis.Client { return client } diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..3684a7e --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,35 @@ +version: "2" +sql: + - engine: postgresql + schema: "internal/db/migrations" + queries: "internal/db/queries" + gen: + go: + package: "db" + out: "internal/db/generated" + sql_package: "pgx/v5" + emit_interface: true + emit_json_tags: true + emit_pointers_for_null_types: true + overrides: + # Timestamp + - db_type: "pg_catalog.timestamp" + nullable: true + go_type: + import: "time" + type: "Time" + pointer: true + - db_type: "pg_catalog.timestamp" + go_type: "time.Time" + # UUID + - db_type: "uuid" + go_type: + import: "github.com/google/uuid" + type: "UUID" + - db_type: "uuid" + nullable: true + go_type: + import: "github.com/google/uuid" + type: "UUID" + pointer: true +