From a19046838ae4a2697ddbf4930314ff2d79dcf3a5 Mon Sep 17 00:00:00 2001 From: bayusamudra5502 <bayusamudra.55.02.com@gmail.com> Date: Tue, 14 Feb 2023 22:03:25 +0700 Subject: [PATCH] feat: finishing login --- docs/docs.go | 149 +++++++++++++++++++++++++++++ docs/swagger.json | 149 +++++++++++++++++++++++++++++ docs/swagger.yaml | 95 ++++++++++++++++++ go.mod | 12 ++- go.sum | 16 ++++ handler/auth/handler.go | 13 +++ handler/auth/login.go | 58 +++++++++++ handler/auth/refresh.go | 57 +++++++++++ handler/auth/types.go | 8 ++ handler/common/home.go | 1 + handler/di.go | 5 + model/domain/user/user.go | 2 +- model/web/auth/login/request.go | 10 +- model/web/auth/login/response.go | 7 +- model/web/auth/refresh/request.go | 5 + model/web/auth/refresh/response.go | 8 ++ model/web/auth/token/token.go | 2 +- repository/user/type.go | 8 +- repository/user/user.go | 24 +++-- routes/auth/route.go | 17 ++++ routes/common/route.go | 6 -- routes/di.go | 2 + routes/routes.go | 2 + service/auth/impl.go | 15 +++ service/auth/login.go | 74 ++++++++++++++ service/auth/refresh.go | 30 ++++++ service/auth/type.go | 8 +- service/di.go | 7 ++ utils/base64/impl.go | 13 +++ utils/base64/type.go | 6 ++ utils/di.go | 15 +++ utils/env/env.go | 8 ++ utils/password/impl.go | 35 +++++++ utils/password/type.go | 6 ++ utils/token/impl.go | 55 +++++++++++ utils/token/type.go | 10 ++ 36 files changed, 907 insertions(+), 31 deletions(-) create mode 100644 handler/auth/handler.go create mode 100644 handler/auth/login.go create mode 100644 handler/auth/refresh.go create mode 100644 handler/auth/types.go create mode 100644 model/web/auth/refresh/request.go create mode 100644 model/web/auth/refresh/response.go create mode 100644 routes/auth/route.go create mode 100644 service/auth/impl.go create mode 100644 service/auth/login.go create mode 100644 service/auth/refresh.go create mode 100644 utils/base64/impl.go create mode 100644 utils/base64/type.go create mode 100644 utils/password/impl.go create mode 100644 utils/password/type.go create mode 100644 utils/token/impl.go create mode 100644 utils/token/type.go diff --git a/docs/docs.go b/docs/docs.go index d9cd11b..a98b111 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -21,6 +21,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "common" + ], "summary": "Index page", "responses": { "200": { @@ -31,9 +34,155 @@ const docTemplate = `{ } } } + }, + "/auth/login": { + "post": { + "description": "Login and generate new pair of token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Login", + "parameters": [ + { + "description": "Login payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/login.LoginRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/login.LoginResponsePayload" + } + } + } + ] + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "Generate new access token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Refresh Token", + "parameters": [ + { + "type": "string", + "description": "Refresh token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/refresh.RefreshResponsePayload" + } + } + } + ] + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } } }, "definitions": { + "login.LoginRequestPayload": { + "description": "Information that should be available when do a login process", + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + }, + "password": { + "description": "User Password", + "type": "string", + "example": "secret" + } + } + }, + "login.LoginResponsePayload": { + "description": "Login response when process success", + "type": "object", + "properties": { + "access_token": { + "description": "Token that used to access the resources", + "type": "string" + }, + "refresh_token": { + "description": "Token that used to generate new access token", + "type": "string" + } + } + }, + "refresh.RefreshResponsePayload": { + "description": "Refresh endpoint response when process success", + "type": "object", + "properties": { + "access_token": { + "description": "Token that used to access the resources", + "type": "string" + } + } + }, "web.BaseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 7b049fa..442fa43 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -13,6 +13,9 @@ "produces": [ "application/json" ], + "tags": [ + "common" + ], "summary": "Index page", "responses": { "200": { @@ -23,9 +26,155 @@ } } } + }, + "/auth/login": { + "post": { + "description": "Login and generate new pair of token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Login", + "parameters": [ + { + "description": "Login payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/login.LoginRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/login.LoginResponsePayload" + } + } + } + ] + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "Generate new access token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Refresh Token", + "parameters": [ + { + "type": "string", + "description": "Refresh token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/refresh.RefreshResponsePayload" + } + } + } + ] + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } } }, "definitions": { + "login.LoginRequestPayload": { + "description": "Information that should be available when do a login process", + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + }, + "password": { + "description": "User Password", + "type": "string", + "example": "secret" + } + } + }, + "login.LoginResponsePayload": { + "description": "Login response when process success", + "type": "object", + "properties": { + "access_token": { + "description": "Token that used to access the resources", + "type": "string" + }, + "refresh_token": { + "description": "Token that used to generate new access token", + "type": "string" + } + } + }, + "refresh.RefreshResponsePayload": { + "description": "Refresh endpoint response when process success", + "type": "object", + "properties": { + "access_token": { + "description": "Token that used to access the resources", + "type": "string" + } + } + }, "web.BaseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7183dda..f21c315 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,36 @@ definitions: + login.LoginRequestPayload: + description: Information that should be available when do a login process + properties: + email: + description: User Email + example: someone@example.com + type: string + password: + description: User Password + example: secret + type: string + required: + - email + - password + type: object + login.LoginResponsePayload: + description: Login response when process success + properties: + access_token: + description: Token that used to access the resources + type: string + refresh_token: + description: Token that used to generate new access token + type: string + type: object + refresh.RefreshResponsePayload: + description: Refresh endpoint response when process success + properties: + access_token: + description: Token that used to access the resources + type: string + type: object web.BaseResponse: properties: data: {} @@ -27,4 +59,67 @@ paths: schema: $ref: '#/definitions/web.BaseResponse' summary: Index page + tags: + - common + /auth/login: + post: + consumes: + - application/json + description: Login and generate new pair of token + parameters: + - description: Login payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/login.LoginRequestPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/web.BaseResponse' + - properties: + data: + $ref: '#/definitions/login.LoginResponsePayload' + type: object + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Login + tags: + - auth + /auth/refresh: + post: + consumes: + - application/json + description: Generate new access token + parameters: + - description: Refresh token + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/web.BaseResponse' + - properties: + data: + $ref: '#/definitions/refresh.RefreshResponsePayload' + type: object + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Refresh Token + tags: + - auth swagger: "2.0" diff --git a/go.mod b/go.mod index 9ab9120..e4ec072 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,9 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.8 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.11.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -31,14 +34,15 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/swaggo/files v1.0.0 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/net v0.5.0 // indirect - golang.org/x/sys v0.4.0 // indirect - golang.org/x/text v0.6.0 // indirect + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/net v0.6.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect golang.org/x/tools v0.5.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a4c86f1..db1952c 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,12 @@ github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyr github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= +github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -66,6 +72,8 @@ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NB github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= @@ -104,6 +112,8 @@ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -116,6 +126,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -132,6 +144,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -146,6 +160,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/handler/auth/handler.go b/handler/auth/handler.go new file mode 100644 index 0000000..6a8f65b --- /dev/null +++ b/handler/auth/handler.go @@ -0,0 +1,13 @@ +package auth + +import ( + "gitlab.informatika.org/ocw/ocw-backend/service/auth" + "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" +) + +type AuthHandlerImpl struct { + auth.AuthService + httputil.HttpUtil + wrapper.WrapperUtil +} diff --git a/handler/auth/login.go b/handler/auth/login.go new file mode 100644 index 0000000..ee72964 --- /dev/null +++ b/handler/auth/login.go @@ -0,0 +1,58 @@ +package auth + +import ( + "net/http" + + "github.com/go-playground/validator/v10" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" +) + +// Index godoc +// +// @Tags auth +// @Summary Login +// @Description Login and generate new pair of token +// @Produce json +// @Accept json +// @Param data body login.LoginRequestPayload true "Login payload" +// @Success 200 {object} web.BaseResponse{data=login.LoginResponsePayload} +// @Failure 403 {object} web.BaseResponse +// @Router /auth/login [post] +func (a AuthHandlerImpl) Login(w http.ResponseWriter, r *http.Request) { + payload := login.LoginRequestPayload{} + validate := validator.New() + + if err := a.HttpUtil.ParseJson(r, &payload); err != nil { + payload := a.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := a.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errList := []string{} + for _, err := range err.(validator.ValidationErrors) { + errList = append(errList, err.Error()) + } + + payload := a.WrapperUtil.ErrorResponseWrap("input validation error", errList) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + response, err := a.AuthService.Login(payload) + + if err != nil { + payload := a.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + responsePayload := a.WrapperUtil.SuccessResponseWrap(response) + a.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/auth/refresh.go b/handler/auth/refresh.go new file mode 100644 index 0000000..bf8fda2 --- /dev/null +++ b/handler/auth/refresh.go @@ -0,0 +1,57 @@ +package auth + +import ( + "net/http" + "strings" + + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/refresh" +) + +// Index godoc +// +// @Tags auth +// @Summary Refresh Token +// @Description Generate new access token +// @Produce json +// @Accept json +// @Param Authorization header string true "Refresh token" +// @Success 200 {object} web.BaseResponse{data=refresh.RefreshResponsePayload} +// @Failure 403 {object} web.BaseResponse +// @Router /auth/refresh [post] +func (a AuthHandlerImpl) Refresh(w http.ResponseWriter, r *http.Request) { + payload := refresh.RefreshRequestPayload{} + + refreshTokenHeader := r.Header.Get("Authorization") + + if refreshTokenHeader == "" { + payload := a.WrapperUtil.ErrorResponseWrap("token is required", nil) + a.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(refreshTokenHeader, " ") + + if len(token) != 2 { + payload := a.WrapperUtil.ErrorResponseWrap("invalid token", nil) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := a.WrapperUtil.ErrorResponseWrap("invalid token", nil) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.RefreshToken = token[1] + response, err := a.AuthService.Refresh(payload) + + if err != nil { + payload := a.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + responsePayload := a.WrapperUtil.SuccessResponseWrap(response) + a.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/auth/types.go b/handler/auth/types.go new file mode 100644 index 0000000..49aa52c --- /dev/null +++ b/handler/auth/types.go @@ -0,0 +1,8 @@ +package auth + +import "net/http" + +type AuthHandler interface { + Login(w http.ResponseWriter, r *http.Request) + Refresh(w http.ResponseWriter, r *http.Request) +} diff --git a/handler/common/home.go b/handler/common/home.go index 81fd72f..7266a95 100644 --- a/handler/common/home.go +++ b/handler/common/home.go @@ -6,6 +6,7 @@ import ( // Index godoc // +// @Tags common // @Summary Index page // @Description Give server index page response // @Produce json diff --git a/handler/di.go b/handler/di.go index 7c3cb64..b07a554 100644 --- a/handler/di.go +++ b/handler/di.go @@ -2,6 +2,7 @@ package handler import ( "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/handler/auth" "gitlab.informatika.org/ocw/ocw-backend/handler/common" "gitlab.informatika.org/ocw/ocw-backend/handler/swagger" ) @@ -14,4 +15,8 @@ var HandlerSet = wire.NewSet( // Swagger wire.Struct(new(swagger.SwaggerHandlerImpl), "*"), wire.Bind(new(swagger.SwaggerHandler), new(*swagger.SwaggerHandlerImpl)), + + // Auth + wire.Struct(new(auth.AuthHandlerImpl), "*"), + wire.Bind(new(auth.AuthHandler), new(*auth.AuthHandlerImpl)), ) diff --git a/model/domain/user/user.go b/model/domain/user/user.go index ad30a66..62d5e1e 100644 --- a/model/domain/user/user.go +++ b/model/domain/user/user.go @@ -1,7 +1,7 @@ package user type User struct { - Username string `gorm:"primaryKey"` + Email string `gorm:"primaryKey"` Password string Name string Role UserRole `gorm:"type:user_role"` diff --git a/model/web/auth/login/request.go b/model/web/auth/login/request.go index cdd602c..716c505 100644 --- a/model/web/auth/login/request.go +++ b/model/web/auth/login/request.go @@ -1,7 +1,11 @@ package login +// Login Request Payload +// @Description Information that should be available when do a login process type LoginRequestPayload struct { - Email string `json:"email"` - Password string `json:"password"` - CaptchaToken string `json:"token"` + // User Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` + + // User Password + Password string `json:"password" validate:"required" example:"secret"` } diff --git a/model/web/auth/login/response.go b/model/web/auth/login/response.go index 4c77fe4..8cb68ca 100644 --- a/model/web/auth/login/response.go +++ b/model/web/auth/login/response.go @@ -1,6 +1,11 @@ package login +// Login Response Payload +// @Description Login response when process success type LoginResponsePayload struct { + // Token that used to generate new access token RefreshToken string `json:"refresh_token"` - AccessToken string `json:"access_token"` + + // Token that used to access the resources + AccessToken string `json:"access_token"` } diff --git a/model/web/auth/refresh/request.go b/model/web/auth/refresh/request.go new file mode 100644 index 0000000..4fd9c44 --- /dev/null +++ b/model/web/auth/refresh/request.go @@ -0,0 +1,5 @@ +package refresh + +type RefreshRequestPayload struct { + RefreshToken string +} diff --git a/model/web/auth/refresh/response.go b/model/web/auth/refresh/response.go new file mode 100644 index 0000000..f349ba6 --- /dev/null +++ b/model/web/auth/refresh/response.go @@ -0,0 +1,8 @@ +package refresh + +// Refresh Response Payload +// @Description Refresh endpoint response when process success +type RefreshResponsePayload struct { + // Token that used to access the resources + AccessToken string `json:"access_token"` +} diff --git a/model/web/auth/token/token.go b/model/web/auth/token/token.go index b17d002..497287e 100644 --- a/model/web/auth/token/token.go +++ b/model/web/auth/token/token.go @@ -6,7 +6,7 @@ import ( ) type UserClaim struct { - jwt.StandardClaims + jwt.RegisteredClaims Name string `json:"name"` Email string `json:"email"` Role user.UserRole `json:"role"` diff --git a/repository/user/type.go b/repository/user/type.go index f181046..047ee69 100644 --- a/repository/user/type.go +++ b/repository/user/type.go @@ -5,8 +5,8 @@ import ( ) type UserRepository interface { - Add(user user.User) - Get(username string) user.User - Update(user user.User) - Delete(username string) + Add(user user.User) error + Get(username string) (*user.User, error) + Update(user user.User) error + Delete(username string) error } diff --git a/repository/user/user.go b/repository/user/user.go index 2287a31..881a4c6 100644 --- a/repository/user/user.go +++ b/repository/user/user.go @@ -16,21 +16,25 @@ func New( return &UserRepositoryImpl{db.Connect()} } -func (repo UserRepositoryImpl) Add(user user.User) { - repo.db.Create(user) +func (repo UserRepositoryImpl) Add(user user.User) error { + return repo.db.Create(user).Error } -func (repo UserRepositoryImpl) Get(username string) user.User { - result := user.User{} - repo.db.First(&result, "username = ?", username) +func (repo UserRepositoryImpl) Get(username string) (*user.User, error) { + result := &user.User{} + err := repo.db.Where("username = ?", username).First(result).Error - return result + if err != nil { + return nil, err + } + + return result, nil } -func (repo UserRepositoryImpl) Update(user user.User) { - repo.db.Save(user) +func (repo UserRepositoryImpl) Update(user user.User) error { + return repo.db.Save(user).Error } -func (repo UserRepositoryImpl) Delete(username string) { - repo.db.Delete(&user.User{}, "username = ?", username) +func (repo UserRepositoryImpl) Delete(username string) error { + return repo.db.Where("username = ?", username).Delete(&user.User{}).Error } diff --git a/routes/auth/route.go b/routes/auth/route.go new file mode 100644 index 0000000..55377de --- /dev/null +++ b/routes/auth/route.go @@ -0,0 +1,17 @@ +package auth + +import ( + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/handler/auth" +) + +type AuthRoutes struct { + auth.AuthHandler +} + +func (ar AuthRoutes) Register(r chi.Router) { + r.Route("/auth", func(r chi.Router) { + r.Post("/login", ar.AuthHandler.Login) + r.Post("/refresh", ar.AuthHandler.Refresh) + }) +} diff --git a/routes/common/route.go b/routes/common/route.go index a04fc14..47e768a 100644 --- a/routes/common/route.go +++ b/routes/common/route.go @@ -4,17 +4,11 @@ import ( "net/http" "github.com/go-chi/chi/v5" - "gitlab.informatika.org/ocw/ocw-backend/service/common" - "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" - "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" commonHandler "gitlab.informatika.org/ocw/ocw-backend/handler/common" ) type CommonRoutes struct { - common.CommonService - httputil.HttpUtil - wrapper.WrapperUtil commonHandler.CommonHandler } diff --git a/routes/di.go b/routes/di.go index 93a65f0..ad350ce 100644 --- a/routes/di.go +++ b/routes/di.go @@ -2,6 +2,7 @@ package routes import ( "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/routes/auth" "gitlab.informatika.org/ocw/ocw-backend/routes/common" "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" ) @@ -9,6 +10,7 @@ import ( var routesCollectionSet = wire.NewSet( wire.Struct(new(common.CommonRoutes), "*"), wire.Struct(new(swagger.SwaggerRoutes), "*"), + wire.Struct(new(auth.AuthRoutes), "*"), ) var RoutesSet = wire.NewSet( diff --git a/routes/routes.go b/routes/routes.go index 6e37d83..c824d2d 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -1,6 +1,7 @@ package routes import ( + "gitlab.informatika.org/ocw/ocw-backend/routes/auth" "gitlab.informatika.org/ocw/ocw-backend/routes/common" "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" @@ -11,6 +12,7 @@ type AppRouter struct { // Routes swagger.SwaggerRoutes common.CommonRoutes + auth.AuthRoutes // Utility Logger logger.Logger diff --git a/service/auth/impl.go b/service/auth/impl.go new file mode 100644 index 0000000..d8ddef7 --- /dev/null +++ b/service/auth/impl.go @@ -0,0 +1,15 @@ +package auth + +import ( + "gitlab.informatika.org/ocw/ocw-backend/repository/user" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/password" + "gitlab.informatika.org/ocw/ocw-backend/utils/token" +) + +type AuthServiceImpl struct { + user.UserRepository + password.PasswordUtil + env.Environment + token.TokenUtil +} diff --git a/service/auth/login.go b/service/auth/login.go new file mode 100644 index 0000000..85d6de9 --- /dev/null +++ b/service/auth/login.go @@ -0,0 +1,74 @@ +package auth + +import ( + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v4" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" + tokenModel "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gorm.io/gorm" +) + +func (auth AuthServiceImpl) Login(payload login.LoginRequestPayload) (*login.LoginResponsePayload, error) { + user, err := auth.Get(payload.Email) + + if err != nil { + var errorObj error + + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + errorObj = fmt.Errorf("username or password combination not found") + default: + errorObj = err + } + + return nil, errorObj + } + + if err := auth.Check(payload.Password, user.Password); err != nil { + return nil, err + } + + refreshClaim := tokenModel.UserClaim{ + Name: user.Name, + Email: user.Email, + Role: user.Role, + Type: tokenModel.Refresh, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(auth.TokenRefreshExpired) * time.Millisecond)), + Issuer: auth.TokenIssuer, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + accessClaim := tokenModel.UserClaim{ + Name: user.Name, + Email: user.Email, + Role: user.Role, + Type: tokenModel.Access, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(auth.TokenAccessExpired) * time.Millisecond)), + Issuer: auth.TokenIssuer, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + refreshToken, err := auth.TokenUtil.Generate(refreshClaim) + + if err != nil { + return nil, err + } + + accessToken, err := auth.TokenUtil.Generate(accessClaim) + + if err != nil { + return nil, err + } + + return &login.LoginResponsePayload{ + RefreshToken: refreshToken, + AccessToken: accessToken, + }, nil +} diff --git a/service/auth/refresh.go b/service/auth/refresh.go new file mode 100644 index 0000000..70aecd6 --- /dev/null +++ b/service/auth/refresh.go @@ -0,0 +1,30 @@ +package auth + +import ( + "time" + + "github.com/golang-jwt/jwt/v4" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/refresh" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" +) + +func (auth AuthServiceImpl) Refresh(payload refresh.RefreshRequestPayload) (*refresh.RefreshResponsePayload, error) { + claim, err := auth.TokenUtil.Validate(payload.RefreshToken, token.Refresh) + + if err != nil { + return nil, err + } + + claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Duration(auth.TokenAccessExpired) * time.Millisecond)) + claim.Type = token.Access + + newToken, err := auth.TokenUtil.Generate(*claim) + + if err != nil { + return nil, err + } + + return &refresh.RefreshResponsePayload{ + AccessToken: newToken, + }, nil +} diff --git a/service/auth/type.go b/service/auth/type.go index 59d4ebd..87a4615 100644 --- a/service/auth/type.go +++ b/service/auth/type.go @@ -1,5 +1,11 @@ package auth +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/refresh" +) + type AuthService interface { - Login(email string, password string) + Login(payload login.LoginRequestPayload) (*login.LoginResponsePayload, error) + Refresh(payload refresh.RefreshRequestPayload) (*refresh.RefreshResponsePayload, error) } diff --git a/service/di.go b/service/di.go index aafd073..b2eb205 100644 --- a/service/di.go +++ b/service/di.go @@ -2,6 +2,7 @@ package service import ( "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/service/auth" "gitlab.informatika.org/ocw/ocw-backend/service/common" "gitlab.informatika.org/ocw/ocw-backend/service/logger" "gitlab.informatika.org/ocw/ocw-backend/service/logger/hooks" @@ -20,6 +21,12 @@ var ServiceTestSet = wire.NewSet( reporter.New, wire.Bind(new(reporter.Reporter), new(*reporter.LogtailReporter)), ), + + // auth service + wire.NewSet( + wire.Struct(new(auth.AuthServiceImpl)), + wire.Bind(new(auth.AuthService), new(*auth.AuthServiceImpl)), + ), ) var ServiceSet = wire.NewSet( diff --git a/utils/base64/impl.go b/utils/base64/impl.go new file mode 100644 index 0000000..9783d90 --- /dev/null +++ b/utils/base64/impl.go @@ -0,0 +1,13 @@ +package base64 + +import "encoding/base64" + +type Base64UtilImpl struct{} + +func (Base64UtilImpl) Encode(input []byte) string { + return base64.StdEncoding.EncodeToString(input) +} + +func (Base64UtilImpl) Decode(input string) ([]byte, error) { + return base64.StdEncoding.DecodeString(input) +} diff --git a/utils/base64/type.go b/utils/base64/type.go new file mode 100644 index 0000000..568b601 --- /dev/null +++ b/utils/base64/type.go @@ -0,0 +1,6 @@ +package base64 + +type Base64Util interface { + Encode(input []byte) string + Decode(input string) ([]byte, error) +} diff --git a/utils/di.go b/utils/di.go index 9e4afce..5b8ac4d 100644 --- a/utils/di.go +++ b/utils/di.go @@ -3,11 +3,14 @@ package utils import ( "github.com/google/wire" "gitlab.informatika.org/ocw/ocw-backend/utils/app" + "gitlab.informatika.org/ocw/ocw-backend/utils/base64" "gitlab.informatika.org/ocw/ocw-backend/utils/db" "gitlab.informatika.org/ocw/ocw-backend/utils/env" "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" "gitlab.informatika.org/ocw/ocw-backend/utils/log" + "gitlab.informatika.org/ocw/ocw-backend/utils/password" "gitlab.informatika.org/ocw/ocw-backend/utils/res" + "gitlab.informatika.org/ocw/ocw-backend/utils/token" "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" ) @@ -32,6 +35,18 @@ var UtilSetTest = wire.NewSet( wire.Struct(new(wrapper.WrapperUtilImpl), "*"), wire.Bind(new(wrapper.WrapperUtil), new(*wrapper.WrapperUtilImpl)), + // Base64 Utility + wire.Struct(new(base64.Base64UtilImpl), "*"), + wire.Bind(new(base64.Base64Util), new(*base64.Base64UtilImpl)), + + // Password utility + wire.Struct(new(password.PasswordUtilImpl), "*"), + wire.Bind(new(password.PasswordUtil), new(*password.PasswordUtilImpl)), + + // Token utility + wire.Struct(new(token.TokenUtilImpl), "*"), + wire.Bind(new(token.TokenUtil), new(*token.TokenUtilImpl)), + // app app.New, wire.Bind(new(app.Server), new(*app.HttpServer)), diff --git a/utils/env/env.go b/utils/env/env.go index df6fcbb..1112fb6 100644 --- a/utils/env/env.go +++ b/utils/env/env.go @@ -19,6 +19,14 @@ type Environment struct { UseReporter bool `env:"USE_REPORTER" envDefault:"true"` DatabaseConnection string `env:"DB_STRING"` + + PasswordCost int `env:"PASSWORD_COST" envDefault:"10"` + + TokenMethod string `env:"TOKEN_SIGNING_METHOD" envDefault:"hs512"` + TokenSecret string `env:"TOKEN_SECRET"` + TokenRefreshExpired int64 `env:"TOKEN_REFRESH_EXPIRED_MS" envDefault:"86400000"` + TokenAccessExpired int64 `env:"TOKEN_ACCESS_EXPIRED_MS" envDefault:"300000"` + TokenIssuer string `env:"TOKEN_ISSUER" envDefault:"ocw"` } func New() (*Environment, error) { diff --git a/utils/password/impl.go b/utils/password/impl.go new file mode 100644 index 0000000..f9cb7ab --- /dev/null +++ b/utils/password/impl.go @@ -0,0 +1,35 @@ +package password + +import ( + "fmt" + + "gitlab.informatika.org/ocw/ocw-backend/utils/base64" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "golang.org/x/crypto/bcrypt" +) + +type PasswordUtilImpl struct { + env.Environment + base64.Base64Util +} + +func (e PasswordUtilImpl) Hash(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), e.Environment.PasswordCost) + return e.Base64Util.Encode(hash), err +} + +func (e PasswordUtilImpl) Check(password string, hashedPassword string) error { + hash, err := e.Base64Util.Decode(hashedPassword) + + if err != nil { + return err + } + + err = bcrypt.CompareHashAndPassword(hash, []byte(password)) + + if err != nil { + return fmt.Errorf("username or password combination is not found") + } + + return nil +} diff --git a/utils/password/type.go b/utils/password/type.go new file mode 100644 index 0000000..026a583 --- /dev/null +++ b/utils/password/type.go @@ -0,0 +1,6 @@ +package password + +type PasswordUtil interface { + Hash(password string) (string, error) + Check(password string, hashedPassword string) error +} diff --git a/utils/token/impl.go b/utils/token/impl.go new file mode 100644 index 0000000..9dde3aa --- /dev/null +++ b/utils/token/impl.go @@ -0,0 +1,55 @@ +package token + +import ( + "fmt" + + "github.com/golang-jwt/jwt/v4" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" +) + +type TokenUtilImpl struct { + env.Environment +} + +func (t TokenUtilImpl) Method() jwt.SigningMethod { + switch t.TokenMethod { + case "hs256": + return jwt.SigningMethodHS256 + default: + return jwt.SigningMethodHS512 + } +} + +func (tu TokenUtilImpl) Validate(tokenString string, tokenType token.TokenType) (*token.UserClaim, error) { + jwtData, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { + if method, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("invalid signing method") + } else if method != tu.Method() { + return nil, fmt.Errorf("invalid signing method") + } + + return tu.Method(), nil + }) + + if err != nil { + return nil, err + } + + claims := jwtData.Claims.(*token.UserClaim) + + if claims.Type != tokenType { + return claims, fmt.Errorf("token type is not valid") + } + + return claims, nil +} + +func (t TokenUtilImpl) Generate(claim token.UserClaim) (string, error) { + token := jwt.NewWithClaims( + jwt.SigningMethodHS512, + claim, + ) + + return token.SignedString([]byte(t.TokenSecret)) +} diff --git a/utils/token/type.go b/utils/token/type.go new file mode 100644 index 0000000..cd6c27a --- /dev/null +++ b/utils/token/type.go @@ -0,0 +1,10 @@ +package token + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" +) + +type TokenUtil interface { + Validate(jwt string, tokenType token.TokenType) (*token.UserClaim, error) + Generate(claim token.UserClaim) (string, error) +} -- GitLab