diff --git a/.env b/.env index 992d1332de568160529eb77e58ab3ab53405b083..3958664c25ea7dec0d286c76fdaa96d87af56610 100644 --- a/.env +++ b/.env @@ -3,4 +3,8 @@ LISTEN_ADDR=0.0.0.0 PORT=8080 LOGTAIL_TOKEN= HTTP_TIMEOUT_SEC=2 -LOG_FLUSH_INTERVAL_MS=1000 \ No newline at end of file +LOG_FLUSH_INTERVAL_MS=1000 +DB_STRING="host=database user=ocw password=ocw dbname=ocw-db port=5432 sslmode=disable TimeZone=Asia/Shanghai" +SMTP_USERNAME="noreply@ocw.id" +SMTP_SERVER=localhost +SMTP_PORT=1025 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..afb1b518807dedc2f9b7e1f399cc585878bcee32 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}" + } + ] +} diff --git a/README.md b/README.md index 09750369fb1b69c3a5ec6e7555749c8c3036a6bb..a22908f3646bda0e98b61c433a6d598de9f47a90 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OCW Backend +# ITBOpenCourseWare Backend Repository ini adalah repository untuk OCW Backend @@ -11,9 +11,10 @@ Ini adalah requirement untuk menjalankan template ini: 3. Postgresql (Optional, when using docker) 4. GNU Make 4.3 (Optional, when using docker) 5. Minio (Optional, when using docker) -6. Google wire v0.5.0 (https://github.com/google/wire) [MANDATORY] -7. Air command line (https://github.com/cosmtrek/air) -8. Redis +6. Google wire v0.5.0 (<https://github.com/google/wire>) [MANDATORY] +7. swaggo (<https://github.com/swaggo/swag>) +8. Air command line (<https://github.com/cosmtrek/air>) +9. Redis ## Cara menjalankan @@ -22,3 +23,66 @@ Untuk menjalankan server ini, gunakan: ```sh make watch ``` + +## Development Guide + +1. Checkout ke branch staging + pull + + ```sh + git checkout staging + git pull + ``` + +2. Buat branch baru dari staging dengan format `feat/s<nomor sprint>-sb<nomor sb>-<nama fitur dipisah dengan strip (-)>`. Jika membuat perubahan dari fitur yang sudah ada di staging, formatnya menjadi `fix/s<nomor sprint>-sb<nomor sb>-<apa yang diubah dari fitur tersebut>` + + ```sh + git checkout -b feat/s1-sb1-login + ``` + +3. Lakukan perubahan pada branch tersebut, commit dengan format `feat(<scope fitur>): <isi perubahan>`. Jika melakukan perubahan pada fitur yang sudah ada, formatnya menjadi `fix(<scope fitur>): <isi perubahan>`, lain-lainnya bisa dilihat di [semantic commit](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) + + ```sh + git commit -m "feat(login): add login page" + ``` + +4. Push branch ke remote + + ```sh + git push origin feat/s1-sb1-login + ``` + +5. Buat merge request ke branch staging, tambah assignee diri sendiri dan reviewer + +### Handling Accidents + +- Salah branch untuk commit? + + Gunakan `git reset --soft HEAD~1` untuk menghapus commit terakhir, lalu `git checkout <branch yang benar>` untuk kembali ke branch yang benar. Commit ulang dengan format yang benar. Tapi hal ini hanya bisa dilakukan sebelum push ke remote. + + Baru sadar salah branch setelah beberapa commit? Gunakan `git reset --soft HEAD~<jumlah commit sebelumnya>` + +- Ada update baru dari staging ketika sedang mengerjakan pekerjaan di branch sendiri? + + Sebenarnya jika itu pekerjaan orang yang tidak ada hubungannya dengan anda, tidak masalah. + + Tapi jika iya, contohnya ingin mengambil komponen yang baru ada di staging, gunakan `git rebase staging` untuk mengambil update dari staging. Jika ada konflik, selesaikan konflik tersebut, lalu `git add .` dan `git rebase --continue` untuk melanjutkan rebase. Jika sudah selesai, push ulang branch anda ke remote dengan `git push -f`. + + Sistem kerja rebase *basically* memutuskan semua commit pekerjaan anda sampai commit terakhir yang sinkron dengan staging, git pull dari staging, lalu menyambungkan kembali sehingga dapat meminimalisir konflik, walaupun tetap bisa ada konflik, terutama jika ada perubahan di file sama. Selengkapnya bisa dilihat ilustrasinya sebagai berikut. + +  + +- Ada konflik saat melakukan rebase? + + Selesaikan konflik tersebut, lalu `git add .` dan `git rebase --continue` untuk melanjutkan rebase. Jika sudah selesai, push ulang branch anda ke remote dengan `git push -f`. + +- Ada konflik saat melakukan merge request? + + Selesaikan konflik tersebut, lalu `git add .` dan `git commit --amend` untuk menggabungkan konflik tersebut ke commit terakhir (jika malas). Push ulang branch anda ke remote dengan `git push -f`. + +- Ada salah di pesan commit terakhir? + + Gunakan `git commit --amend` untuk mengganti commit terakhir dengan commit baru. Jika sudah selesai, push ulang branch anda ke remote dengan `git push -f`. + +- Baru sadar ada salah pesan di beberapa commit sebelumnya? + + Gunakan `git rebase -i HEAD~<jumlah commit sebelumnya>` untuk mengubah pesan commit dari commit terakhir hingga beberapa sebelumnya. Jika sudah selesai, push ulang branch anda ke remote dengan `git push -f`. diff --git a/di.go b/di.go index 25590b935772d6f66e014cea15c7885f889eb4fd..66bbf655c71fb48a23bc81ddca3513bcfec22cc9 100644 --- a/di.go +++ b/di.go @@ -8,6 +8,8 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/handler" "gitlab.informatika.org/ocw/ocw-backend/middleware" + "gitlab.informatika.org/ocw/ocw-backend/provider" + "gitlab.informatika.org/ocw/ocw-backend/repository" "gitlab.informatika.org/ocw/ocw-backend/routes" "gitlab.informatika.org/ocw/ocw-backend/service" "gitlab.informatika.org/ocw/ocw-backend/utils" @@ -18,10 +20,12 @@ import ( func CreateServer() (app.Server, error) { wire.Build( utils.UtilSet, + repository.RepositorySet, handler.HandlerSet, middleware.MiddlewareSet, routes.RoutesSet, service.ServiceSet, + provider.ProviderSet, ) return nil, nil diff --git a/docs/docs.go b/docs/docs.go index a98b111dc8da9f556c81a35f860f70b8bf847d49..6fece1a6822d55bcc98a20a155a4039e5fb1a903 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -35,6 +35,100 @@ const docTemplate = `{ } } }, + "/admin/user": { + "get": { + "description": "Get all users from database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get All User", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "post": { + "description": "Add a user to database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Add User", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "delete": { + "description": "Delete a user from database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete User By Id", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "patch": { + "description": "Update a user from database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update User By Id", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/admin/user/{id}": { + "get": { + "description": "Get a user from database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get User By Email", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, "/auth/login": { "post": { "description": "Login and generate new pair of token", @@ -61,7 +155,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Login Success", "schema": { "allOf": [ { @@ -78,8 +172,47 @@ const docTemplate = `{ ] } }, + "400": { + "description": "Bad Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, "403": { - "description": "Forbidden", + "description": "Login Credential Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "415": { + "description": "Not a json request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Invalid JSON input", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Unknown Internal Error", "schema": { "$ref": "#/definitions/web.BaseResponse" } @@ -128,8 +261,66 @@ const docTemplate = `{ ] } }, - "403": { - "description": "Forbidden", + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Generate New Account as Member", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register New Account", + "parameters": [ + { + "description": "Register Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/register.RegisterRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/web.BaseResponse" } @@ -183,6 +374,38 @@ const docTemplate = `{ } } }, + "register.RegisterRequestPayload": { + "description": "Information that should be available when do a registration process", + "type": "object", + "required": [ + "email", + "name", + "password", + "password_validation" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + }, + "name": { + "description": "User name", + "type": "string", + "example": "someone" + }, + "password": { + "description": "User Password", + "type": "string", + "example": "secret" + }, + "password_validation": { + "description": "User Password Validation, must be same as user", + "type": "string", + "example": "secret" + } + } + }, "web.BaseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 442fa43a8941e6e2ba171b1a31a3da9bd35cfd73..513bd4b6b3f4b57241fb86bbb6eb81d7ed0bd1b1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -27,6 +27,100 @@ } } }, + "/admin/user": { + "get": { + "description": "Get all users from database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get All User", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "post": { + "description": "Add a user to database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Add User", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "delete": { + "description": "Delete a user from database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete User By Id", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "patch": { + "description": "Update a user from database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update User By Id", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/admin/user/{id}": { + "get": { + "description": "Get a user from database", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get User By Email", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, "/auth/login": { "post": { "description": "Login and generate new pair of token", @@ -53,7 +147,7 @@ ], "responses": { "200": { - "description": "OK", + "description": "Login Success", "schema": { "allOf": [ { @@ -70,8 +164,47 @@ ] } }, + "400": { + "description": "Bad Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, "403": { - "description": "Forbidden", + "description": "Login Credential Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "415": { + "description": "Not a json request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Invalid JSON input", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Unknown Internal Error", "schema": { "$ref": "#/definitions/web.BaseResponse" } @@ -120,8 +253,66 @@ ] } }, - "403": { - "description": "Forbidden", + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Generate New Account as Member", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register New Account", + "parameters": [ + { + "description": "Register Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/register.RegisterRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/web.BaseResponse" } @@ -175,6 +366,38 @@ } } }, + "register.RegisterRequestPayload": { + "description": "Information that should be available when do a registration process", + "type": "object", + "required": [ + "email", + "name", + "password", + "password_validation" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + }, + "name": { + "description": "User name", + "type": "string", + "example": "someone" + }, + "password": { + "description": "User Password", + "type": "string", + "example": "secret" + }, + "password_validation": { + "description": "User Password Validation, must be same as user", + "type": "string", + "example": "secret" + } + } + }, "web.BaseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f21c3156cb87cc9dfa4db1b7077bec04d0cc4c99..ee76e9375fe026003fafa69a41451de0804766b5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -31,6 +31,31 @@ definitions: description: Token that used to access the resources type: string type: object + register.RegisterRequestPayload: + description: Information that should be available when do a registration process + properties: + email: + description: User Email + example: someone@example.com + type: string + name: + description: User name + example: someone + type: string + password: + description: User Password + example: secret + type: string + password_validation: + description: User Password Validation, must be same as user + example: secret + type: string + required: + - email + - name + - password + - password_validation + type: object web.BaseResponse: properties: data: {} @@ -61,6 +86,68 @@ paths: summary: Index page tags: - common + /admin/user: + delete: + description: Delete a user from database + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Delete User By Id + tags: + - admin + get: + description: Get all users from database + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Get All User + tags: + - admin + patch: + description: Update a user from database + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Update User By Id + tags: + - admin + post: + description: Add a user to database + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Add User + tags: + - admin + /admin/user/{id}: + get: + description: Get a user from database + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Get User By Email + tags: + - admin /auth/login: post: consumes: @@ -77,7 +164,7 @@ paths: - application/json responses: "200": - description: OK + description: Login Success schema: allOf: - $ref: '#/definitions/web.BaseResponse' @@ -85,8 +172,31 @@ paths: data: $ref: '#/definitions/login.LoginResponsePayload' type: object + "400": + description: Bad Input + schema: + allOf: + - $ref: '#/definitions/web.BaseResponse' + - properties: + data: + items: + type: string + type: array + type: object "403": - description: Forbidden + description: Login Credential Error + schema: + $ref: '#/definitions/web.BaseResponse' + "415": + description: Not a json request + schema: + $ref: '#/definitions/web.BaseResponse' + "422": + description: Invalid JSON input + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Unknown Internal Error schema: $ref: '#/definitions/web.BaseResponse' summary: Login @@ -115,11 +225,49 @@ paths: data: $ref: '#/definitions/refresh.RefreshResponsePayload' type: object - "403": - description: Forbidden + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error schema: $ref: '#/definitions/web.BaseResponse' summary: Refresh Token tags: - auth + /auth/register: + post: + consumes: + - application/json + description: Generate New Account as Member + parameters: + - description: Register Payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/register.RegisterRequestPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Register New Account + tags: + - auth swagger: "2.0" diff --git a/go.mod b/go.mod index e4ec072ee7ce0bf1e440935c9cf64e66a807fb97..b69d320688f3db0c6bbdcc1ad19eea994fde74ea 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,17 @@ require ( github.com/caarlos0/env/v6 v6.10.1 github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/cors v1.2.1 + github.com/go-playground/validator/v10 v10.11.2 + github.com/golang-jwt/jwt/v4 v4.4.3 github.com/google/wire v0.5.0 github.com/joho/godotenv v1.5.0 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 github.com/swaggo/http-swagger v1.3.3 github.com/swaggo/swag v1.8.10 + golang.org/x/crypto v0.6.0 + gorm.io/driver/postgres v1.4.6 + gorm.io/gorm v1.24.5 ) require ( @@ -19,15 +24,17 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/cosmtrek/air v1.41.0 // indirect + github.com/creack/pty v1.1.18 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.14.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect 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 github.com/jackc/pgx/v5 v5.2.0 // indirect @@ -36,16 +43,16 @@ require ( 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/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/pelletier/go-toml v1.9.5 // 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.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 - gorm.io/driver/postgres v1.4.6 // indirect - gorm.io/gorm v1.24.5 // indirect ) diff --git a/go.sum b/go.sum index db1952c412bd4a9347d5c577dbd6e51d28ee171c..d771a874f694fecc968e13f8658ba7de1cd1ca6f 100644 --- a/go.sum +++ b/go.sum @@ -8,10 +8,18 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= +github.com/cosmtrek/air v1.41.0 h1:6ck2LbcVvby6cyuwE8ruia41U2nppMZGWOpq+E/EhoU= +github.com/cosmtrek/air v1.41.0/go.mod h1:+RBGjJt7T2f3I7td8Tvk0XsH/hZ3E1QBLfiWObICO4c= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= @@ -47,6 +55,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= @@ -80,7 +90,14 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -140,6 +157,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= @@ -180,6 +199,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.4.6 h1:1FPESNXqIKG5JmraaH2bfCVlMQ7paLoCreFxDtqzwdc= diff --git a/handler/admin/addUser.go b/handler/admin/addUser.go new file mode 100644 index 0000000000000000000000000000000000000000..7b53f3d3d4977c157d67048d3079c40e46e446fe --- /dev/null +++ b/handler/admin/addUser.go @@ -0,0 +1,18 @@ +package admin + +import ( + "net/http" +) + +// Index godoc +// +// @Tags admin +// @Summary Add User +// @Description Add a user to database +// @Produce json +// @Success 200 {object} web.BaseResponse +// @Router /admin/user [post] +func (route AdminHandlerImpl) AddUser(w http.ResponseWriter, r *http.Request){ + payload := route.WrapperUtil.SuccessResponseWrap(route.AdminService.AddUser()) + route.HttpUtil.WriteSuccessJson(w, payload) +} \ No newline at end of file diff --git a/handler/admin/deleteUser.go b/handler/admin/deleteUser.go new file mode 100644 index 0000000000000000000000000000000000000000..0be96ccc00451aaf0dd1490d6b29154a1b2d62dd --- /dev/null +++ b/handler/admin/deleteUser.go @@ -0,0 +1,18 @@ +package admin + +import ( + "net/http" +) + +// Index godoc +// +// @Tags admin +// @Summary Delete User By Id +// @Description Delete a user from database +// @Produce json +// @Success 200 {object} web.BaseResponse +// @Router /admin/user [delete] +func (route AdminHandlerImpl) DeleteUser(w http.ResponseWriter, r *http.Request){ + payload := route.WrapperUtil.SuccessResponseWrap(route.AdminService.DeleteUser()) + route.HttpUtil.WriteSuccessJson(w, payload) +} \ No newline at end of file diff --git a/handler/admin/getAllUser.go b/handler/admin/getAllUser.go new file mode 100644 index 0000000000000000000000000000000000000000..32771d8d99fcf9d82f53279bd70706a35d30f1b7 --- /dev/null +++ b/handler/admin/getAllUser.go @@ -0,0 +1,27 @@ +package admin + +import ( + "net/http" +) + +// Index godoc +// +// @Tags admin +// @Summary Get All User +// @Description Get all users from database +// @Produce json +// @Success 200 {object} web.BaseResponse +// @Router /admin/user [get] +func (route AdminHandlerImpl) GetAllUser(w http.ResponseWriter, r *http.Request){ + // get all user from service + users, err := route.AdminService.GetAllUser() + if err != nil { + payload := route.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + route.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // wrap the response + payload := route.WrapperUtil.SuccessResponseWrap(users) + route.HttpUtil.WriteSuccessJson(w, payload) +} \ No newline at end of file diff --git a/handler/admin/getUserByEmail.go b/handler/admin/getUserByEmail.go new file mode 100644 index 0000000000000000000000000000000000000000..0ad7a59fce2214548fd0aaae8b2443e4b5595ca7 --- /dev/null +++ b/handler/admin/getUserByEmail.go @@ -0,0 +1,18 @@ +package admin + +import ( + "net/http" +) + +// Index godoc +// +// @Tags admin +// @Summary Get User By Email +// @Description Get a user from database +// @Produce json +// @Success 200 {object} web.BaseResponse +// @Router /admin/user/{id} [get] +func (route AdminHandlerImpl) GetUserByEmail(w http.ResponseWriter, r *http.Request) { + payload := route.WrapperUtil.SuccessResponseWrap(route.AdminService.GetUserByEmail()) + route.HttpUtil.WriteSuccessJson(w, payload) +} diff --git a/handler/admin/handler.go b/handler/admin/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..ac705969f7b899ea3b8749d288ddf047ea01d7dc --- /dev/null +++ b/handler/admin/handler.go @@ -0,0 +1,13 @@ +package admin + +import ( + "gitlab.informatika.org/ocw/ocw-backend/service/admin" + "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" +) + +type AdminHandlerImpl struct { + admin.AdminService + httputil.HttpUtil + wrapper.WrapperUtil +} diff --git a/handler/admin/types.go b/handler/admin/types.go new file mode 100644 index 0000000000000000000000000000000000000000..2f30c48ef83a3af3db9f4b5f71e8cfc5653c9166 --- /dev/null +++ b/handler/admin/types.go @@ -0,0 +1,11 @@ +package admin + +import "net/http" + +type AdminHandler interface { + GetAllUser(w http.ResponseWriter, r *http.Request) + GetUserByEmail(w http.ResponseWriter, r *http.Request) + AddUser(w http.ResponseWriter, r *http.Request) + UpdateUser(w http.ResponseWriter, r *http.Request) + DeleteUser(w http.ResponseWriter, r *http.Request) +} diff --git a/handler/admin/updateUser.go b/handler/admin/updateUser.go new file mode 100644 index 0000000000000000000000000000000000000000..c6531ec04835aabca40eedf74f3f008bc27d017c --- /dev/null +++ b/handler/admin/updateUser.go @@ -0,0 +1,18 @@ +package admin + +import ( + "net/http" +) + +// Index godoc +// +// @Tags admin +// @Summary Update User By Id +// @Description Update a user from database +// @Produce json +// @Success 200 {object} web.BaseResponse +// @Router /admin/user [patch] +func (route AdminHandlerImpl) UpdateUser(w http.ResponseWriter, r *http.Request){ + payload := route.WrapperUtil.SuccessResponseWrap(route.AdminService.UpdateUser()) + route.HttpUtil.WriteSuccessJson(w, payload) +} \ No newline at end of file diff --git a/handler/auth/handler.go b/handler/auth/handler.go index 6a8f65ba49157b46c2ddde121c86c823a8966bb2..adae847f2baec21f127940794b447cffdb9824b3 100644 --- a/handler/auth/handler.go +++ b/handler/auth/handler.go @@ -2,6 +2,7 @@ package auth import ( "gitlab.informatika.org/ocw/ocw-backend/service/auth" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" ) @@ -10,4 +11,5 @@ type AuthHandlerImpl struct { auth.AuthService httputil.HttpUtil wrapper.WrapperUtil + logger.Logger } diff --git a/handler/auth/login.go b/handler/auth/login.go index ee729647365b4e536d7c3fbcf9ae2f79fb7ddcd8..3ab6f0759cde01fa545c1299fb1d2f9b056afafa 100644 --- a/handler/auth/login.go +++ b/handler/auth/login.go @@ -1,9 +1,11 @@ package auth import ( + "fmt" "net/http" "github.com/go-playground/validator/v10" + "gitlab.informatika.org/ocw/ocw-backend/model/web" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" ) @@ -15,16 +17,26 @@ import ( // @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 +// @Success 200 {object} web.BaseResponse{data=login.LoginResponsePayload} "Login Success" +// @Failure 400 {object} web.BaseResponse{data=[]string} "Bad Input" +// @Failure 403 {object} web.BaseResponse "Login Credential Error" +// @Failure 415 {object} web.BaseResponse "Not a json request" +// @Failure 422 {object} web.BaseResponse "Invalid JSON input" +// @Failure 500 {object} web.BaseResponse "Unknown Internal Error" // @Router /auth/login [post] func (a AuthHandlerImpl) Login(w http.ResponseWriter, r *http.Request) { payload := login.LoginRequestPayload{} validate := validator.New() + if r.Header.Get("Content-Type") != "application/json" { + payload := a.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + a.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + if err := a.HttpUtil.ParseJson(r, &payload); err != nil { - payload := a.WrapperUtil.ErrorResponseWrap(err.Error(), nil) - a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + payload := a.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + a.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) return } @@ -35,12 +47,8 @@ func (a AuthHandlerImpl) Login(w http.ResponseWriter, r *http.Request) { return } - errList := []string{} - for _, err := range err.(validator.ValidationErrors) { - errList = append(errList, err.Error()) - } - - payload := a.WrapperUtil.ErrorResponseWrap("input validation error", errList) + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := a.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) return } @@ -48,8 +56,22 @@ func (a AuthHandlerImpl) Login(w http.ResponseWriter, r *http.Request) { response, err := a.AuthService.Login(payload) if err != nil { - payload := a.WrapperUtil.ErrorResponseWrap(err.Error(), nil) - a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + respErr, ok := err.(web.ResponseError) + if ok { + payload := a.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr) + + if respErr.Code != web.InvalidLogin { + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + a.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + } + } else { + a.Logger.Error( + fmt.Sprintf("[AUTH] some error happened when do login: %s", err.Error()), + ) + payload := a.WrapperUtil.ErrorResponseWrap("internal server error", nil) + a.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } return } diff --git a/handler/auth/refresh.go b/handler/auth/refresh.go index bf8fda26d51162c13917bc13b1e02040532e3bb5..20eaed5d8803d078beed923cf0855b9bc394f438 100644 --- a/handler/auth/refresh.go +++ b/handler/auth/refresh.go @@ -1,9 +1,11 @@ package auth import ( + "fmt" "net/http" "strings" + "gitlab.informatika.org/ocw/ocw-backend/model/web" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/refresh" ) @@ -16,7 +18,9 @@ import ( // @Accept json // @Param Authorization header string true "Refresh token" // @Success 200 {object} web.BaseResponse{data=refresh.RefreshResponsePayload} -// @Failure 403 {object} web.BaseResponse +// @Failure 400 {object} web.BaseResponse +// @Failure 401 {object} web.BaseResponse +// @Failure 500 {object} web.BaseResponse // @Router /auth/refresh [post] func (a AuthHandlerImpl) Refresh(w http.ResponseWriter, r *http.Request) { payload := refresh.RefreshRequestPayload{} @@ -47,8 +51,17 @@ func (a AuthHandlerImpl) Refresh(w http.ResponseWriter, r *http.Request) { response, err := a.AuthService.Refresh(payload) if err != nil { - payload := a.WrapperUtil.ErrorResponseWrap(err.Error(), nil) - a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + if errData, ok := err.(web.ResponseError); ok { + payload := a.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + a.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + a.Logger.Error( + fmt.Sprintf("[AUTH] some error happened when do refresh: %s", err.Error()), + ) + payload := a.WrapperUtil.ErrorResponseWrap("internal server error", nil) + a.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) return } diff --git a/handler/auth/register.go b/handler/auth/register.go new file mode 100644 index 0000000000000000000000000000000000000000..8a24f5da071967f00ddcea7e973bd6a46230fa1b --- /dev/null +++ b/handler/auth/register.go @@ -0,0 +1,77 @@ +package auth + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-playground/validator/v10" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/register" +) + +// Index godoc +// +// @Tags auth +// @Summary Register New Account +// @Description Generate New Account as Member +// @Produce json +// @Accept json +// @Param data body register.RegisterRequestPayload true "Register Payload" +// @Success 200 {object} web.BaseResponse +// @Failure 400 {object} web.BaseResponse +// @Failure 500 {object} web.BaseResponse +// @Router /auth/register [post] +func (a AuthHandlerImpl) Register(w http.ResponseWriter, r *http.Request) { + payload := register.RegisterRequestPayload{} + validate := validator.New() + + if r.Header.Get("Content-Type") != "application/json" { + payload := a.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + a.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := a.HttpUtil.ParseJson(r, &payload); err != nil { + payload := a.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + a.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, 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 + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := a.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + err := a.AuthService.Register(payload) + + if err != nil { + if strings.Contains(err.Error(), "duplicate key") { + err = web.NewResponseErrorFromError(err, web.EmailExist) + } + + respErr, ok := err.(web.ResponseError) + if ok { + payload := a.WrapperUtil.ErrorResponseWrap("email was registered by other account", respErr) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + a.Logger.Error( + fmt.Sprintf("[AUTH] some error happened when do register: %s", err.Error()), + ) + payload := a.WrapperUtil.ErrorResponseWrap("internal server error", nil) + a.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := a.WrapperUtil.SuccessResponseWrap(nil) + a.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/auth/types.go b/handler/auth/types.go index 49aa52cee884d6fbdfb60b30b19556e1b5e18962..71b4d1eba14c50a290f9c43565da1dd707e73f29 100644 --- a/handler/auth/types.go +++ b/handler/auth/types.go @@ -4,5 +4,6 @@ import "net/http" type AuthHandler interface { Login(w http.ResponseWriter, r *http.Request) + Register(w http.ResponseWriter, r *http.Request) Refresh(w http.ResponseWriter, r *http.Request) } diff --git a/handler/di.go b/handler/di.go index b07a5541580debe9eefda7f6979670ae3dd34696..835b134c53081d8bfb44e80ddda91bb03f7f7eeb 100644 --- a/handler/di.go +++ b/handler/di.go @@ -2,8 +2,10 @@ package handler import ( "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/handler/admin" "gitlab.informatika.org/ocw/ocw-backend/handler/auth" "gitlab.informatika.org/ocw/ocw-backend/handler/common" + "gitlab.informatika.org/ocw/ocw-backend/handler/reset" "gitlab.informatika.org/ocw/ocw-backend/handler/swagger" ) @@ -15,8 +17,16 @@ var HandlerSet = wire.NewSet( // Swagger wire.Struct(new(swagger.SwaggerHandlerImpl), "*"), wire.Bind(new(swagger.SwaggerHandler), new(*swagger.SwaggerHandlerImpl)), - + + // Admin + wire.Struct(new(admin.AdminHandlerImpl), "*"), + wire.Bind(new(admin.AdminHandler), new(*admin.AdminHandlerImpl)), + // Auth wire.Struct(new(auth.AuthHandlerImpl), "*"), wire.Bind(new(auth.AuthHandler), new(*auth.AuthHandlerImpl)), + + // Reset + wire.Struct(new(reset.ResetHandlerImpl), "*"), + wire.Bind(new(reset.ResetHandler), new(*reset.ResetHandlerImpl)), ) diff --git a/handler/reset/confirm.go b/handler/reset/confirm.go new file mode 100644 index 0000000000000000000000000000000000000000..e68ba0568c58bacb594068f183d79c7d5a20f94b --- /dev/null +++ b/handler/reset/confirm.go @@ -0,0 +1,91 @@ +package reset + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-playground/validator/v10" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/confirm" +) + +func (rs ResetHandlerImpl) Confirm(w http.ResponseWriter, r *http.Request) { + payload := confirm.ConfirmRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := rs.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + rs.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := rs.HttpUtil.ParseJson(r, &payload); err != nil { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + rs.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := rs.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := rs.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + confirmTokenHeader := r.Header.Get("Authorization") + + if confirmTokenHeader == "" { + payload := rs.WrapperUtil.ErrorResponseWrap("token is required", nil) + rs.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(confirmTokenHeader, " ") + + if len(token) != 2 { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Service to handle + payload.ConfirmToken = token[1] + err := rs.ResetService.Confirm(payload) + + if err != nil { + if strings.Contains(err.Error(), "expired/not exist") { + err = web.NewResponseErrorFromError(err, web.EmailNotExist) + } + + respErr, ok := err.(web.ResponseError) + if ok { + payload := rs.WrapperUtil.ErrorResponseWrap("email was not found", respErr) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + rs.Logger.Error( + fmt.Sprintf("[RESET] some error happened when requesting password reset: %s", err.Error()), + ) + payload := rs.WrapperUtil.ErrorResponseWrap("internal server error", nil) + rs.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil) + rs.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/reset/handler.go b/handler/reset/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..b9ee5874d324580d9f1ceebeb7c296c0aa1b09a4 --- /dev/null +++ b/handler/reset/handler.go @@ -0,0 +1,15 @@ +package reset + +import ( + "gitlab.informatika.org/ocw/ocw-backend/service/reset" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" +) + +type ResetHandlerImpl struct { + reset.ResetService + httputil.HttpUtil + wrapper.WrapperUtil + logger.Logger +} diff --git a/handler/reset/request.go b/handler/reset/request.go new file mode 100644 index 0000000000000000000000000000000000000000..f2dc0d450e58ca4c26d33f574a403e3e3269c221 --- /dev/null +++ b/handler/reset/request.go @@ -0,0 +1,65 @@ +package reset + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-playground/validator/v10" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request" +) + +func (rs ResetHandlerImpl) Request(w http.ResponseWriter, r *http.Request) { + payload := request.RequestRequestPayload{} + validate := validator.New() + + if r.Header.Get("Content-Type") != "application/json" { + payload := rs.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + rs.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := rs.HttpUtil.ParseJson(r, &payload); err != nil { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + rs.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := rs.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := rs.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + err := rs.ResetService.Request(payload) + + if err != nil { + if strings.Contains(err.Error(), "unknown key") { + err = web.NewResponseErrorFromError(err, web.EmailNotExist) + } + + respErr, ok := err.(web.ResponseError) + if ok { + payload := rs.WrapperUtil.ErrorResponseWrap("email was not found", respErr) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + rs.Logger.Error( + fmt.Sprintf("[RESET] some error happened when requesting reset email: %s", err.Error()), + ) + payload := rs.WrapperUtil.ErrorResponseWrap("internal server error", nil) + rs.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil) + rs.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/reset/types.go b/handler/reset/types.go new file mode 100644 index 0000000000000000000000000000000000000000..7f0a8dbcfb276d41ad8067d6848d857535d739c4 --- /dev/null +++ b/handler/reset/types.go @@ -0,0 +1,9 @@ +package reset + +import "net/http" + +type ResetHandler interface { + Request(w http.ResponseWriter, r *http.Request) + Confirm(w http.ResponseWriter, r *http.Request) + Validate(w http.ResponseWriter, r *http.Request) +} diff --git a/handler/reset/validate.go b/handler/reset/validate.go new file mode 100644 index 0000000000000000000000000000000000000000..19d48dbfdd2e9cf42e5c874779a1f03c8db100f9 --- /dev/null +++ b/handler/reset/validate.go @@ -0,0 +1,57 @@ +package reset + +import ( + "fmt" + "net/http" + "strings" + + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate" +) + +func (rs ResetHandlerImpl) Validate(w http.ResponseWriter, r *http.Request) { + payload := validate.ValidateRequestPayload{} + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := rs.WrapperUtil.ErrorResponseWrap("token is required", nil) + rs.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ValidateToken = token[1] + err := rs.ResetService.Validate(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := rs.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + rs.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + rs.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := rs.WrapperUtil.ErrorResponseWrap("internal server error", nil) + rs.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil) + rs.HttpUtil.WriteSuccessJson(w, responsePayload) + +} \ No newline at end of file diff --git a/model/domain/user/role.go b/model/domain/user/role.go index 25e8bd6bb3b4c3bfc94085a2b54b85a86bab57e1..0b036ee36a86133115151235f152272f8fe2be91 100644 --- a/model/domain/user/role.go +++ b/model/domain/user/role.go @@ -11,18 +11,18 @@ type UserRole int const ( Admin UserRole = iota - Member + Student Contributor ) var roleMapping = map[UserRole]string{ Admin: "admin", - Member: "member", + Student: "student", Contributor: "contributor", } func (ur *UserRole) Scan(value interface{}) error { - val := string(value.([]byte)) + val := value.(string) for key, label := range roleMapping { if label == val { diff --git a/model/domain/user/user.go b/model/domain/user/user.go index 62d5e1ec20457e1f4cecf43269ca308beb5913de..f2fbb565f6dd4aa94cbf7f15b260980ead0351f9 100644 --- a/model/domain/user/user.go +++ b/model/domain/user/user.go @@ -1,11 +1,17 @@ package user +import "time" + type User struct { Email string `gorm:"primaryKey"` Password string Name string Role UserRole `gorm:"type:user_role"` IsActivated bool - CreatedAt int64 `gorm:"autoCreateTime:nano"` - UpdatedAt int64 `gorm:"autoUpdateTime:nano"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (User) TableName() string { + return "user" } diff --git a/model/web/auth/register/request.go b/model/web/auth/register/request.go new file mode 100644 index 0000000000000000000000000000000000000000..48d58105956364d445c27ea82129f2d0c27581eb --- /dev/null +++ b/model/web/auth/register/request.go @@ -0,0 +1,17 @@ +package register + +// Register Request Payload +// @Description Information that should be available when do a registration process +type RegisterRequestPayload struct { + // User Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` + + // User Password + Password string `json:"password" validate:"required" example:"secret"` + + // User Password Validation, must be same as user + PasswordValidation string `json:"password_validation" validate:"required,eqfield=Password" example:"secret"` + + // User name + Name string `json:"name" validate:"required" example:"someone"` +} diff --git a/model/web/error.go b/model/web/error.go new file mode 100644 index 0000000000000000000000000000000000000000..2ef06063a41bdb0ebbcaf077dac124ae30dac97e --- /dev/null +++ b/model/web/error.go @@ -0,0 +1,64 @@ +package web + +import ( + "fmt" + + "github.com/go-playground/validator/v10" +) + +type ResponseError struct { + Message string `json:"-"` + Code string `json:"code"` + Details interface{} `json:"details"` +} + +func (v ResponseError) Error() string { + return v.Message +} + +func NewResponseError( + message string, + code string, +) ResponseError { + return ResponseError{ + Message: message, + Code: code, + Details: []string{}, + } +} + +func NewResponseErrorFromError( + err error, + code string, +) ResponseError { + return NewResponseError(err.Error(), code) +} + +func NewResponseErrorFromValidator( + err validator.ValidationErrors, +) ResponseError { + errList := []map[string]string{} + for _, err := range err { + errList = append(errList, map[string]string{ + "field": err.Field(), + "tag": err.Tag(), + }) + } + + return ResponseError{ + Message: "input is not valid", + Code: InvalidInput, + Details: errList, + } +} + +func NewResponseErrorf( + code string, + format string, + value ...interface{}, +) ResponseError { + return NewResponseErrorFromError( + fmt.Errorf(format, value...), + code, + ) +} diff --git a/model/web/error_code.go b/model/web/error_code.go new file mode 100644 index 0000000000000000000000000000000000000000..e610d20e8224716d3e4ff4ea6daa8c66948eb093 --- /dev/null +++ b/model/web/error_code.go @@ -0,0 +1,14 @@ +package web + +const ( + InvalidInput string = "INVALID_INPUT" + + InvalidLogin string = "INVALID_LOGIN" + UnauthorizedAccess string = "UNAUTHORIZED" + InactiveUser string = "INACTIVE_ACCOUNT" + EmailExist string = "EMAIL_EXIST" + EmailNotExist string = "EMAIL_NOT_EXIST" + LinkNotAvailable string = "LINK_NOT_AVAILABLE" + + TokenError string = "TOKEN_ERROR" +) diff --git a/model/web/reset/confirm/request.go b/model/web/reset/confirm/request.go new file mode 100644 index 0000000000000000000000000000000000000000..6d864a35e49373198dfff3fe18da82094519c052 --- /dev/null +++ b/model/web/reset/confirm/request.go @@ -0,0 +1,14 @@ +package confirm + +// Confirm Request Payload +// @Description Information that should be available when you confirm a password reset +type ConfirmRequestPayload struct { + // Web Token that was appended to the link + ConfirmToken string + + // User Password + Password string `json:"password" validate:"required" example:"secret"` + + // User Password Validation, must be same as user + PasswordValidation string `json:"password_validation" validate:"required,eqfield=Password" example:"secret"` +} diff --git a/model/web/reset/request/request.go b/model/web/reset/request/request.go new file mode 100644 index 0000000000000000000000000000000000000000..cdd5d15bfbbf4cdccbe381d1af776bf58a299b36 --- /dev/null +++ b/model/web/reset/request/request.go @@ -0,0 +1,8 @@ +package request + +// Request Request Payload +// @Description Information that should be available when password reset is requested +type RequestRequestPayload struct { + // User Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` +} diff --git a/model/web/reset/validate/request.go b/model/web/reset/validate/request.go new file mode 100644 index 0000000000000000000000000000000000000000..f13973a086b4048d637a660d08c75076a5ec3378 --- /dev/null +++ b/model/web/reset/validate/request.go @@ -0,0 +1,8 @@ +package validate + +// Validate Request Payload +// @Description Information that should be available when link validation is done +type ValidateRequestPayload struct { + // Web Token that was appended to the link + ValidateToken string +} diff --git a/provider/di.go b/provider/di.go new file mode 100644 index 0000000000000000000000000000000000000000..f1ffaae3f3d010c4ea5a836222e0efdaab8b9c76 --- /dev/null +++ b/provider/di.go @@ -0,0 +1,16 @@ +package provider + +import ( + "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/provider/mail" + "gitlab.informatika.org/ocw/ocw-backend/provider/mail/smtp" +) + +var ProviderSet = wire.NewSet( + // Provider + smtp.New, + mail.NewQueue, + + wire.Bind(new(mail.MailQueue), new(*mail.MailQueueImpl)), + wire.Bind(new(mail.MailProvider), new(*smtp.SmtpMailProvider)), +) diff --git a/provider/mail/queue.go b/provider/mail/queue.go new file mode 100644 index 0000000000000000000000000000000000000000..84acd6815999beab1a5ad6fcd97781402b8c6896 --- /dev/null +++ b/provider/mail/queue.go @@ -0,0 +1,101 @@ +package mail + +import ( + "context" + "fmt" + "sync" + "time" + + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" +) + +type MailQueueImpl struct { + provider MailProvider + queue []Mail + mutex sync.Mutex + logUtil logger.Logger + env *env.Environment + isStarted bool +} + +func NewQueue( + provider MailProvider, + log logger.Logger, + env *env.Environment, +) *MailQueueImpl { + return &MailQueueImpl{ + provider: provider, + queue: []Mail{}, + mutex: sync.Mutex{}, + logUtil: log, + isStarted: false, + env: env, + } +} + +func (mq *MailQueueImpl) Send(mail Mail) { + if !mq.isStarted { + return + } + + mq.mutex.Lock() + defer mq.mutex.Unlock() + + mq.queue = append(mq.queue, mail) +} + +func (mq *MailQueueImpl) Flush() { + mq.mutex.Lock() + defer mq.mutex.Unlock() + + if len(mq.queue) == 0 { + return + } + + sendBuffer := make([]Mail, len(mq.queue)) + copy(sendBuffer, mq.queue) + + go func(buffer *[]Mail) { + for _, data := range *buffer { + err := mq.provider.Send(data.To, data.Subject, data.Message) + + if err != nil { + mq.logUtil.Error( + fmt.Sprintf("Problem with sending mail: %s", err.Error()), + ) + } + } + }(&sendBuffer) + + mq.queue = []Mail{} +} + +func (mq *MailQueueImpl) Start(ctx context.Context) { + go func() { + mq.isStarted = true + defer func() { + mq.isStarted = false + mq.logUtil.Info("🛑 Mail queue has been stopped.") + }() + + interval := time.Duration(mq.env.MailingInterval) + timer := time.NewTicker(interval * time.Millisecond) + defer timer.Stop() + + mq.logUtil.Info( + fmt.Sprintf("Mailing started to listen... (interval: %dms)", interval), + ) + + isLoop := true + + for isLoop { + select { + case <-ctx.Done(): + isLoop = false + case <-timer.C: + mq.Flush() + } + } + }() +} diff --git a/provider/mail/smtp/smtp.go b/provider/mail/smtp/smtp.go new file mode 100644 index 0000000000000000000000000000000000000000..dd2873a17083a7381fa3009679552d7dbd121590 --- /dev/null +++ b/provider/mail/smtp/smtp.go @@ -0,0 +1,44 @@ +package smtp + +import ( + "fmt" + "net/smtp" + + "gitlab.informatika.org/ocw/ocw-backend/utils/env" +) + +type SmtpMailProvider struct { + *env.Environment + smtp.Auth +} + +func New(env *env.Environment) *SmtpMailProvider { + auth := smtp.PlainAuth( + env.SmtpIdentity, + env.SmtpUsername, + env.SmtpPassword, + env.SmtpServer, + ) + + return &SmtpMailProvider{ + Environment: env, + Auth: auth, + } +} + +func (s SmtpMailProvider) Send(to []string, subject string, message string) error { + payload := fmt.Sprintf( + "To: %s\r\n"+ + "Subject: %s\r\n"+ + "\r\n%s\r\n", + to, subject, message, + ) + + return smtp.SendMail( + fmt.Sprintf("%s:%d", s.SmtpServer, s.SmtpPort), + s.Auth, + s.SmtpUsername, + to, + []byte(payload), + ) +} diff --git a/provider/mail/type.go b/provider/mail/type.go new file mode 100644 index 0000000000000000000000000000000000000000..dbd185ef281d1f2ae39d09b8e15af75bc169c76e --- /dev/null +++ b/provider/mail/type.go @@ -0,0 +1,19 @@ +package mail + +import "context" + +type Mail struct { + To []string + Subject string + Message string +} + +type MailProvider interface { + Send(to []string, subject string, message string) error +} + +type MailQueue interface { + Send(mail Mail) + Flush() + Start(ctx context.Context) +} diff --git a/repository/user/type.go b/repository/user/type.go index 047ee696b02aef9c583d3cd0ff7e18878e0f25e5..77304dccc4c5ee44ac66d1e77af7d4144970b7ed 100644 --- a/repository/user/type.go +++ b/repository/user/type.go @@ -7,6 +7,8 @@ import ( type UserRepository interface { Add(user user.User) error Get(username string) (*user.User, error) + GetAll() ([]user.User, error) Update(user user.User) error Delete(username string) error + IsExist(user string) (bool, error) } diff --git a/repository/user/user.go b/repository/user/user.go index 881a4c6953af8c7e995fd6ea9e4b0e8fb6322923..f2919855b4b118e37a4afce174f83fceef2554dd 100644 --- a/repository/user/user.go +++ b/repository/user/user.go @@ -1,6 +1,8 @@ package user import ( + "errors" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" "gitlab.informatika.org/ocw/ocw-backend/utils/db" "gorm.io/gorm" @@ -16,13 +18,38 @@ func New( return &UserRepositoryImpl{db.Connect()} } +func (repo UserRepositoryImpl) IsExist(email string) (bool, error) { + _, err := repo.Get(email) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return true, nil + } + + return true, err + } + + return false, nil +} + func (repo UserRepositoryImpl) Add(user user.User) error { - return repo.db.Create(user).Error + return repo.db.Create(&user).Error } -func (repo UserRepositoryImpl) Get(username string) (*user.User, error) { +func (repo UserRepositoryImpl) Get(email string) (*user.User, error) { result := &user.User{} - err := repo.db.Where("username = ?", username).First(result).Error + err := repo.db.Where("email = ?", email).First(result).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo UserRepositoryImpl) GetAll() ([]user.User, error) { + var result []user.User + err := repo.db.Find(&result).Error if err != nil { return nil, err diff --git a/routes/admin/route.go b/routes/admin/route.go new file mode 100644 index 0000000000000000000000000000000000000000..01f19943524626adb4aea1b73f5a912d4fdee3fa --- /dev/null +++ b/routes/admin/route.go @@ -0,0 +1,20 @@ +package admin + +import ( + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/handler/admin" +) + +type AdminRoutes struct { + admin.AdminHandler +} + +func (adr AdminRoutes) Register(r chi.Router) { + r.Route("/admin", func(r chi.Router) { + r.Get("/user", adr.AdminHandler.GetAllUser) + r.Get("/user/{id}", adr.AdminHandler.GetUserByEmail) + r.Post("/user", adr.AdminHandler.AddUser) + r.Patch("/user/{id}", adr.AdminHandler.UpdateUser) + r.Delete("/user/{id}", adr.AdminHandler.DeleteUser) + }) +} diff --git a/routes/auth/route.go b/routes/auth/route.go index 55377de3603255eb0ab5ba3d0950dd8d5bfbcb4a..d7ea811b69fd604111f9a013b7821a10ef005619 100644 --- a/routes/auth/route.go +++ b/routes/auth/route.go @@ -13,5 +13,6 @@ 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) + r.Post("/register", ar.AuthHandler.Register) }) } diff --git a/routes/di.go b/routes/di.go index ad350ce824b581f436f168d7c2f2c38882e1c41f..b44b2ed0ae71dda868755ffdf82ecf3c636a7a1e 100644 --- a/routes/di.go +++ b/routes/di.go @@ -2,8 +2,10 @@ package routes import ( "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/routes/admin" "gitlab.informatika.org/ocw/ocw-backend/routes/auth" "gitlab.informatika.org/ocw/ocw-backend/routes/common" + "gitlab.informatika.org/ocw/ocw-backend/routes/reset" "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" ) @@ -11,6 +13,8 @@ var routesCollectionSet = wire.NewSet( wire.Struct(new(common.CommonRoutes), "*"), wire.Struct(new(swagger.SwaggerRoutes), "*"), wire.Struct(new(auth.AuthRoutes), "*"), + wire.Struct(new(admin.AdminRoutes), "*"), + wire.Struct(new(reset.ResetRoutes), "*"), ) var RoutesSet = wire.NewSet( diff --git a/routes/reset/route.go b/routes/reset/route.go new file mode 100644 index 0000000000000000000000000000000000000000..46b32e0f9f389bfe8f3ff730d2c683225dfcee91 --- /dev/null +++ b/routes/reset/route.go @@ -0,0 +1,18 @@ +package reset + +import ( + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/handler/reset" +) + +type ResetRoutes struct { + reset.ResetHandler +} + +func (rr ResetRoutes) Register(r chi.Router) { + r.Route("/reset", func(r chi.Router) { + r.Post("/request", rr.ResetHandler.Request) + r.Post("/confirm", rr.ResetHandler.Confirm) + r.Post("/validate", rr.ResetHandler.Validate) + }) +} diff --git a/routes/routes.go b/routes/routes.go index c824d2db4b82e25a80bff0bddf83f3bf30905c35..4c358d7a37e29d0c2e62fb603f5d5aa70469f061 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -1,8 +1,10 @@ package routes import ( + "gitlab.informatika.org/ocw/ocw-backend/routes/admin" "gitlab.informatika.org/ocw/ocw-backend/routes/auth" "gitlab.informatika.org/ocw/ocw-backend/routes/common" + "gitlab.informatika.org/ocw/ocw-backend/routes/reset" "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" "gitlab.informatika.org/ocw/ocw-backend/service/logger" @@ -11,8 +13,10 @@ import ( type AppRouter struct { // Routes swagger.SwaggerRoutes + admin.AdminRoutes common.CommonRoutes auth.AuthRoutes + reset.ResetRoutes // Utility Logger logger.Logger diff --git a/service/admin/addUser.go b/service/admin/addUser.go new file mode 100644 index 0000000000000000000000000000000000000000..644bde9d0547485f4eb3ef45a4876d12327550c6 --- /dev/null +++ b/service/admin/addUser.go @@ -0,0 +1,16 @@ +package admin + +// import ( + // "errors" + // "time" + + // "github.com/golang-jwt/jwt/v4" + // "gitlab.informatika.org/ocw/ocw-backend/model/web" + // "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 (AdminServiceImpl) AddUser() string { + return "add user" +} \ No newline at end of file diff --git a/service/admin/deleteUser.go b/service/admin/deleteUser.go new file mode 100644 index 0000000000000000000000000000000000000000..0e34cf7227863d1865c5de527b1bfa12806c8573 --- /dev/null +++ b/service/admin/deleteUser.go @@ -0,0 +1,16 @@ +package admin + +// import ( + // "errors" + // "time" + + // "github.com/golang-jwt/jwt/v4" + // "gitlab.informatika.org/ocw/ocw-backend/model/web" + // "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 (AdminServiceImpl) DeleteUser() string { + return "delete user" +} diff --git a/service/admin/getAllUser.go b/service/admin/getAllUser.go new file mode 100644 index 0000000000000000000000000000000000000000..0977c267a8cbd463bc359a8b496105690cead99e --- /dev/null +++ b/service/admin/getAllUser.go @@ -0,0 +1,11 @@ +package admin + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" +) + +func (as AdminServiceImpl) GetAllUser() ([]user.User, error) { + var users []user.User + users, nil := as.UserRepository.GetAll() + return users, nil +} \ No newline at end of file diff --git a/service/admin/getUserByEmail.go b/service/admin/getUserByEmail.go new file mode 100644 index 0000000000000000000000000000000000000000..e577536e5acccc895a4861a6845d9cd3b13018d8 --- /dev/null +++ b/service/admin/getUserByEmail.go @@ -0,0 +1,16 @@ +package admin + +// import ( + // "errors" + // "time" + + // "github.com/golang-jwt/jwt/v4" + // "gitlab.informatika.org/ocw/ocw-backend/model/web" + // "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 (AdminServiceImpl) GetUserByEmail() string { + return "get user by email" +} \ No newline at end of file diff --git a/service/admin/impl.go b/service/admin/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..7bc0dd3364dc3f46e47d282c647bb26380d4a31c --- /dev/null +++ b/service/admin/impl.go @@ -0,0 +1,9 @@ +package admin + +import ( + "gitlab.informatika.org/ocw/ocw-backend/repository/user" +) + +type AdminServiceImpl struct { + UserRepository user.UserRepository +} \ No newline at end of file diff --git a/service/admin/type.go b/service/admin/type.go new file mode 100644 index 0000000000000000000000000000000000000000..d624c56274802d9fc9523aa092d21f1fed14a314 --- /dev/null +++ b/service/admin/type.go @@ -0,0 +1,13 @@ +package admin + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" +) + +type AdminService interface { + GetAllUser() ([]user.User, error) + GetUserByEmail() string + AddUser() string + UpdateUser() string + DeleteUser() string +} diff --git a/service/admin/updateUser.go b/service/admin/updateUser.go new file mode 100644 index 0000000000000000000000000000000000000000..e3bce672ae924615b9eb4df9ebda6d12c8afad00 --- /dev/null +++ b/service/admin/updateUser.go @@ -0,0 +1,16 @@ +package admin + +// import ( + // "errors" + // "time" + + // "github.com/golang-jwt/jwt/v4" + // "gitlab.informatika.org/ocw/ocw-backend/model/web" + // "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 (AdminServiceImpl) UpdateUser() string { + return "update user" +} \ No newline at end of file diff --git a/service/auth/impl.go b/service/auth/impl.go index d8ddef7c19cfb3dcd80116be650c34a3d5b6256e..9ac5f343fb68b92e44a3a51491389a21d5c70c5a 100644 --- a/service/auth/impl.go +++ b/service/auth/impl.go @@ -2,6 +2,7 @@ package auth import ( "gitlab.informatika.org/ocw/ocw-backend/repository/user" + "gitlab.informatika.org/ocw/ocw-backend/service/verification" "gitlab.informatika.org/ocw/ocw-backend/utils/env" "gitlab.informatika.org/ocw/ocw-backend/utils/password" "gitlab.informatika.org/ocw/ocw-backend/utils/token" @@ -10,6 +11,7 @@ import ( type AuthServiceImpl struct { user.UserRepository password.PasswordUtil - env.Environment + *env.Environment token.TokenUtil + verification.VerificationService } diff --git a/service/auth/login.go b/service/auth/login.go index 0ed6024ef98e633119cabaaacac98709e875d155..33d52237aec5b52a8a56796b5c410f54e0fe6189 100644 --- a/service/auth/login.go +++ b/service/auth/login.go @@ -2,24 +2,24 @@ package auth import ( "errors" - "fmt" "time" "github.com/golang-jwt/jwt/v4" + "gitlab.informatika.org/ocw/ocw-backend/model/web" "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) + user, err := auth.UserRepository.Get(payload.Email) if err != nil { var errorObj error switch { case errors.Is(err, gorm.ErrRecordNotFound): - errorObj = fmt.Errorf("username and password combination not found") + errorObj = web.NewResponseError("username and password combination not found", web.InvalidLogin) default: errorObj = err } @@ -28,11 +28,11 @@ func (auth AuthServiceImpl) Login(payload login.LoginRequestPayload) (*login.Log } if err := auth.Check(payload.Password, user.Password); err != nil { - return nil, fmt.Errorf("username and password combination not found") + return nil, web.NewResponseError("username and password combination not found", web.InvalidLogin) } if !user.IsActivated { - return nil, fmt.Errorf("user is not activated yet") + return nil, web.NewResponseError("user is not activated yet", web.InactiveUser) } refreshClaim := tokenModel.UserClaim{ @@ -59,14 +59,12 @@ func (auth AuthServiceImpl) Login(payload login.LoginRequestPayload) (*login.Log }, } - refreshToken, err := auth.TokenUtil.Generate(refreshClaim) - + refreshToken, err := auth.TokenUtil.Generate(refreshClaim, auth.TokenUtil.DefaultMethod()) if err != nil { return nil, err } - accessToken, err := auth.TokenUtil.Generate(accessClaim) - + accessToken, err := auth.TokenUtil.Generate(accessClaim, auth.TokenUtil.DefaultMethod()) if err != nil { return nil, err } diff --git a/service/auth/refresh.go b/service/auth/refresh.go index 70aecd6488fc5d569a07cd7c9c5ab1cb9f6337a7..02306777bf36ffa80375d9a0150a9199a3d62b6e 100644 --- a/service/auth/refresh.go +++ b/service/auth/refresh.go @@ -4,6 +4,7 @@ import ( "time" "github.com/golang-jwt/jwt/v4" + "gitlab.informatika.org/ocw/ocw-backend/model/web" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/refresh" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" ) @@ -12,13 +13,13 @@ func (auth AuthServiceImpl) Refresh(payload refresh.RefreshRequestPayload) (*ref claim, err := auth.TokenUtil.Validate(payload.RefreshToken, token.Refresh) if err != nil { - return nil, err + return nil, web.NewResponseErrorFromError(err, web.TokenError) } claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Duration(auth.TokenAccessExpired) * time.Millisecond)) claim.Type = token.Access - newToken, err := auth.TokenUtil.Generate(*claim) + newToken, err := auth.TokenUtil.Generate(*claim, auth.TokenUtil.DefaultMethod()) if err != nil { return nil, err diff --git a/service/auth/register.go b/service/auth/register.go new file mode 100644 index 0000000000000000000000000000000000000000..6eb423b39900bb2502234c2558b0414a4e50f10c --- /dev/null +++ b/service/auth/register.go @@ -0,0 +1,22 @@ +package auth + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/register" +) + +func (auth AuthServiceImpl) Register(payload register.RegisterRequestPayload) error { + err := auth.UserRepository.Add(user.User{ + Email: payload.Email, + Password: payload.Password, + Name: payload.Name, + Role: user.Student, + IsActivated: false, + }) + + if err == nil { + auth.SendVerifyMail(payload.Email) + } + + return err +} diff --git a/service/auth/type.go b/service/auth/type.go index 87a4615ba223ae43031005df40950cf550db5fcb..2503726fe0562e9b2b26544dabae60017d13c386 100644 --- a/service/auth/type.go +++ b/service/auth/type.go @@ -3,9 +3,11 @@ package auth import ( "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/refresh" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/register" ) type AuthService interface { Login(payload login.LoginRequestPayload) (*login.LoginResponsePayload, error) Refresh(payload refresh.RefreshRequestPayload) (*refresh.RefreshResponsePayload, error) + Register(payload register.RegisterRequestPayload) error } diff --git a/service/di.go b/service/di.go index b2eb205fb0c4b678f4846fb09d680eb2a0306d0b..763b0502928cbdacbe020c1c3d1e4ba64e590989 100644 --- a/service/di.go +++ b/service/di.go @@ -3,10 +3,13 @@ package service import ( "github.com/google/wire" "gitlab.informatika.org/ocw/ocw-backend/service/auth" + "gitlab.informatika.org/ocw/ocw-backend/service/admin" "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" "gitlab.informatika.org/ocw/ocw-backend/service/reporter" + "gitlab.informatika.org/ocw/ocw-backend/service/reset" + "gitlab.informatika.org/ocw/ocw-backend/service/verification" ) var ServiceTestSet = wire.NewSet( @@ -24,9 +27,27 @@ var ServiceTestSet = wire.NewSet( // auth service wire.NewSet( - wire.Struct(new(auth.AuthServiceImpl)), + wire.Struct(new(auth.AuthServiceImpl), "*"), wire.Bind(new(auth.AuthService), new(*auth.AuthServiceImpl)), ), + + // admin service + wire.NewSet( + wire.Struct(new(admin.AdminServiceImpl), "*"), + wire.Bind(new(admin.AdminService), new(*admin.AdminServiceImpl)), + ), + + // reset service + wire.NewSet( + wire.Struct(new(reset.ResetServiceImpl), "*"), + wire.Bind(new(reset.ResetService), new(*reset.ResetServiceImpl)), + ), + + // verification service + wire.NewSet( + wire.Struct(new(verification.VerificationServiceImpl), "*"), + wire.Bind(new(verification.VerificationService), new(*verification.VerificationServiceImpl)), + ), ) var ServiceSet = wire.NewSet( diff --git a/service/reporter/logtail.go b/service/reporter/logtail.go index db034e18f5b0f33cea3583cb78a35b198c9882e7..50d58d90d0172f84426acbf3cc51c1ca3efc67a1 100644 --- a/service/reporter/logtail.go +++ b/service/reporter/logtail.go @@ -134,7 +134,16 @@ func (l *LogtailReporter) Start(ctx context.Context) { go func() { l.isStarted = true - defer func() { l.isStarted = false }() + defer func() { + l.isStarted = false + + l.logUtil.PrintFormattedOutput( + "Reporter has been stopped.", + "Report", + "info", + log.ForeGreen, + ) + }() defer l.Flush() interval := time.Duration(l.env.LogFlushInterval) @@ -148,10 +157,12 @@ func (l *LogtailReporter) Start(ctx context.Context) { log.ForeGreen, ) - for { + isLoop := true + + for isLoop { select { case <-ctx.Done(): - break + isLoop = false case <-timer.C: l.Flush() } diff --git a/service/reset/confirm.go b/service/reset/confirm.go new file mode 100644 index 0000000000000000000000000000000000000000..426af52f5514eec16fd0a6c4553bd817974177fd --- /dev/null +++ b/service/reset/confirm.go @@ -0,0 +1,11 @@ +package reset + +import ( + // "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/confirm" +) + +func (rs ResetServiceImpl) Confirm(payload confirm.ConfirmRequestPayload) error { + // TODO replace dummy + return nil +} \ No newline at end of file diff --git a/service/reset/impl.go b/service/reset/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..b9986e2509f4af7f8c0deb9053bf8d30640e4168 --- /dev/null +++ b/service/reset/impl.go @@ -0,0 +1,17 @@ +package reset + +import ( + "gitlab.informatika.org/ocw/ocw-backend/repository/user" + "gitlab.informatika.org/ocw/ocw-backend/service/verification" + "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 ResetServiceImpl struct { + user.UserRepository + password.PasswordUtil + *env.Environment + token.TokenUtil + verification.VerificationService +} diff --git a/service/reset/request.go b/service/reset/request.go new file mode 100644 index 0000000000000000000000000000000000000000..05584e6a9bb3dcf0a7a78c4ec5e12e1c4502732d --- /dev/null +++ b/service/reset/request.go @@ -0,0 +1,11 @@ +package reset + +import ( + // "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request" +) + +func (rs ResetServiceImpl) Request(payload request.RequestRequestPayload) error { + // TODO replace dummy + return nil +} \ No newline at end of file diff --git a/service/reset/type.go b/service/reset/type.go new file mode 100644 index 0000000000000000000000000000000000000000..f1658d2fa25575421a24bcf7fa8ac331a88b5dbe --- /dev/null +++ b/service/reset/type.go @@ -0,0 +1,13 @@ +package reset + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/confirm" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate" +) + +type ResetService interface { + Request(payload request.RequestRequestPayload) error + Confirm(payload confirm.ConfirmRequestPayload) error + Validate(payload validate.ValidateRequestPayload) error +} diff --git a/service/reset/validate.go b/service/reset/validate.go new file mode 100644 index 0000000000000000000000000000000000000000..7cd09fe9bc83b23a2e4d0bbc0abf95fe1758b5cc --- /dev/null +++ b/service/reset/validate.go @@ -0,0 +1,11 @@ +package reset + +import ( + // "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate" +) + +func (rs ResetServiceImpl) Validate(payload validate.ValidateRequestPayload) error { + // TODO replace dummy + return nil +} \ No newline at end of file diff --git a/service/verification/impl.go b/service/verification/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..94db17485a4173f2ad820bbc100685d06f00d9b5 --- /dev/null +++ b/service/verification/impl.go @@ -0,0 +1,11 @@ +package verification + +import ( + "gitlab.informatika.org/ocw/ocw-backend/provider/mail" + "gitlab.informatika.org/ocw/ocw-backend/repository/user" +) + +type VerificationServiceImpl struct { + mail.MailQueue + user.UserRepository +} diff --git a/service/verification/send.go b/service/verification/send.go new file mode 100644 index 0000000000000000000000000000000000000000..9f404a803958d11c9f10a121ceacfc7aa87318d6 --- /dev/null +++ b/service/verification/send.go @@ -0,0 +1,6 @@ +package verification + +func (v VerificationServiceImpl) SendVerifyMail(email string) error { + // TODO + return nil +} diff --git a/service/verification/type.go b/service/verification/type.go new file mode 100644 index 0000000000000000000000000000000000000000..e6df1d1939e37be6abf399db52d68632e0924a4c --- /dev/null +++ b/service/verification/type.go @@ -0,0 +1,6 @@ +package verification + +type VerificationService interface { + SendVerifyMail(email string) error + SetVerification(email string, isVerified bool) error +} diff --git a/service/verification/verify.go b/service/verification/verify.go new file mode 100644 index 0000000000000000000000000000000000000000..b18ce1ecaa19fd332e495f81a8377b814a010246 --- /dev/null +++ b/service/verification/verify.go @@ -0,0 +1,6 @@ +package verification + +func (v VerificationServiceImpl) SetVerification(email string, isVerified bool) error { + // TODO + return nil +} diff --git a/test/api.go b/test/api.go new file mode 100644 index 0000000000000000000000000000000000000000..b67503d1f08ec5f0a09053ad8741a638a9a226bb --- /dev/null +++ b/test/api.go @@ -0,0 +1,11 @@ +package test + +import ( + "gitlab.informatika.org/ocw/ocw-backend/test/db" + "gitlab.informatika.org/ocw/ocw-backend/utils/app" +) + +type ApiTestPack struct { + *db.MockDatabase + app.Server +} diff --git a/test/db/di.go b/test/db/di.go new file mode 100644 index 0000000000000000000000000000000000000000..e1a84e965cb97818bb3645a349eded41c0734c2a --- /dev/null +++ b/test/db/di.go @@ -0,0 +1,11 @@ +package db + +import ( + "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/utils/db" +) + +var DbTestSet = wire.NewSet( + New, + wire.Bind(new(db.Database), new(*MockDatabase)), +) diff --git a/test/db/mock.go b/test/db/mock.go new file mode 100644 index 0000000000000000000000000000000000000000..8e86aa035eb1cc7078b386bac144c31a3640405e --- /dev/null +++ b/test/db/mock.go @@ -0,0 +1,32 @@ +package db + +import ( + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type MockDatabase struct { + db *gorm.DB + Mock sqlmock.Sqlmock +} + +func New() (*MockDatabase, error) { + db, mock, err := sqlmock.New() + + if err != nil { + return nil, err + } + + g, err := gorm.Open(postgres.New( + postgres.Config{ + Conn: db, + }, + ), &gorm.Config{}) + + return &MockDatabase{g, mock}, err +} + +func (m MockDatabase) Connect() *gorm.DB { + return m.db +} diff --git a/test/di.go b/test/di.go index 9030d589cf278b3c0460d7c60045eb1c07fbc1be..46c16536d7737ad0f00d0cfc4cfb4f99f0b75e07 100644 --- a/test/di.go +++ b/test/di.go @@ -8,22 +8,29 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/handler" "gitlab.informatika.org/ocw/ocw-backend/middleware" + "gitlab.informatika.org/ocw/ocw-backend/provider" + "gitlab.informatika.org/ocw/ocw-backend/repository" "gitlab.informatika.org/ocw/ocw-backend/routes" "gitlab.informatika.org/ocw/ocw-backend/service" "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/test/db" "gitlab.informatika.org/ocw/ocw-backend/utils" - "gitlab.informatika.org/ocw/ocw-backend/utils/app" "gitlab.informatika.org/ocw/ocw-backend/utils/env" ) -func CreateServer(logger logger.Logger, envTest *env.Environment) (app.Server, error) { +func CreateServer(logger logger.Logger, envTest *env.Environment) (*ApiTestPack, error) { wire.Build( + wire.Struct(new(ApiTestPack), "*"), + utils.UtilSetTest, + repository.RepositoryBasicSet, handler.HandlerSet, middleware.MiddlewareSet, routes.RoutesSet, service.ServiceTestSet, + db.DbTestSet, + provider.ProviderSet, ) return nil, nil diff --git a/test/utils/base.go b/test/utils/base.go index 4a75e7745250daf15dc3ffdfbce8f83c22527fdc..e744a1ff61ef13b5842d21561d746430e6f295f1 100644 --- a/test/utils/base.go +++ b/test/utils/base.go @@ -4,19 +4,20 @@ import ( "net/http" "gitlab.informatika.org/ocw/ocw-backend/test" + "gitlab.informatika.org/ocw/ocw-backend/test/db" "gitlab.informatika.org/ocw/ocw-backend/utils/env" ) -func NewTestHandler() (http.Handler, *MockLogger, error) { +func NewTestHandler() (http.Handler, *MockLogger, *db.MockDatabase, error) { logger := NewMockLogger() handler, err := test.CreateServer(logger, &env.Environment{ AppEnvironment: "DEVELOPMENT", }) if err != nil { - return nil, nil, err + return nil, nil, nil, err } logger.CleanLog() - return handler.GetServer(), logger, nil + return handler.GetServer(), logger, handler.MockDatabase, nil } diff --git a/test/utils/executor.go b/test/utils/executor.go index 1b4fda663f3e7372703dd1f7107878f9bbaaf8a3..ced02a1a5ca17d6578e2a8a51ccde8f31cdfac87 100644 --- a/test/utils/executor.go +++ b/test/utils/executor.go @@ -15,7 +15,7 @@ type RequestData struct { } func ExecuteJSON(reqData RequestData) (*http.Response, *MockLogger, error) { - r, log, err := NewTestHandler() + r, log, _, err := NewTestHandler() if err != nil { return nil, nil, err @@ -50,3 +50,32 @@ func ExecuteJSON(reqData RequestData) (*http.Response, *MockLogger, error) { return res, log, nil } + +func ExecuteJSONWithHandler(r http.Handler, reqData RequestData) (*http.Response, error) { + var req *http.Request + + if reqData.Data == nil { + req = httptest.NewRequest(reqData.Method, reqData.Endpoint, nil) + } else { + byteData, err := json.Marshal(reqData.Data) + + if err != nil { + return nil, err + } + + reader := bytes.NewReader(byteData) + req = httptest.NewRequest(reqData.Method, reqData.Endpoint, reader) + } + + if reqData.Headers != nil { + for key, value := range reqData.Headers { + req.Header.Add(key, value) + } + } + + rec := httptest.NewRecorder() + + r.ServeHTTP(rec, req) + + return rec.Result(), nil +} diff --git a/test/utils/password/password_test.go b/test/utils/password/password_test.go index e7205b89ea1532365cda6a3814d4bd43ed6f510b..3ffa6e81349941d9532eaa4231ebc4a89db917f9 100644 --- a/test/utils/password/password_test.go +++ b/test/utils/password/password_test.go @@ -4,11 +4,16 @@ import ( "testing" "github.com/stretchr/testify/assert" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" "gitlab.informatika.org/ocw/ocw-backend/utils/password" ) func TestPasswordHash(t *testing.T) { - obj := password.PasswordUtilImpl{} + obj := password.PasswordUtilImpl{ + Environment: &env.Environment{ + PasswordCost: 10, + }, + } t.Run("PasswordCanBeHashed", func(t *testing.T) { _, err := obj.Hash("admin") @@ -16,9 +21,18 @@ func TestPasswordHash(t *testing.T) { assert.Nil(t, err) }) + t.Run("PasswordHashMustBeDifferOnSamePass", func(t *testing.T) { + hash1, err := obj.Hash("admin") + assert.Nil(t, err) + + hash2, err := obj.Hash("admin") + assert.Nil(t, err) + + assert.NotEqual(t, hash1, hash2) + }) + t.Run("PasswordCanBeHashAndValidateCorrectly", func(t *testing.T) { hash, err := obj.Hash("admin") - assert.Nil(t, err) err = obj.Check("admin", hash) diff --git a/test/utils/token/token_test.go b/test/utils/token/token_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5f950ba6534563a7f3f27a740e55fd40a107c09a --- /dev/null +++ b/test/utils/token/token_test.go @@ -0,0 +1,77 @@ +package token_test + +import ( + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/stretchr/testify/assert" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + tokenData "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/token" +) + +func TestToken(t *testing.T) { + tokenObj := token.TokenUtilImpl{ + Environment: &env.Environment{ + TokenSecret: "secret", + TokenMethod: "hs512", + }, + } + + t.Run("UserTokenTest", func(t *testing.T) { + claim := tokenData.UserClaim{ + Name: "Someone", + Email: "someone@example.com", + Role: user.Student, + Type: tokenData.Refresh, + } + + token, err := tokenObj.Generate(claim, tokenObj.DefaultMethod()) + assert.Nil(t, err) + + extractedToken, err := tokenObj.Validate(token, tokenData.Refresh) + assert.Nil(t, err) + assert.NotNil(t, extractedToken) + + assert.Equal(t, claim, *extractedToken) + }) + + t.Run("UserTokenInvalidType", func(t *testing.T) { + claim := tokenData.UserClaim{ + Name: "Someone", + Email: "someone@example.com", + Role: user.Student, + Type: tokenData.Refresh, + } + + token, err := tokenObj.Generate(claim, tokenObj.DefaultMethod()) + assert.Nil(t, err) + + extractedToken, err := tokenObj.Validate(token, tokenData.Access) + assert.NotNil(t, err) + assert.Nil(t, extractedToken) + assert.Equal(t, err.Error(), "token type is not valid") + }) + + t.Run("UserTokenExpired", func(t *testing.T) { + claim := tokenData.UserClaim{ + Name: "Someone", + Email: "someone@example.com", + Role: user.Student, + Type: tokenData.Refresh, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now()), + }, + } + + token, err := tokenObj.Generate(claim, tokenObj.DefaultMethod()) + assert.Nil(t, err) + + extractedToken, err := tokenObj.Validate(token, tokenData.Refresh) + assert.NotNil(t, err) + assert.Nil(t, extractedToken) + assert.Contains(t, err.Error(), "expired") + }) +} diff --git a/utils/app/app.go b/utils/app/app.go index 03fb68309608095773735f47cfb51ec05bd5ad29..fe944504503b0d188f8734d7af924282c8018b7a 100644 --- a/utils/app/app.go +++ b/utils/app/app.go @@ -3,6 +3,7 @@ package app import ( "github.com/go-chi/chi/v5" "gitlab.informatika.org/ocw/ocw-backend/middleware" + "gitlab.informatika.org/ocw/ocw-backend/provider/mail" "gitlab.informatika.org/ocw/ocw-backend/routes" "gitlab.informatika.org/ocw/ocw-backend/service/logger" "gitlab.informatika.org/ocw/ocw-backend/service/reporter" @@ -18,6 +19,7 @@ type HttpServer struct { res res.Resource env *env.Environment reporter reporter.Reporter + mail mail.MailQueue middlewaresName []string } @@ -29,6 +31,7 @@ func New( logUtil log.LogUtils, res res.Resource, reporter reporter.Reporter, + mailqueue mail.MailQueue, ) *HttpServer { r := chi.NewRouter() @@ -49,5 +52,6 @@ func New( env: env, reporter: reporter, middlewaresName: middlewareName, + mail: mailqueue, } } diff --git a/utils/app/start.go b/utils/app/start.go index 1ea8ecd578c8aa85178ec5e9b2e18c50b566d10b..239f31530d1475a671b677e570849d911750332a 100644 --- a/utils/app/start.go +++ b/utils/app/start.go @@ -23,6 +23,7 @@ func (l *HttpServer) Start() { serverCtx, cancelServer := context.WithCancel(context.Background()) l.reporter.Start(serverCtx) + l.mail.Start(serverCtx) sig := make(chan os.Signal, 3) signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) diff --git a/utils/db/database.go b/utils/db/database.go index cf7545443c36da981607f993b2afd9b39af051d7..66f170bd385d3cc89d2ccefc9a243eac7def74cf 100644 --- a/utils/db/database.go +++ b/utils/db/database.go @@ -1,8 +1,12 @@ package db import ( - "database/sql" + "fmt" + "os" + "runtime/debug" + "strings" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" "gitlab.informatika.org/ocw/ocw-backend/utils/env" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -12,23 +16,29 @@ type DatabaseImpl struct { DB *gorm.DB } -func NewPostgresConn( - conn *sql.Conn, -) (*DatabaseImpl, error) { - res, err := gorm.Open(postgres.New(postgres.Config{ - Conn: conn, - }), &gorm.Config{}) +func resolver(log logger.Logger) { + if rec := recover(); rec != nil { + log.Error("Some panic occured when processing request:") + log.Error(fmt.Sprint(rec)) + log.Error("") - if err != nil { - return nil, err - } + log.Error("Stack Trace:") + stacks := strings.Split(string(debug.Stack()), "\n") - return &DatabaseImpl{res}, nil + for _, val := range stacks { + log.Error(val) + } + + os.Exit(-1) + } } func NewPostgresEnv( - env env.Environment, + env *env.Environment, + log logger.Logger, ) (*DatabaseImpl, error) { + defer resolver(log) + res, err := gorm.Open(postgres.Open(env.DatabaseConnection), &gorm.Config{}) if err != nil { diff --git a/utils/di.go b/utils/di.go index 5b8ac4d17ddce29b51d5b29cf86923f76d0abb51..83aae428e68d27093cd90e32c4c1e9dbce765c59 100644 --- a/utils/di.go +++ b/utils/di.go @@ -14,10 +14,6 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" ) -var DatabaseTestingSet = wire.NewSet( - db.NewPostgresConn, -) - var UtilSetTest = wire.NewSet( // httputil utility wire.Struct(new(httputil.HttpUtilImpl), "*"), @@ -53,12 +49,12 @@ var UtilSetTest = wire.NewSet( ) var UtilSet = wire.NewSet( + // env + env.New, + UtilSetTest, // Database utility wire.Bind(new(db.Database), new(*db.DatabaseImpl)), db.NewPostgresEnv, - - // env - env.New, ) diff --git a/utils/env/env.go b/utils/env/env.go index 1112fb6d6706e4b17edceac36b75c03d420db32b..a55416e52bf462568666b3b4de1afc886d97fa44 100644 --- a/utils/env/env.go +++ b/utils/env/env.go @@ -27,6 +27,15 @@ type Environment struct { 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"` + + MailingProvider string `env:"MAIL_PROVIDER" envDefault:"smtp"` + MailingInterval int64 `env:"MAIL_INTERVAL_MS" envDefault:"1000"` + + SmtpIdentity string `env:"SMTP_IDENTITY"` + SmtpUsername string `env:"SMTP_USERNAME"` + SmtpPassword string `env:"SMTP_PASSWORD"` + SmtpServer string `env:"SMTP_SERVER"` + SmtpPort int `env:"SMTP_PORT" envDefault:"25"` } func New() (*Environment, error) { diff --git a/utils/password/impl.go b/utils/password/impl.go index 9a660b472041db250ca4f6664ebb461db0560f39..63a6ed20648fa01054660b811adb1af7822a416e 100644 --- a/utils/password/impl.go +++ b/utils/password/impl.go @@ -3,14 +3,12 @@ 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 + *env.Environment } func (e PasswordUtilImpl) Hash(password string) (string, error) { diff --git a/utils/token/impl.go b/utils/token/impl.go index 9dde3aa95fc02d9be6e1e968ce62f0178da26080..3f026c5de86d524fd60f113b55feac678ff3cfee 100644 --- a/utils/token/impl.go +++ b/utils/token/impl.go @@ -9,10 +9,10 @@ import ( ) type TokenUtilImpl struct { - env.Environment + *env.Environment } -func (t TokenUtilImpl) Method() jwt.SigningMethod { +func (t TokenUtilImpl) DefaultMethod() jwt.SigningMethod { switch t.TokenMethod { case "hs256": return jwt.SigningMethodHS256 @@ -22,32 +22,36 @@ func (t TokenUtilImpl) Method() jwt.SigningMethod { } func (tu TokenUtilImpl) Validate(tokenString string, tokenType token.TokenType) (*token.UserClaim, error) { - jwtData, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { + jwtData, err := jwt.ParseWithClaims(tokenString, &token.UserClaim{}, 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() { + } else if method != tu.DefaultMethod() { return nil, fmt.Errorf("invalid signing method") } - return tu.Method(), nil + return []byte(tu.Environment.TokenSecret), nil }) if err != nil { return nil, err } - claims := jwtData.Claims.(*token.UserClaim) + claims, ok := jwtData.Claims.(*token.UserClaim) + + if !ok { + return nil, fmt.Errorf("invalid claim") + } if claims.Type != tokenType { - return claims, fmt.Errorf("token type is not valid") + return nil, fmt.Errorf("token type is not valid") } return claims, nil } -func (t TokenUtilImpl) Generate(claim token.UserClaim) (string, error) { +func (t TokenUtilImpl) Generate(claim token.UserClaim, method jwt.SigningMethod) (string, error) { token := jwt.NewWithClaims( - jwt.SigningMethodHS512, + method, claim, ) diff --git a/utils/token/type.go b/utils/token/type.go index cd6c27a2469bf7357953a88744a7609e307e02a2..d338aad223926ef45a93871ee0c61b570f2c705f 100644 --- a/utils/token/type.go +++ b/utils/token/type.go @@ -1,10 +1,12 @@ package token import ( + "github.com/golang-jwt/jwt/v4" "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) + Generate(claim token.UserClaim, method jwt.SigningMethod) (string, error) + DefaultMethod() jwt.SigningMethod }