Compare commits
No commits in common. "cb7a4bf5c56433459b209a5740ce8c6e6161d881" and "344485d082216b702d8caf112fc30f399da1f99e" have entirely different histories.
cb7a4bf5c5
...
344485d082
69
README.md
69
README.md
@ -13,9 +13,6 @@
|
||||
- [Version](#version)
|
||||
- [2024/10/03](#20241003)
|
||||
- [2024/10/04](#20241004)
|
||||
- [2024/10/06](#20241006)
|
||||
- [Workflow](#workflow)
|
||||
- [2024/10/07](#20241007)
|
||||
<!--toc:end-->
|
||||
|
||||
A tricount like expense-sharing system written in Go
|
||||
@ -244,69 +241,3 @@ rest, I just put unit tests aside for later.
|
||||
That should be the correct workflow. But to save time, I will cut off the
|
||||
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,79 +37,28 @@ servers:
|
||||
- url: https:/localhost:8000/v1
|
||||
tags:
|
||||
- name: user
|
||||
- name: session
|
||||
|
||||
paths:
|
||||
/user/create:
|
||||
/user/signup:
|
||||
post:
|
||||
tags:
|
||||
- user
|
||||
description: Create a new user
|
||||
description: Sign up as a new user
|
||||
requestBody:
|
||||
description: Create a new user
|
||||
description: Sign up
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserCreateRequest'
|
||||
$ref: '#/components/schemas/UserSignUpRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
'400':
|
||||
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:
|
||||
application/json:
|
||||
schema:
|
||||
$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':
|
||||
description: Server side error
|
||||
content:
|
||||
@ -119,7 +68,7 @@ paths:
|
||||
|
||||
components:
|
||||
schemas:
|
||||
UserCreateRequest:
|
||||
UserSignUpRequest:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
@ -139,24 +88,12 @@ components:
|
||||
- fist_name
|
||||
- last_name
|
||||
- password
|
||||
SessionCreateRequest:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
example: bruce@wayne.com
|
||||
password:
|
||||
type: string
|
||||
example: verystrongpassword
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
ErrResponse:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
example: InternalError
|
||||
example: FailedOperation.UserAlreadyExists
|
||||
message:
|
||||
type: string
|
||||
example: "Server internal error."
|
||||
example: "User already exists."
|
||||
|
4
go.mod
4
go.mod
@ -3,20 +3,17 @@ module git.vinchent.xyz/vinchent/howmuch
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gosuri/uitable v0.0.4
|
||||
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/pflag v1.0.5
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/net v0.25.0
|
||||
)
|
||||
|
||||
@ -61,6 +58,7 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.uber.org/multierr v1.11.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/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -1,5 +1,3 @@
|
||||
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/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
@ -86,8 +84,6 @@ 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/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/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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
@ -29,15 +29,16 @@ import (
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase"
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// User is the user controller interface, it describes all the handlers
|
||||
// that need to be implemented for the /user endpoint
|
||||
type User interface {
|
||||
Create(core.Context)
|
||||
Signup(core.Context)
|
||||
UpdateInfo(*gin.Context)
|
||||
Login(*gin.Context)
|
||||
Logout(*gin.Context)
|
||||
ChangePassword(*gin.Context)
|
||||
}
|
||||
|
||||
@ -57,7 +58,7 @@ func NewUserController(us usecase.User) User {
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *UserController) Create(ctx core.Context) {
|
||||
func (uc *UserController) Signup(ctx core.Context) {
|
||||
var params model.User
|
||||
|
||||
if err := ctx.Bind(¶ms); err != nil {
|
||||
@ -65,15 +66,9 @@ func (uc *UserController) Create(ctx core.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(params)
|
||||
if err != nil {
|
||||
errno := UserParamsErr
|
||||
errno.Message = err.Error()
|
||||
core.WriteResponse(ctx, errno, nil)
|
||||
return
|
||||
}
|
||||
// TODO: check params validity (govalidator)
|
||||
|
||||
_, err = uc.userUsecase.Create(ctx, ¶ms)
|
||||
_, err := uc.userUsecase.Create(ctx, ¶ms)
|
||||
if err != nil {
|
||||
core.WriteResponse(ctx, err, nil)
|
||||
return
|
||||
@ -85,5 +80,11 @@ func (uc *UserController) Create(ctx core.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) {
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ func Routes(engine *gin.Engine, c controller.AppController) *gin.Engine {
|
||||
{
|
||||
userV1 := v1.Group("/user")
|
||||
{
|
||||
userV1.POST("/create", func(ctx *gin.Context) { c.User.Create(ctx) })
|
||||
userV1.POST("/signup", func(ctx *gin.Context) { c.User.Signup(ctx) })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,10 +27,10 @@ import "time"
|
||||
// User model
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Email string `json:"email" valid:"email"`
|
||||
FirstName string `json:"first_name" valid:"required"`
|
||||
LastName string `json:"last_name" valid:"required"`
|
||||
Password string `json:"password" valid:"required"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Password string `json:"password"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ import (
|
||||
"context"
|
||||
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type TestUserRepository struct{}
|
||||
@ -39,10 +38,5 @@ func (tur *TestUserRepository) Create(
|
||||
user := *u
|
||||
|
||||
user.ID = 123
|
||||
|
||||
if user.Email == "duplicate@error.com" {
|
||||
return nil, errors.New("blabla (SQLSTATE 23505)")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
@ -25,22 +25,12 @@ package usecase
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||
"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"
|
||||
"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 {
|
||||
userRepo repo.UserRepository
|
||||
dbRepo repo.DBRepository
|
||||
@ -58,22 +48,15 @@ func NewUserUsecase(r repo.UserRepository, d repo.DBRepository) User {
|
||||
}
|
||||
|
||||
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(
|
||||
ctx,
|
||||
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)
|
||||
if err != nil {
|
||||
match, _ := regexp.MatchString("SQLSTATE 23505", err.Error())
|
||||
if match {
|
||||
return nil, UserExisted
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -32,34 +32,18 @@ import (
|
||||
)
|
||||
|
||||
func TestCreateUser(t *testing.T) {
|
||||
t.Run("normal create", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userUsecase := NewUserUsecase(&repo.TestUserRepository{}, &repo.TestDBRepository{})
|
||||
input := &model.User{
|
||||
Email: "a@b.c",
|
||||
FirstName: "James",
|
||||
LastName: "Bond",
|
||||
Password: "verystrong",
|
||||
}
|
||||
want := input
|
||||
want.ID = 123
|
||||
ctx := context.Background()
|
||||
userUsecase := NewUserUsecase(&repo.TestUserRepository{}, &repo.TestDBRepository{})
|
||||
input := &model.User{
|
||||
Email: "a@b.c",
|
||||
FirstName: "James",
|
||||
LastName: "Bond",
|
||||
Password: "verystrong",
|
||||
}
|
||||
want := input
|
||||
want.ID = 123
|
||||
|
||||
got, err := userUsecase.Create(ctx, input)
|
||||
assert.NoError(t, err)
|
||||
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())
|
||||
})
|
||||
got, err := userUsecase.Create(ctx, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user