Compare commits
4 Commits
344485d082
...
cb7a4bf5c5
Author | SHA1 | Date | |
---|---|---|---|
|
cb7a4bf5c5 | ||
|
c1173b4bcc | ||
|
b7697bc89b | ||
|
ba8570857d |
69
README.md
69
README.md
@ -13,6 +13,9 @@
|
|||||||
- [Version](#version)
|
- [Version](#version)
|
||||||
- [2024/10/03](#20241003)
|
- [2024/10/03](#20241003)
|
||||||
- [2024/10/04](#20241004)
|
- [2024/10/04](#20241004)
|
||||||
|
- [2024/10/06](#20241006)
|
||||||
|
- [Workflow](#workflow)
|
||||||
|
- [2024/10/07](#20241007)
|
||||||
<!--toc:end-->
|
<!--toc:end-->
|
||||||
|
|
||||||
A tricount like expense-sharing system written in Go
|
A tricount like expense-sharing system written in Go
|
||||||
@ -241,3 +244,69 @@ rest, I just put unit tests aside for later.
|
|||||||
That should be the correct workflow. But to save time, I will cut off the
|
That should be the correct workflow. But to save time, I will cut off the
|
||||||
integration test part (the 2nd point).
|
integration test part (the 2nd point).
|
||||||
|
|
||||||
|
### 2024/10/07
|
||||||
|
|
||||||
|
I rethought about the whole API design (even though I have only one yet). I
|
||||||
|
have created `/signup` and `/login` without thinking too much, but in fact
|
||||||
|
it is not quite *RESTful*.
|
||||||
|
|
||||||
|
**REST** is all about resources. While `/signup` and `/login` is quite
|
||||||
|
comprehensible, thus service-oriented, they don't follow the REST philosophy,
|
||||||
|
that is to say, **resource-oriented**.
|
||||||
|
|
||||||
|
If we rethink about `/signup`, what it does is to create a resource of `User`.
|
||||||
|
Thus, for a backend API, it'd better be named as `User.Create`. But what
|
||||||
|
about `/login`, it doesn't do anything about `User`. It would be strange to
|
||||||
|
declare it as a User-relevant method.
|
||||||
|
|
||||||
|
Instead, what `/login` really does, is to **create a session**.
|
||||||
|
In consequence, we have to create a new struct `Session` that can be created,
|
||||||
|
deleted, or updated.
|
||||||
|
|
||||||
|
It might seem overkill, and in real life, even in the official Pet store
|
||||||
|
example of OpenAPI, signup and login are under /user. But it just opened my
|
||||||
|
mind and forces me to **think and design RESTfully**!
|
||||||
|
|
||||||
|
That being said, for the user side, we shall still have `/signup` and `/login`,
|
||||||
|
because on the Front-end, we must be user-centered. We can even make this
|
||||||
|
2 functions on the same page with the same endpoint `/login`. The user enter
|
||||||
|
the email and the password, then clicks on `Login or Signup`. If the login
|
||||||
|
is successful, then he is logged in. Otherwise, if the user doesn't exist
|
||||||
|
yet, we open up 2 more inputs (first name and last name) for signup. They
|
||||||
|
can just provide the extra information and click again on `Signup`.
|
||||||
|
|
||||||
|
That, again, being said, I am thinking about doing some Front-end stuff just
|
||||||
|
to make the validation tests of the product simpler.
|
||||||
|
|
||||||
|
#### The choice of the front end framework
|
||||||
|
|
||||||
|
I have considered several choices.
|
||||||
|
|
||||||
|
If I didn't purposely make the backend code to provide a REST API, I might
|
||||||
|
choose server-side-rendering with `templ + htmx`, or even `template+vanilla
|
||||||
|
javascript`.
|
||||||
|
|
||||||
|
I can still write a rather static Go-frontend-server to serve HTMLs and call
|
||||||
|
my Go backend. *And it might be a good idea if they communicate on Go native
|
||||||
|
rpc.* It worth a try.
|
||||||
|
|
||||||
|
And I have moved on to `Svelte` which seems very simple by design and the
|
||||||
|
whole compile thing makes it really charm. But this is mainly a Go project,
|
||||||
|
to learn something new with a rather small community means potentially more
|
||||||
|
investment. I can learn it later.
|
||||||
|
|
||||||
|
Among `Angular`, `React` and `Vue`, I prefer `Vue`, for several reasons.
|
||||||
|
First, `Angular` is clearly overkill for this small demo project. Second,
|
||||||
|
`React` is good but I personally like the way of Vue doing things. And I
|
||||||
|
work with Vue at work, so I might have more technical help from my colleagues.
|
||||||
|
|
||||||
|
So the plan for this week is to have both the Front end part and Backend part
|
||||||
|
working, just for user signup and login.
|
||||||
|
|
||||||
|
I would like to directly put this stuff on a CI-pipeline for tests and
|
||||||
|
deployment, even I have barely nothing yet. It is always good to do this
|
||||||
|
preparation stuff at the early stage of the project. So we can benefit from
|
||||||
|
them all the way along.
|
||||||
|
|
||||||
|
Moreover, even I am not really finishing the project, it can still be
|
||||||
|
something representable that I can show to a future interviewer.
|
||||||
|
@ -37,28 +37,79 @@ servers:
|
|||||||
- url: https:/localhost:8000/v1
|
- url: https:/localhost:8000/v1
|
||||||
tags:
|
tags:
|
||||||
- name: user
|
- name: user
|
||||||
|
- name: session
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/user/signup:
|
/user/create:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- user
|
- user
|
||||||
description: Sign up as a new user
|
description: Create a new user
|
||||||
requestBody:
|
requestBody:
|
||||||
description: Sign up
|
description: Create a new user
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UserSignUpRequest'
|
$ref: '#/components/schemas/UserCreateRequest'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful operation
|
description: Successful operation
|
||||||
'400':
|
'400':
|
||||||
description: Client side error
|
description: Client side error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
example: FailedOperation.UserExisted
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: "Email already existed."
|
||||||
|
'500':
|
||||||
|
description: Server side error
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ErrResponse'
|
$ref: '#/components/schemas/ErrResponse'
|
||||||
|
/session/create:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
description: Create a new session for a user
|
||||||
|
requestBody:
|
||||||
|
description: Create session
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SessionCreateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
headers:
|
||||||
|
X-Expires-After:
|
||||||
|
description: date in UTC when token expires
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
'400':
|
||||||
|
description: Client side error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
example: AuthFailure
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: "wrong email password."
|
||||||
'500':
|
'500':
|
||||||
description: Server side error
|
description: Server side error
|
||||||
content:
|
content:
|
||||||
@ -68,7 +119,7 @@ paths:
|
|||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
UserSignUpRequest:
|
UserCreateRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
@ -88,12 +139,24 @@ components:
|
|||||||
- fist_name
|
- fist_name
|
||||||
- last_name
|
- last_name
|
||||||
- password
|
- password
|
||||||
|
SessionCreateRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
example: bruce@wayne.com
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
example: verystrongpassword
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
- password
|
||||||
ErrResponse:
|
ErrResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
type: string
|
type: string
|
||||||
example: FailedOperation.UserAlreadyExists
|
example: InternalError
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
example: "User already exists."
|
example: "Server internal error."
|
||||||
|
4
go.mod
4
go.mod
@ -3,17 +3,20 @@ module git.vinchent.xyz/vinchent/howmuch
|
|||||||
go 1.23.1
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
github.com/gin-contrib/cors v1.7.2
|
github.com/gin-contrib/cors v1.7.2
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gosuri/uitable v0.0.4
|
github.com/gosuri/uitable v0.0.4
|
||||||
github.com/jackc/pgx/v5 v5.7.1
|
github.com/jackc/pgx/v5 v5.7.1
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/crypto v0.27.0
|
||||||
golang.org/x/net v0.25.0
|
golang.org/x/net v0.25.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -58,7 +61,6 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/crypto v0.27.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.18.0 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -1,3 +1,5 @@
|
|||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
@ -84,6 +86,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
@ -29,16 +29,15 @@ import (
|
|||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User is the user controller interface, it describes all the handlers
|
// User is the user controller interface, it describes all the handlers
|
||||||
// that need to be implemented for the /user endpoint
|
// that need to be implemented for the /user endpoint
|
||||||
type User interface {
|
type User interface {
|
||||||
Signup(core.Context)
|
Create(core.Context)
|
||||||
UpdateInfo(*gin.Context)
|
UpdateInfo(*gin.Context)
|
||||||
Login(*gin.Context)
|
|
||||||
Logout(*gin.Context)
|
|
||||||
ChangePassword(*gin.Context)
|
ChangePassword(*gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +57,7 @@ func NewUserController(us usecase.User) User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) Signup(ctx core.Context) {
|
func (uc *UserController) Create(ctx core.Context) {
|
||||||
var params model.User
|
var params model.User
|
||||||
|
|
||||||
if err := ctx.Bind(¶ms); err != nil {
|
if err := ctx.Bind(¶ms); err != nil {
|
||||||
@ -66,9 +65,15 @@ func (uc *UserController) Signup(ctx core.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check params validity (govalidator)
|
_, err := govalidator.ValidateStruct(params)
|
||||||
|
if err != nil {
|
||||||
|
errno := UserParamsErr
|
||||||
|
errno.Message = err.Error()
|
||||||
|
core.WriteResponse(ctx, errno, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_, err := uc.userUsecase.Create(ctx, ¶ms)
|
_, err = uc.userUsecase.Create(ctx, ¶ms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.WriteResponse(ctx, err, nil)
|
core.WriteResponse(ctx, err, nil)
|
||||||
return
|
return
|
||||||
@ -80,11 +85,5 @@ func (uc *UserController) Signup(ctx core.Context) {
|
|||||||
func (uc *UserController) UpdateInfo(ctx *gin.Context) {
|
func (uc *UserController) UpdateInfo(ctx *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) Login(ctx *gin.Context) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *UserController) Logout(ctx *gin.Context) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *UserController) ChangePassword(ctx *gin.Context) {
|
func (uc *UserController) ChangePassword(ctx *gin.Context) {
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ func Routes(engine *gin.Engine, c controller.AppController) *gin.Engine {
|
|||||||
{
|
{
|
||||||
userV1 := v1.Group("/user")
|
userV1 := v1.Group("/user")
|
||||||
{
|
{
|
||||||
userV1.POST("/signup", func(ctx *gin.Context) { c.User.Signup(ctx) })
|
userV1.POST("/create", func(ctx *gin.Context) { c.User.Create(ctx) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,10 +27,10 @@ import "time"
|
|||||||
// User model
|
// User model
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email" valid:"email"`
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name" valid:"required"`
|
||||||
LastName string `json:"last_name"`
|
LastName string `json:"last_name" valid:"required"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password" valid:"required"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestUserRepository struct{}
|
type TestUserRepository struct{}
|
||||||
@ -38,5 +39,10 @@ func (tur *TestUserRepository) Create(
|
|||||||
user := *u
|
user := *u
|
||||||
|
|
||||||
user.ID = 123
|
user.ID = 123
|
||||||
|
|
||||||
|
if user.Email == "duplicate@error.com" {
|
||||||
|
return nil, errors.New("blabla (SQLSTATE 23505)")
|
||||||
|
}
|
||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
@ -25,12 +25,22 @@ package usecase
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var UserExisted = &errno.Errno{
|
||||||
|
HTTP: http.StatusBadRequest,
|
||||||
|
Code: errno.ErrorCode(errno.FailedOperationCode, "UserExisted"),
|
||||||
|
Message: "email already existed.",
|
||||||
|
}
|
||||||
|
|
||||||
type userUsecase struct {
|
type userUsecase struct {
|
||||||
userRepo repo.UserRepository
|
userRepo repo.UserRepository
|
||||||
dbRepo repo.DBRepository
|
dbRepo repo.DBRepository
|
||||||
@ -48,15 +58,22 @@ func NewUserUsecase(r repo.UserRepository, d repo.DBRepository) User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uuc *userUsecase) Create(ctx context.Context, u *model.User) (*model.User, error) {
|
func (uuc *userUsecase) Create(ctx context.Context, u *model.User) (*model.User, error) {
|
||||||
|
// Hash the password
|
||||||
|
encrypted, err := bcrypt.GenerateFromPassword([]byte(u.Password), 12)
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorLog("encrypt password error", "err", err)
|
||||||
|
return nil, errno.InternalServerErr
|
||||||
|
}
|
||||||
|
u.Password = string(encrypted)
|
||||||
data, err := uuc.dbRepo.Transaction(
|
data, err := uuc.dbRepo.Transaction(
|
||||||
ctx,
|
ctx,
|
||||||
func(txCtx context.Context, tx interface{}) (interface{}, error) {
|
func(txCtx context.Context, tx interface{}) (interface{}, error) {
|
||||||
// TODO: should check if the user exists
|
|
||||||
// DB will return an error since we have set email to UNIQUE.
|
|
||||||
// But we may not want to expose the exact db error.
|
|
||||||
|
|
||||||
u, err := uuc.userRepo.Create(txCtx, tx, u)
|
u, err := uuc.userRepo.Create(txCtx, tx, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
match, _ := regexp.MatchString("SQLSTATE 23505", err.Error())
|
||||||
|
if match {
|
||||||
|
return nil, UserExisted
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateUser(t *testing.T) {
|
func TestCreateUser(t *testing.T) {
|
||||||
|
t.Run("normal create", func(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userUsecase := NewUserUsecase(&repo.TestUserRepository{}, &repo.TestDBRepository{})
|
userUsecase := NewUserUsecase(&repo.TestUserRepository{}, &repo.TestDBRepository{})
|
||||||
input := &model.User{
|
input := &model.User{
|
||||||
@ -46,4 +47,19 @@ func TestCreateUser(t *testing.T) {
|
|||||||
got, err := userUsecase.Create(ctx, input)
|
got, err := userUsecase.Create(ctx, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, want, got)
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("duplicate create", func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
userUsecase := NewUserUsecase(&repo.TestUserRepository{}, &repo.TestDBRepository{})
|
||||||
|
input := &model.User{
|
||||||
|
Email: "duplicate@error.com",
|
||||||
|
FirstName: "James",
|
||||||
|
LastName: "Bond",
|
||||||
|
Password: "verystrong",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := userUsecase.Create(ctx, input)
|
||||||
|
assert.EqualError(t, err, UserExisted.Error())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user