add user management functionality with OTP verification and consent handling, DI introduced

This commit is contained in:
2025-08-10 10:38:49 +03:00
parent 6a9061a3de
commit 5d80a68b44
30 changed files with 828 additions and 528 deletions

View File

@ -137,9 +137,14 @@ components:
type: boolean type: boolean
description: Status of the verification description: Status of the verification
example: true example: true
message:
type: string
description: Confirmation message
example: "OTP verified successfully"
required: required:
- redirect_url - redirect_url
- ok - ok
- message
AcceptConsentRequest: AcceptConsentRequest:
type: object type: object
properties: properties:
@ -147,8 +152,14 @@ components:
type: string type: string
description: The consent challenge to accept description: The consent challenge to accept
example: "challenge123" example: "challenge123"
phone_number:
type: string
description: Phone number associated with the consent
example: "+79999999999"
maxLength: 15
required: required:
- consent_challenge - consent_challenge
- phone_number
AcceptConsentResponse: AcceptConsentResponse:
type: object type: object
properties: properties:

View File

@ -5,35 +5,11 @@ info:
servers: servers:
- url: https://api.example.com/v1 - url: https://api.example.com/v1
paths: 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}: /users/{userId}:
get: get:
tags:
- users
operationId: getUserById
summary: Get a user by ID summary: Get a user by ID
parameters: parameters:
- name: userId - name: userId
@ -47,92 +23,65 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: object
properties:
user:
$ref: '#/components/schemas/User' $ref: '#/components/schemas/User'
required:
- user
'404': '404':
description: User not found description: User not found
put: content:
summary: Update a user by ID application/json:
parameters:
- name: userId
in: path
required: true
schema: schema:
type: object
properties:
message:
type: string type: string
example: "User not found"
required:
- message
/users:
post:
tags:
- users
operationId: createUser
summary: "Create a new user with phone number"
requestBody: requestBody:
required: true required: true
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/UserUpdate' $ref: '#/components/schemas/UserCreate'
responses: responses:
'200': '200':
description: User updated description: User created successfully
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/User' $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: components:
schemas: schemas:
User: User:
type: object type: object
properties: properties:
id: uuid:
type: string type: string
example: "123" example: "123"
username: phone_number:
type: string type: string
example: "johndoe" example: "johndoe"
email:
type: string
format: email
example: "johndoe@example.com"
required: required:
- id - uuid
- username - phone_number
- email
UserCreate: UserCreate:
type: object type: object
properties: properties:
username: phone_number:
type: string type: string
example: "johndoe"
email:
type: string
format: email
example: "johndoe@example.com"
password:
type: string
format: password
required: required:
- username - phone_number
- 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

View File

@ -1,28 +1,57 @@
package main package main
import ( 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/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/hydra_client"
"git.logidex.ru/fakz9/logidex-id/internal/redis" "git.logidex.ru/fakz9/logidex-id/internal/redis"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"strconv" "go.uber.org/fx"
) )
func main() { func NewFiberApp(cfg config.Config) *fiber.App {
config.Init()
err := redis.Init()
if err != nil {
panic(err)
}
hydra_client.InitClient()
app := fiber.New() app := fiber.New()
api := app.Group("/api") return app
authApi.RegisterApp(api) }
err = app.Listen(":" + strconv.Itoa(config.Cfg.App.Port)) func StartFiberApp(lifecycle fx.Lifecycle, app *fiber.App, cfg config.Config) {
if err != nil { lifecycle.Append(fx.Hook{
panic(err) 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()
} }

1
combine.sh Executable file
View File

@ -0,0 +1 @@
swagger-cli bundle openapi.yaml --outfile combined.yaml --type yaml

View File

@ -8,3 +8,10 @@ redis:
hydra: hydra:
host: https://oauth2.logidex.ru/admin host: https://oauth2.logidex.ru/admin
db:
host: localhost
port: 5432
user: postgres
password: postgres
dbname: logidex-id

4
go.mod
View File

@ -27,6 +27,7 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/puddle/v2 v2.2.2 // 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/jinzhu/inflection v1.0.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect github.com/joho/godotenv v1.5.1 // indirect
github.com/josharian/intern v1.0.0 // 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/mattn/go-runewidth v0.0.16 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/ncruces/go-strftime v0.1.9 // 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/oapi-codegen/v2 v2.5.0 // indirect
github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/oapi-codegen/runtime v1.1.2 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // 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/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
go.uber.org/atomic v1.11.0 // 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/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.40.0 // indirect golang.org/x/crypto v0.40.0 // indirect

8
go.sum
View File

@ -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/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 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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= 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/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.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 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 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU=
github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w= 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= 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.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 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 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/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.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=

View 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
View 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)
}),
)

View File

@ -14,6 +14,9 @@ import (
type AcceptConsentRequest struct { type AcceptConsentRequest struct {
// ConsentChallenge The consent challenge to accept // ConsentChallenge The consent challenge to accept
ConsentChallenge string `json:"consent_challenge"` ConsentChallenge string `json:"consent_challenge"`
// PhoneNumber Phone number associated with the consent
PhoneNumber string `json:"phone_number"`
} }
// AcceptConsentResponse defines model for AcceptConsentResponse. // AcceptConsentResponse defines model for AcceptConsentResponse.
@ -56,6 +59,9 @@ type VerifyOTPRequest struct {
// VerifyOTPResponse defines model for VerifyOTPResponse. // VerifyOTPResponse defines model for VerifyOTPResponse.
type VerifyOTPResponse struct { type VerifyOTPResponse struct {
// Message Confirmation message
Message string `json:"message"`
// Ok Status of the verification // Ok Status of the verification
Ok bool `json:"ok"` Ok bool `json:"ok"`

View File

@ -2,113 +2,68 @@ package handler
import ( import (
"context" "context"
"fmt"
"git.logidex.ru/fakz9/logidex-id/internal/hydra_client" "git.logidex.ru/fakz9/logidex-id/internal/api/auth/service"
"git.logidex.ru/fakz9/logidex-id/internal/redis"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
hydraApi "github.com/ory/hydra-client-go"
) )
type AuthHandler struct{} type AuthHandler struct {
service service.AuthService
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
} }
func (a AuthHandler) PostAuthOtpRequest(ctx context.Context, request PostAuthOtpRequestRequestObject) (PostAuthOtpRequestResponseObject, error) { func (h AuthHandler) PostAuthOtpRequest(ctx context.Context, request PostAuthOtpRequestRequestObject) (PostAuthOtpRequestResponseObject, error) {
redisClient := redis.GetClient() err := h.service.OtpRequest(ctx, request.Body.PhoneNumber)
// TODO implement OTP request logic
err := redisClient.Do(ctx, redisClient.B().Set().Key("otp:"+request.Body.PhoneNumber).Value("123456").Build()).Error()
if err != nil { if err != nil {
return PostAuthOtpRequest400JSONResponse{ return PostAuthOtpRequest400JSONResponse{
Message: "Failed to set OTP in Redis", Message: err.Error(),
Ok: false, Ok: false,
}, nil }, nil
} }
return PostAuthOtpRequest200JSONResponse{ return PostAuthOtpRequest200JSONResponse{
Message: "Код успешно отправлен", Message: "OTP request successful",
Ok: true, Ok: true,
}, nil }, nil
} }
func (a AuthHandler) PostAuthOtpVerify(ctx context.Context, request PostAuthOtpVerifyRequestObject) (PostAuthOtpVerifyResponseObject, error) { func (h AuthHandler) PostAuthOtpVerify(ctx context.Context, request PostAuthOtpVerifyRequestObject) (PostAuthOtpVerifyResponseObject, error) {
redisClient := redis.GetClient() redirectUrl, err := h.service.OtpVerify(ctx, request.Body.PhoneNumber, request.Body.Otp, request.Body.LoginChallenge)
hydraClient := hydra_client.GetClient()
sentOtp, err := redisClient.Do(ctx, redisClient.B().Get().Key("otp:"+request.Body.PhoneNumber).Build()).ToString()
if err != nil { if err != nil {
return PostAuthOtpVerify400JSONResponse{ return PostAuthOtpVerify400JSONResponse{
RedirectUrl: "", Message: err.Error(),
Ok: false, Ok: false,
}, nil
}
if sentOtp != request.Body.Otp {
return PostAuthOtpVerify400JSONResponse{
RedirectUrl: "", 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 }, nil
} }
return PostAuthOtpVerify200JSONResponse{ return PostAuthOtpVerify200JSONResponse{
RedirectUrl: hydraResponse.RedirectTo, Message: "OTP verification successful",
Ok: true, 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 }, nil
} }
var _ StrictServerInterface = (*AuthHandler)(nil) var _ StrictServerInterface = (*AuthHandler)(nil)
func NewAuthHandler() *AuthHandler { func NewAuthHandler(service service.AuthService) *AuthHandler {
return &AuthHandler{} return &AuthHandler{service: service}
} }
func RegisterApp(router fiber.Router) { func (h AuthHandler) RegisterRoutes(router fiber.Router) {
//authGroup := router.Group("/auth") server := NewStrictHandler(h, nil)
server := NewStrictHandler(NewAuthHandler(), nil)
RegisterHandlers(router, server) RegisterHandlers(router, server)
} }

View 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}
}

View 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,
}
}

View File

@ -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
}

20
internal/api/user/fx.go Normal file
View File

@ -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)
}),
)

View File

@ -9,52 +9,30 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/oapi-codegen/runtime" "github.com/oapi-codegen/runtime"
openapi_types "github.com/oapi-codegen/runtime/types"
) )
// User defines model for User. // User defines model for User.
type User struct { type User struct {
Email openapi_types.Email `json:"email"` PhoneNumber string `json:"phone_number"`
Id string `json:"id"` Uuid string `json:"uuid"`
Username string `json:"username"`
} }
// UserCreate defines model for UserCreate. // UserCreate defines model for UserCreate.
type UserCreate struct { type UserCreate struct {
Email openapi_types.Email `json:"email"` PhoneNumber string `json:"phone_number"`
Password string `json:"password"`
Username string `json:"username"`
} }
// UserUpdate defines model for UserUpdate. // CreateUserJSONRequestBody defines body for CreateUser for application/json ContentType.
type UserUpdate struct { type CreateUserJSONRequestBody = UserCreate
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
// ServerInterface represents all server handlers. // ServerInterface represents all server handlers.
type ServerInterface interface { type ServerInterface interface {
// Get all users // Create a new user with phone number
// (GET /users)
GetUsers(c *fiber.Ctx) error
// Create a new user
// (POST /users) // (POST /users)
PostUsers(c *fiber.Ctx) error CreateUser(c *fiber.Ctx) error
// Delete a user by ID
// (DELETE /users/{userId})
DeleteUsersUserId(c *fiber.Ctx, userId string) error
// Get a user by ID // Get a user by ID
// (GET /users/{userId}) // (GET /users/{userId})
GetUsersUserId(c *fiber.Ctx, userId string) error GetUserById(c *fiber.Ctx, userId string) error
// Update a user by ID
// (PUT /users/{userId})
PutUsersUserId(c *fiber.Ctx, userId string) error
} }
// ServerInterfaceWrapper converts contexts to parameters. // ServerInterfaceWrapper converts contexts to parameters.
@ -64,20 +42,14 @@ type ServerInterfaceWrapper struct {
type MiddlewareFunc fiber.Handler type MiddlewareFunc fiber.Handler
// GetUsers operation middleware // CreateUser operation middleware
func (siw *ServerInterfaceWrapper) GetUsers(c *fiber.Ctx) error { func (siw *ServerInterfaceWrapper) CreateUser(c *fiber.Ctx) error {
return siw.Handler.GetUsers(c) return siw.Handler.CreateUser(c)
} }
// PostUsers operation middleware // GetUserById operation middleware
func (siw *ServerInterfaceWrapper) PostUsers(c *fiber.Ctx) error { func (siw *ServerInterfaceWrapper) GetUserById(c *fiber.Ctx) error {
return siw.Handler.PostUsers(c)
}
// DeleteUsersUserId operation middleware
func (siw *ServerInterfaceWrapper) DeleteUsersUserId(c *fiber.Ctx) error {
var err 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 fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter userId: %w", err).Error())
} }
return siw.Handler.DeleteUsersUserId(c, userId) return siw.Handler.GetUserById(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)
} }
// FiberServerOptions provides options for the Fiber server. // 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.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.Get(options.BaseURL+"/users/:userId", wrapper.GetUserById)
router.Delete(options.BaseURL+"/users/:userId", wrapper.DeleteUsersUserId)
router.Get(options.BaseURL+"/users/:userId", wrapper.GetUsersUserId)
router.Put(options.BaseURL+"/users/:userId", wrapper.PutUsersUserId)
} }
type GetUsersRequestObject struct { type CreateUserRequestObject struct {
Body *CreateUserJSONRequestBody
} }
type GetUsersResponseObject interface { type CreateUserResponseObject interface {
VisitGetUsersResponse(ctx *fiber.Ctx) error 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.Response().Header.Set("Content-Type", "application/json")
ctx.Status(200) ctx.Status(200)
return ctx.JSON(&response) return ctx.JSON(&response)
} }
type PostUsersRequestObject struct { type GetUserByIdRequestObject 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 {
UserId string `json:"userId"` UserId string `json:"userId"`
} }
type DeleteUsersUserIdResponseObject interface { type GetUserByIdResponseObject interface {
VisitDeleteUsersUserIdResponse(ctx *fiber.Ctx) error VisitGetUserByIdResponse(ctx *fiber.Ctx) error
} }
type DeleteUsersUserId204Response struct { type GetUserById200JSONResponse struct {
User User `json:"user"`
} }
func (response DeleteUsersUserId204Response) VisitDeleteUsersUserIdResponse(ctx *fiber.Ctx) error { func (response GetUserById200JSONResponse) VisitGetUserByIdResponse(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 {
ctx.Response().Header.Set("Content-Type", "application/json") ctx.Response().Header.Set("Content-Type", "application/json")
ctx.Status(200) ctx.Status(200)
return ctx.JSON(&response) return ctx.JSON(&response)
} }
type GetUsersUserId404Response struct { type GetUserById404JSONResponse struct {
Message string `json:"message"`
} }
func (response GetUsersUserId404Response) VisitGetUsersUserIdResponse(ctx *fiber.Ctx) error { func (response GetUserById404JSONResponse) VisitGetUserByIdResponse(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 {
ctx.Response().Header.Set("Content-Type", "application/json") ctx.Response().Header.Set("Content-Type", "application/json")
ctx.Status(200) ctx.Status(404)
return ctx.JSON(&response) 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. // StrictServerInterface represents all server handlers.
type StrictServerInterface interface { type StrictServerInterface interface {
// Get all users // Create a new user with phone number
// (GET /users)
GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error)
// Create a new user
// (POST /users) // (POST /users)
PostUsers(ctx context.Context, request PostUsersRequestObject) (PostUsersResponseObject, error) CreateUser(ctx context.Context, request CreateUserRequestObject) (CreateUserResponseObject, error)
// Delete a user by ID
// (DELETE /users/{userId})
DeleteUsersUserId(ctx context.Context, request DeleteUsersUserIdRequestObject) (DeleteUsersUserIdResponseObject, error)
// Get a user by ID // Get a user by ID
// (GET /users/{userId}) // (GET /users/{userId})
GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) GetUserById(ctx context.Context, request GetUserByIdRequestObject) (GetUserByIdResponseObject, error)
// Update a user by ID
// (PUT /users/{userId})
PutUsersUserId(ctx context.Context, request PutUsersUserIdRequestObject) (PutUsersUserIdResponseObject, error)
} }
type StrictHandlerFunc func(ctx *fiber.Ctx, args interface{}) (interface{}, error) type StrictHandlerFunc func(ctx *fiber.Ctx, args interface{}) (interface{}, error)
@ -297,54 +161,29 @@ type strictHandler struct {
middlewares []StrictMiddlewareFunc middlewares []StrictMiddlewareFunc
} }
// GetUsers operation middleware // CreateUser operation middleware
func (sh *strictHandler) GetUsers(ctx *fiber.Ctx) error { func (sh *strictHandler) CreateUser(ctx *fiber.Ctx) error {
var request GetUsersRequestObject var request CreateUserRequestObject
handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { var body CreateUserJSONRequestBody
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
if err := ctx.BodyParser(&body); err != nil { if err := ctx.BodyParser(&body); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} }
request.Body = &body request.Body = &body
handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { 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 { for _, middleware := range sh.middlewares {
handler = middleware(handler, "PostUsers") handler = middleware(handler, "CreateUser")
} }
response, err := handler(ctx, request) response, err := handler(ctx, request)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if validResponse, ok := response.(PostUsersResponseObject); ok { } else if validResponse, ok := response.(CreateUserResponseObject); ok {
if err := validResponse.VisitPostUsersResponse(ctx); err != nil { if err := validResponse.VisitCreateUserResponse(ctx); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} }
} else if response != nil { } else if response != nil {
@ -353,85 +192,25 @@ func (sh *strictHandler) PostUsers(ctx *fiber.Ctx) error {
return nil return nil
} }
// DeleteUsersUserId operation middleware // GetUserById operation middleware
func (sh *strictHandler) DeleteUsersUserId(ctx *fiber.Ctx, userId string) error { func (sh *strictHandler) GetUserById(ctx *fiber.Ctx, userId string) error {
var request DeleteUsersUserIdRequestObject var request GetUserByIdRequestObject
request.UserId = userId request.UserId = userId
handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { 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 { for _, middleware := range sh.middlewares {
handler = middleware(handler, "DeleteUsersUserId") handler = middleware(handler, "GetUserById")
} }
response, err := handler(ctx, request) response, err := handler(ctx, request)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if validResponse, ok := response.(DeleteUsersUserIdResponseObject); ok { } else if validResponse, ok := response.(GetUserByIdResponseObject); ok {
if err := validResponse.VisitDeleteUsersUserIdResponse(ctx); err != nil { if err := validResponse.VisitGetUserByIdResponse(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 {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} }
} else if response != nil { } else if response != nil {

View File

@ -1,3 +1,3 @@
package handler 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

View File

@ -2,46 +2,52 @@ package handler
import ( import (
"context" "context"
"git.logidex.ru/fakz9/logidex-id/internal/api/user/service"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/jinzhu/copier"
) )
type UserHandler struct { 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) var _ StrictServerInterface = (*UserHandler)(nil)
func (u UserHandler) GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error) { func NewUserHandler(
var response = make([]User, 0) service service.UserService,
) *UserHandler {
return GetUsers200JSONResponse(response), nil return &UserHandler{service: service}
} }
func (u UserHandler) PostUsers(ctx context.Context, request PostUsersRequestObject) (PostUsersResponseObject, error) { func (h UserHandler) RegisterRoutes(router fiber.Router) {
//TODO implement me server := NewStrictHandler(h, nil)
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)
RegisterHandlers(router, server) RegisterHandlers(router, server)
} }
//func RegisterUserHandler(router fiber.Router) {
// server := NewStrictHandler(NewUserHandler(), nil)
// RegisterHandlers(router, server)
//
//}

View File

@ -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}
}

View File

@ -1,4 +0,0 @@
package repository
type UserRepo interface {
}

View File

@ -1,13 +1,47 @@
package service package service
import ( 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 { type UserService interface {
repo *repository.UserRepo 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 { func (u userService) GetUserByUuid(ctx context.Context, uuid string) (*domain.User, error) {
return &UserService{repo: repo} 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}
} }

View File

@ -1,9 +1,10 @@
package config package config
import ( import (
"log"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/spf13/viper" "github.com/spf13/viper"
"log"
) )
type Config struct { type Config struct {
@ -20,11 +21,16 @@ type Config struct {
Host string Host string
Password string Password string
} }
DB struct {
Host string
Port int
User string
Password string
Dbname string
}
} }
var Cfg *Config func NewConfig() Config {
func Init() {
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
log.Println("Error loading .env file") log.Println("Error loading .env file")
@ -48,5 +54,5 @@ func Init() {
if err != nil { if err != nil {
log.Fatalf("Unable to decode config into struct, %v", err) log.Fatalf("Unable to decode config into struct, %v", err)
} }
Cfg = &config return config
} }

24
internal/db/database.go Normal file
View File

@ -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
}

15
internal/db/fx.go Normal file
View File

@ -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)),
),
),
)

View File

@ -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
);

View File

@ -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 *;

View File

@ -3,26 +3,15 @@ package hydra_client
import ( import (
"git.logidex.ru/fakz9/logidex-id/internal/config" "git.logidex.ru/fakz9/logidex-id/internal/config"
hydraApi "github.com/ory/hydra-client-go" hydraApi "github.com/ory/hydra-client-go"
"sync"
) )
var ( func NewHydraClient(appConfig config.Config) *hydraApi.APIClient {
client *hydraApi.APIClient
initClient sync.Once
)
func InitClient() {
cfg := hydraApi.NewConfiguration() cfg := hydraApi.NewConfiguration()
cfg.AddDefaultHeader("X-Secret", config.Cfg.Hydra.Password) cfg.AddDefaultHeader("X-Secret", appConfig.Hydra.Password)
cfg.Servers = []hydraApi.ServerConfiguration{ cfg.Servers = []hydraApi.ServerConfiguration{
{ {
URL: config.Cfg.Hydra.Host, URL: appConfig.Hydra.Host,
}, },
} }
client = hydraApi.NewAPIClient(cfg) return hydraApi.NewAPIClient(cfg)
}
func GetClient() *hydraApi.APIClient {
initClient.Do(InitClient)
return client
} }

View File

@ -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
}

View File

@ -1,26 +1,21 @@
package redis package redis
import ( import (
"strconv"
"git.logidex.ru/fakz9/logidex-id/internal/config" "git.logidex.ru/fakz9/logidex-id/internal/config"
"github.com/redis/rueidis" "github.com/redis/rueidis"
"strconv"
) )
var client rueidis.Client func NewRedisClient(cfg config.Config) rueidis.Client {
func Init() error {
var err error var err error
client, err = rueidis.NewClient(rueidis.ClientOption{ client, err := rueidis.NewClient(rueidis.ClientOption{
// Set the address of your Redis server // Set the address of your Redis server
InitAddress: []string{config.Cfg.Redis.Host + ":" + strconv.Itoa(config.Cfg.Redis.Port)}, InitAddress: []string{cfg.Redis.Host + ":" + strconv.Itoa(cfg.Redis.Port)},
Password: config.Cfg.Redis.Password, Password: cfg.Redis.Password,
}) })
if err != nil { if err != nil {
return err
}
return nil return nil
} }
func GetClient() rueidis.Client {
return client return client
} }

35
sqlc.yaml Normal file
View File

@ -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