diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..0c241635b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+README.md
+.vscode
+db
\ No newline at end of file
diff --git a/.env b/.env
new file mode 100644
index 000000000..0dc5a0d1b
--- /dev/null
+++ b/.env
@@ -0,0 +1,6 @@
+PORT=3000
+CONNECT_STR=postgres://postgres:manabie@database:5432/togo?sslmode=disable
+SECRET_TOKEN=token
+POSTGRES_PASSWORD=manabie
+POSTGRES_DB=togo
+CONNECT_STR_FOR_TEST=postgres://postgres:manabie@localhost:2345/togo?sslmode=disable
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..495ebf67a
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,40 @@
+{
+ // 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": "Attach to Process",
+ "type": "go",
+ "request": "attach",
+ "mode": "local",
+ "processId": 0
+ },
+
+ {
+ "name": "Launch file",
+ "type": "go",
+ "request": "launch",
+ "mode": "debug",
+ "program": "${file}"
+ },
+ {
+ "name": "Connect to server",
+ "type": "go",
+ "request": "attach",
+ "mode": "remote",
+ "remotePath": "${workspaceFolder}",
+ "port": 2345,
+ "host": "127.0.0.1"
+ },
+ {
+ "name": "Launch Package",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceRoot}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..a2208ca38
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "cSpell.words": [
+ "manabie"
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 8df9d4d3a..41fe3b2b5 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,42 @@
-### Requirements
-
-- Implement one single API which accepts a todo task and records it
- - There is a maximum **limit of N tasks per user** that can be added **per day**.
- - Different users can have **different** maximum daily limit.
-- Write integration (functional) tests
-- Write unit tests
-- Choose a suitable architecture to make your code simple, organizable, and maintainable
-- Write a concise README
- - How to run your code locally?
- - A sample “curl” command to call your API
- - How to run your unit tests locally?
- - What do you love about your solution?
- - What else do you want us to know about however you do not have enough time to complete?
-
-### Notes
-
-- We're using Golang at Manabie. **However**, we encourage you to use the programming language that you are most comfortable with because we want you to **shine** with all your skills and knowledge.
-
-### How to submit your solution?
-
-- Fork this repo and show us your development progress via a PR
-
-### Interesting facts about Manabie
-
-- Monthly there are about 2 million lines of code changes (inserted/updated/deleted) committed into our GitHub repositories. To avoid **regression bugs**, we write different kinds of **automated tests** (unit/integration (functionality)/end2end) as parts of the definition of done of our assigned tasks.
-- We nurture the cultural values: **knowledge sharing** and **good communication**, therefore good written documents and readable, organizable, and maintainable code are in our blood when we build any features to grow our products.
-- We have **collaborative** culture at Manabie. Feel free to ask trieu@manabie.com any questions. We are very happy to answer all of them.
-
-Thank you for spending time to read and attempt our take-home assessment. We are looking forward to your submission.
+# TOGO
+
+## Description
+
+ **Simple API using Golang, Postgresql and jwt for authentication**
+
+## 1. How to run your code locally?
+- Requirements
+ - Install [Docker Engine](https://docs.docker.com/engine/install/)
+ - Install [docker-compose](https://docs.docker.com/compose/install/)
+- Git clone repository
+
+ ```bash
+ git clone https://github.com/qanghaa/togo.git
+ ```
+- Go to ***togo***:open_file_folder: directory
+- Using `docker-compose` commands with ***root*** permission
+ ```bash
+ # this command make sure next command working as expected
+ sudo docker-compose down --volumes .
+ ```
+
+ ```bash
+ sudo docker-compose up
+ ```
+
+## 2. Sample “curl” Command:
+ API Document: [here](https://documenter.getpostman.com/view/15522883/UzBvHPBC)
+
Note: ***Using Bearer Authorization Header for endpoints required***
+
+## 3. How to run your unit tests locally?
+ - Go to ***togo***:open_file_folder: directory
+ - type in cmd:
+ ```bash
+ go test ./...
+ ```
+
+## 4. What do you love about your solution?
+ Overall, nothing outstanding. However I quite like the payment feature. Although the implementation is simple, it will help the user become a Premium user LOL. This feature helps users to overcome the limit of creating tasks in 1 day (20 tasks/day) instead òf 10 as usual. Not related to feature, probably Docker, I spent 2 days learning and trying to work on it and I was able to use it in this project :D.
+
+## 5. What else do you want us to know about however you do not have enough time to complete?
+Probably Testing. I haven't writed unit test enough possible scenarios with my API yet.
diff --git a/app/app.go b/app/app.go
new file mode 100644
index 000000000..1913766c0
--- /dev/null
+++ b/app/app.go
@@ -0,0 +1,36 @@
+package app
+
+import (
+ "database/sql"
+ "fmt"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ _ "github.com/lib/pq"
+)
+
+type App struct {
+ Router *mux.Router
+ DB *sql.DB
+}
+
+func (a *App) Init(connectURL string) {
+ db, err := sql.Open("postgres", connectURL)
+
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = db.Ping()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println("Database is connected!")
+ a.DB = db
+ a.Router = mux.NewRouter()
+ a.Routes()
+}
+
+func (a *App) Run(host string) {
+ log.Fatal(http.ListenAndServe(host, a.Router))
+}
diff --git a/app/handlers.go b/app/handlers.go
new file mode 100644
index 000000000..ad61b13b0
--- /dev/null
+++ b/app/handlers.go
@@ -0,0 +1,51 @@
+package app
+
+import (
+ "net/http"
+
+ c "github.com/manabie-com/togo/controllers"
+)
+
+func (a *App) GetMe(w http.ResponseWriter, r *http.Request) {
+ c.GetMe(a.DB, w, r)
+}
+
+func (a *App) SignUp(w http.ResponseWriter, r *http.Request) {
+ c.SignUp(a.DB, w, r)
+}
+
+func (a *App) Login(w http.ResponseWriter, r *http.Request) {
+ c.Login(a.DB, w, r)
+}
+
+func (a *App) UpdateMe(w http.ResponseWriter, r *http.Request) {
+ c.UpdateMe(a.DB, w, r)
+}
+
+func (a *App) DeleteMe(w http.ResponseWriter, r *http.Request) {
+ c.DeleteMe(a.DB, w, r)
+}
+
+func (a *App) GetTasks(w http.ResponseWriter, r *http.Request) {
+ c.GetTasks(a.DB, w, r)
+}
+
+func (a *App) GetTask(w http.ResponseWriter, r *http.Request) {
+ c.GetTask(a.DB, w, r)
+}
+
+func (a *App) Add(w http.ResponseWriter, r *http.Request) {
+ c.Add(a.DB, w, r)
+}
+
+func (a *App) Edit(w http.ResponseWriter, r *http.Request) {
+ c.Edit(a.DB, w, r)
+}
+
+func (a *App) Delete(w http.ResponseWriter, r *http.Request) {
+ c.Delete(a.DB, w, r)
+}
+
+func (a *App) Payment(w http.ResponseWriter, r *http.Request) {
+ c.Payment(a.DB, w, r)
+}
diff --git a/app/routes.go b/app/routes.go
new file mode 100644
index 000000000..00c368f8e
--- /dev/null
+++ b/app/routes.go
@@ -0,0 +1,31 @@
+package app
+
+import (
+ "github.com/gorilla/mux"
+ "github.com/manabie-com/togo/controllers"
+)
+
+func (a *App) Routes() {
+
+ router := a.Router
+ // sub router like http://:/api/users
+ userRouter := router.PathPrefix("/api/users").Subrouter()
+ userRouter.HandleFunc("/me", a.GetMe).Methods("GET")
+ userRouter.HandleFunc("/signup", a.SignUp).Methods("POST")
+ userRouter.HandleFunc("/login", a.Login).Methods("POST")
+ userRouter.HandleFunc("/edit", a.UpdateMe).Methods("PATCH")
+ userRouter.HandleFunc("/delete", a.DeleteMe).Methods("DELETE")
+ // sub router like http://:/api/tasks
+ taskRouter := router.PathPrefix("/api/tasks").Subrouter()
+ taskRouter.HandleFunc("", a.GetTasks).Methods("GET")
+ taskRouter.HandleFunc("/{id}", a.GetTask).Methods("GET")
+ taskRouter.HandleFunc("/add", a.Add).Methods("POST")
+ taskRouter.HandleFunc("/{id}", a.Edit).Methods("PATCH")
+ taskRouter.HandleFunc("/{id}", a.Delete).Methods("DELETE")
+ // sub routes like http://:/api/payments
+ paymentRouter := router.PathPrefix("/api/payments").Subrouter()
+ paymentRouter.HandleFunc("", a.Payment).Methods("POST")
+ // runs database
+ router.Use(mux.CORSMethodMiddleware(router))
+ router.Use(controllers.JwtAuthentication)
+}
diff --git a/controllers/auth.go b/controllers/auth.go
new file mode 100644
index 000000000..00706fe63
--- /dev/null
+++ b/controllers/auth.go
@@ -0,0 +1,60 @@
+package controllers
+
+import (
+ "context"
+ "net/http"
+ "os"
+ "strings"
+
+ "github.com/manabie-com/togo/models"
+ u "github.com/manabie-com/togo/utils"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+var JwtAuthentication = func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ notAuth := []string{"/api/users/signup", "/api/users/login"} //List of endpoints that doesn't require auth
+ requestPath := r.URL.Path //current request path
+
+ //check if request does not need authentication, serve the request if it doesn't need it
+ for _, value := range notAuth {
+ if value == requestPath {
+ next.ServeHTTP(w, r)
+ return
+ }
+ }
+
+ tokenHeader := r.Header.Get("Authorization") //Grab the token from the header
+ if tokenHeader == "" { //Token is missing, returns with error code 403 Unauthorized
+ u.FailureRespond(w, http.StatusForbidden, "Missing auth token")
+ return
+ }
+
+ splitted := strings.Split(tokenHeader, " ") //The token normally comes in format `Bearer {token-body}`, we check if the retrieved token matched this requirement
+ if len(splitted) != 2 {
+ u.FailureRespond(w, http.StatusForbidden, "Invalid/Malformed auth token")
+ return
+ }
+
+ tokenPart := splitted[1] //Grab the token part, what we are truly interested in
+ tk := &models.Token{}
+ token, err := jwt.ParseWithClaims(tokenPart, tk, func(token *jwt.Token) (interface{}, error) {
+ return []byte(os.Getenv("SECRET_TOKEN")), nil
+ })
+ if err != nil { //Malformed token, returns with http code 403 as usual
+ u.FailureRespond(w, http.StatusForbidden, "Malformed authentication token")
+ return
+ }
+
+ if !token.Valid { //Token is invalid, maybe not signed on this server
+ u.FailureRespond(w, http.StatusForbidden, "Token is not valid.")
+ return
+ }
+ //Everything went well, proceed with the request and set the caller to the user retrieved from the parsed token
+ //Useful for monitoring
+ ctx := context.WithValue(r.Context(), "user", tk)
+ next.ServeHTTP(w, r.WithContext(ctx)) //proceed in the middleware chain!
+ })
+}
diff --git a/controllers/controllers_main_test.go b/controllers/controllers_main_test.go
new file mode 100644
index 000000000..8ad66b0d4
--- /dev/null
+++ b/controllers/controllers_main_test.go
@@ -0,0 +1,79 @@
+package controllers_test
+
+import (
+ "log"
+ "os"
+ "syscall"
+ "testing"
+
+ "net/http"
+ "net/http/httptest"
+
+ "github.com/joho/godotenv"
+ "github.com/manabie-com/togo/app"
+)
+
+type response struct {
+ Status string
+ Message string
+ Data map[string]interface{}
+}
+
+var (
+ a app.App
+ r response
+)
+
+func TestMain(m *testing.M) {
+ if err := godotenv.Load("../.env"); err != nil {
+ log.Fatal("Error loading .env file")
+ }
+ CONNECT_STR_FOR_TEST, ok := syscall.Getenv("CONNECT_STR_FOR_TEST")
+ if !ok {
+ log.Fatal("Please set CONNECT_STR_FOR_TEST environment")
+ }
+ a = app.App{}
+ a.Init(CONNECT_STR_FOR_TEST)
+ code := m.Run()
+ os.Exit(code)
+}
+
+func executeRequest(r *http.Request) *httptest.ResponseRecorder {
+ rr := httptest.NewRecorder()
+ a.Router.ServeHTTP(rr, r)
+ return rr
+}
+
+func checkResponseCode(t *testing.T, expected, actual int) {
+ if expected != actual {
+ t.Errorf("Expected response code %d. Got %d\n", expected, actual)
+ }
+}
+
+func checkResponseStatus(t *testing.T, expected, actual string) {
+ if expected != actual {
+ t.Errorf("Expected response Status %s. Got %s\n", expected, actual)
+ }
+}
+
+func checkResponseMessage(t *testing.T, expected, actual string) {
+ if expected != actual {
+ t.Errorf("Expected response Message %s. Got %s\n", expected, actual)
+ }
+}
+
+func rollbackUser() error {
+ _, err := a.DB.Exec(`DELETE FROM users WHERE email = $1`, "test_user@gmail.com")
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func rollbackTask() error {
+ _, err := a.DB.Exec(`DELETE FROM tasks WHERE user_id = (SELECT users.id FROM users where email = $1)`, "test_user@gmail.com")
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/controllers/payment.go b/controllers/payment.go
new file mode 100644
index 000000000..fc47270f4
--- /dev/null
+++ b/controllers/payment.go
@@ -0,0 +1,31 @@
+package controllers
+
+import (
+ "database/sql"
+ "net/http"
+
+ "github.com/manabie-com/togo/models"
+ u "github.com/manabie-com/togo/utils"
+)
+
+func Payment(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ decoded := r.Context().Value("user").(*models.Token)
+ user := &models.User{
+ ID: decoded.UserId,
+ LimitDayTasks: 20,
+ IsPayment: true,
+ }
+
+ err := user.UpgradePremium(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Something went wrong. Please try again")
+ return
+ }
+
+ u.SuccessRespond(w, http.StatusOK, "Success upgrade Premium account. Please login again to try new upgrade", map[string]interface{}{
+ "name": user.Name,
+ "email": user.Email,
+ "is_payment": user.IsPayment,
+ "limit_day_tasks": user.LimitDayTasks,
+ })
+}
diff --git a/controllers/payment_test.go b/controllers/payment_test.go
new file mode 100644
index 000000000..5bb79797b
--- /dev/null
+++ b/controllers/payment_test.go
@@ -0,0 +1,36 @@
+package controllers_test
+
+import (
+ "encoding/json"
+ "net/http"
+ "testing"
+)
+
+// Pass ✅
+func TestPayment(t *testing.T) {
+ signup()
+ token := getToken()
+ req, _ := http.NewRequest("POST", "/api/payments", nil)
+ req.Header.Set("Authorization", token)
+ res := executeRequest(req)
+
+ json.Unmarshal(res.Body.Bytes(), &r)
+ checkResponseCode(t, http.StatusOK, res.Code)
+ checkResponseStatus(t, "Success", r.Status)
+ checkResponseMessage(t, "Success upgrade Premium account. Please login again to try new upgrade", r.Message)
+
+ if r.Data["name"] != "test_user" {
+ t.Errorf("Expected user name is 'test_user'. Got '%v'", r.Data["name"])
+ }
+ if r.Data["email"] != "test_user@gmail.com" {
+ t.Errorf("Expected user email is 'test_user@gmail.com'. Got '%v'", r.Data["email"])
+ }
+ if r.Data["is_payment"] != true {
+ t.Errorf("Expected user is_payment field is 'true'. Got '%v'", r.Data["is_payment"])
+ }
+ if r.Data["limit_day_tasks"] != 20.0 {
+ t.Errorf("Expected user limit task field is '20'. Got '%v'", r.Data["limit_day_tasks"])
+ }
+ // rollback before Payments
+ rollbackUser()
+}
diff --git a/controllers/task.go b/controllers/task.go
new file mode 100644
index 000000000..89c9e06e7
--- /dev/null
+++ b/controllers/task.go
@@ -0,0 +1,164 @@
+package controllers
+
+import (
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/gorilla/mux"
+ "github.com/manabie-com/togo/models"
+ u "github.com/manabie-com/togo/utils"
+)
+
+var GetTasks = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ // get user id here
+ decoded := r.Context().Value("user").(*models.Token)
+ task := &models.Task{
+ UserId: decoded.UserId,
+ }
+ tasks, err := task.GetTasksByUserId(db)
+ if err != nil {
+ u.SuccessRespond(w, http.StatusOK, "Success", nil)
+ return
+ }
+ //Everything OK
+ u.SuccessRespond(w, http.StatusOK, "Success", tasks)
+}
+
+var GetTask = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ // decode token from middleware
+ decoded := r.Context().Value("user").(*models.Token)
+ // convert string id to uint32
+ id, err := u.Str2Uint32(mux.Vars(r)["id"])
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "ID must be a number type.")
+ return
+ }
+
+ task := &models.Task{
+ ID: id,
+ UserId: decoded.UserId,
+ }
+ err = task.GetTaskByUserId(db)
+ if err != nil {
+ u.SuccessRespond(w, http.StatusOK, "Not found task", nil)
+ return
+ }
+
+ u.SuccessRespond(w, http.StatusOK, "Success", map[string]interface{}{
+ "name": task.Name,
+ "content": task.Content,
+ "created_at": task.CreatedAt,
+ })
+}
+
+var Add = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ // decode token from middleware
+ decoded := r.Context().Value("user").(*models.Token)
+ task := &models.Task{
+ UserId: decoded.UserId,
+ }
+ // json body -> task object
+ err := json.NewDecoder(r.Body).Decode(task)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "invalid request")
+ return
+ }
+ // validate task object
+ validate := validator.New()
+ err = validate.Struct(task)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ // check today tasks limit
+ if task.IsLimit(db, decoded.LimitDayTasks) {
+ u.FailureRespond(w, http.StatusBadRequest, "Today tasks had limited, Please Comeback tomorrow.")
+ return
+ }
+ // insert database
+ err = task.InsertTask(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ //everything OK
+ u.SuccessRespond(w, http.StatusCreated, "Success create task", map[string]interface{}{
+ "id": fmt.Sprint(task.ID),
+ "name": task.Name,
+ "content": task.Content,
+ "created_at": task.CreatedAt,
+ })
+}
+
+var Edit = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ decoded := r.Context().Value("user").(*models.Token)
+ // convert string id to uint32
+ id, err := u.Str2Uint32(mux.Vars(r)["id"])
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "ID must be a number type.")
+ return
+ }
+
+ task := &models.Task{
+ ID: id,
+ UserId: decoded.UserId,
+ }
+
+ // err := GetTaskByUserId(db)
+ err = json.NewDecoder(r.Body).Decode(task)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ var (
+ newName string = task.Name
+ newContent string = task.Content
+ )
+ err = task.GetTaskByUserId(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusInternalServerError, "Somethings went wrong. Please try again"+err.Error())
+ return
+ }
+ if newName != "" {
+ task.Name = newName
+ }
+ if newContent != "" {
+ task.Content = newContent
+ }
+ err = task.UpdateTaskById(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ u.SuccessRespond(w, http.StatusOK, "Success update task", map[string]interface{}{
+ "name": task.Name,
+ "content": task.Content,
+ "created_at": task.CreatedAt,
+ })
+}
+
+var Delete = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ decoded := r.Context().Value("user").(*models.Token)
+ // convert string id to uint32
+ id, err := u.Str2Uint32(mux.Vars(r)["id"])
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "ID must be a number type.")
+ return
+ }
+
+ task := &models.Task{
+ ID: id,
+ UserId: decoded.UserId,
+ }
+
+ err = task.DeleteTaskById(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ u.SuccessRespond(w, http.StatusNoContent, "Success delete task", nil)
+}
diff --git a/controllers/task_test.go b/controllers/task_test.go
new file mode 100644
index 000000000..ec20df535
--- /dev/null
+++ b/controllers/task_test.go
@@ -0,0 +1,170 @@
+package controllers_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "testing"
+)
+
+// Pass ✅
+func TestAdd(t *testing.T) {
+ signup()
+ token := getToken()
+ payload := []byte(`{
+ "name" : "task name",
+ "content" : "task content"
+ }`)
+ req, _ := http.NewRequest("POST", "/api/tasks/add", bytes.NewBuffer(payload))
+ req.Header.Set("Authorization", token)
+ res := executeRequest(req)
+ json.Unmarshal(res.Body.Bytes(), &r)
+
+ checkResponseCode(t, http.StatusCreated, res.Code)
+ checkResponseStatus(t, "Success", r.Status)
+ checkResponseMessage(t, "Success create task", r.Message)
+
+ if r.Data["name"] != "task name" {
+ t.Errorf("Expected task name is 'task name'. Got '%v'", r.Data["name"])
+ }
+ if r.Data["content"] != "task content" {
+ t.Errorf("Expected task content is 'task content'. Got '%v'", r.Data["content"])
+ }
+}
+
+// Pass ✅
+func TestGetTasks(t *testing.T) {
+ type tasksResponse struct {
+ Status string
+ Message string
+ Data []map[string]string
+ }
+ signup()
+ token := getToken()
+ // get user id here
+ req, _ := http.NewRequest("GET", "/api/tasks", nil)
+ req.Header.Set("Authorization", token)
+ res := executeRequest(req)
+ tr := tasksResponse{}
+ json.Unmarshal(res.Body.Bytes(), &tr)
+
+ checkResponseCode(t, http.StatusOK, res.Code)
+ checkResponseStatus(t, "Success", tr.Status)
+ checkResponseMessage(t, "Success", tr.Message)
+ // json.NewDecoder(res.Body).Decode(&m)
+
+ for _, v := range tr.Data {
+ if v["name"] != "task name" {
+ t.Errorf("Expected task name is 'task name'. Got '%v'", v["name"])
+ }
+ if v["content"] != "task content" {
+ t.Errorf("Expected task content is 'task content'. Got '%v'", v["content"])
+ }
+ }
+}
+
+// Pass ✅
+func TestGetTask(t *testing.T) {
+ signup()
+ token := getToken()
+ id := getIdFromCreatedTask(token)
+ req, _ := http.NewRequest("GET", "/api/tasks/"+id, nil)
+ // auth token
+ req.Header.Set("Authorization", token)
+
+ res := executeRequest(req)
+ re := response{}
+ json.Unmarshal(res.Body.Bytes(), &re)
+
+ checkResponseCode(t, http.StatusOK, res.Code)
+ checkResponseStatus(t, "Success", re.Status)
+ checkResponseMessage(t, "Success", re.Message)
+
+ if re.Data["name"] != "task name" {
+ t.Errorf("Expected task name to be 'task name' value. Got '%v'", re.Data["name"])
+ }
+ if re.Data["content"] != "task content" {
+ t.Errorf("Expected task content to be 'task content' value'. Got '%v'", re.Data["content"])
+ }
+}
+
+// Pass ✅
+func TestEdit(t *testing.T) {
+ signup()
+ token := getToken()
+ id := getIdFromCreatedTask(token)
+ payload := []byte(`{
+ "name" : "task name",
+ "content" : "task content"
+ }`)
+ req, _ := http.NewRequest("PATCH", "/api/tasks/"+id, bytes.NewBuffer(payload))
+ req.Header.Set("Authorization", token)
+ res := executeRequest(req)
+
+ json.Unmarshal(res.Body.Bytes(), &r)
+
+ checkResponseCode(t, http.StatusOK, res.Code)
+ checkResponseStatus(t, "Success", r.Status)
+ checkResponseMessage(t, "Success update task", r.Message)
+ if r.Data["name"] != "task name" {
+ t.Errorf("Expected task name is 'task name'. Got '%v'", r.Data["name"])
+ }
+ if r.Data["content"] != "task content" {
+ t.Errorf("Expected task content is 'task content'. Got '%v'", r.Data["content"])
+ }
+}
+
+// Pass ✅
+func TestDelete(t *testing.T) {
+ signup()
+ token := getToken()
+ id := getIdFromCreatedTask(token)
+ req, _ := http.NewRequest("DELETE", "/api/tasks/"+id, nil)
+ req.Header.Set("Authorization", token)
+ res := executeRequest(req)
+
+ json.Unmarshal(res.Body.Bytes(), &r)
+
+ checkResponseCode(t, http.StatusNoContent, res.Code)
+ checkResponseStatus(t, "Success", r.Status)
+ checkResponseMessage(t, "Success delete task", r.Message)
+
+ if r.Data != nil {
+ t.Errorf("Expected type of Data to be 'nil' value. Got '%v'", r.Data)
+ }
+ rollbackTask()
+ rollbackUser()
+}
+
+func signup() {
+ payload := []byte(`{
+ "name": "test_user",
+ "email": "test_user@gmail.com",
+ "password": "123456"
+ }`)
+ req, _ := http.NewRequest("POST", "/api/users/signup", bytes.NewBuffer(payload))
+ req.Header.Set("Content-Type", "application/json")
+ res := executeRequest(req)
+ json.Unmarshal(res.Body.Bytes(), &r)
+ if r.Status == "Failure" {
+ return
+ }
+}
+
+func getIdFromCreatedTask(token string) string {
+ type createTaskRes struct {
+ Status string
+ Message string
+ Data map[string]string
+ }
+ payload := []byte(`{
+ "name" : "task name",
+ "content" : "task content"
+ }`)
+ req, _ := http.NewRequest("POST", "/api/tasks/add", bytes.NewBuffer(payload))
+ req.Header.Set("Authorization", token)
+ res := executeRequest(req)
+ r := createTaskRes{}
+ json.Unmarshal(res.Body.Bytes(), &r)
+ return r.Data["id"]
+}
diff --git a/controllers/user.go b/controllers/user.go
new file mode 100644
index 000000000..4df89a0bc
--- /dev/null
+++ b/controllers/user.go
@@ -0,0 +1,210 @@
+package controllers
+
+import (
+ "database/sql"
+ "encoding/json"
+ "net/http"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/manabie-com/togo/models"
+ u "github.com/manabie-com/togo/utils"
+)
+
+var SignUp = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ user := &models.User{
+ IsPayment: false,
+ LimitDayTasks: 10,
+ }
+ // decode json body to user
+ err := json.NewDecoder(r.Body).Decode(user)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Invalid input format: "+err.Error())
+ return
+ }
+ // validate user object
+ validate := validator.New()
+ err = validate.Struct(user)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Invalid input field: "+err.Error())
+ return
+ }
+ // insert database
+ err = user.InsertUser(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Your email is duplicated, Please try again")
+ return
+ }
+ // send token jwt here
+ tk := &models.Token{UserId: user.ID, LimitDayTasks: user.LimitDayTasks}
+ tokenString := tk.CreateToken()
+ // everything Ok
+ u.SuccessRespond(w, http.StatusCreated, "Created Account", map[string]interface{}{
+ "name": user.Name,
+ "email": user.Email,
+ "token": tokenString,
+ })
+}
+
+var Login = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ user := &models.User{}
+ // validate
+ err := json.NewDecoder(r.Body).Decode(user)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Invalid input format: "+err.Error())
+ return
+ }
+
+ var (
+ email string = user.Email
+ password string = user.Password
+ )
+ // get user by email
+ err = user.GetUserByEmail(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Not found account with your email, Please provide a valid email address!")
+ return
+ }
+ // if email exist and password incorrect
+ if email != user.Email || password != user.Password {
+ u.FailureRespond(w, http.StatusUnauthorized, "Password incorrect")
+ return
+ }
+ // create message case 1: Active user, case 2: unActive user
+ var message = "Login Success"
+ if !user.IsActive {
+ message = "Welcome back"
+ err = user.ActiveUser(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusInternalServerError, "Something went wrong"+err.Error())
+ return
+ }
+ }
+ // if email and password OK
+ //Create JWT token
+ tk := &models.Token{
+ UserId: user.ID,
+ LimitDayTasks: user.LimitDayTasks,
+ }
+ tokenString := tk.CreateToken()
+ // everything Ok
+ u.SuccessRespond(w, http.StatusOK, message, map[string]interface{}{
+ "name": user.Name,
+ "email": user.Email,
+ "token": tokenString,
+ })
+}
+
+var GetMe = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ // get decoded token from middleware
+ decoded := r.Context().Value("user").(*models.Token)
+ user := &models.User{
+ ID: decoded.UserId,
+ }
+ // query
+ err := user.GetUserById(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusInternalServerError, "Something went wrong when collect your account. Please try again"+err.Error())
+ return
+ }
+ // everything Ok
+ u.SuccessRespond(w, http.StatusOK, "Success", map[string]interface{}{
+ "name": user.Name,
+ "email": user.Email,
+ "is_payment": user.IsPayment,
+ "limit_day_tasks": user.LimitDayTasks,
+ })
+}
+
+var UpdateMe = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ // get decoded token from middleware
+ decoded := r.Context().Value("user").(*models.Token)
+ user := &models.User{
+ ID: decoded.UserId,
+ }
+ // convert json -> user object
+ err := json.NewDecoder(r.Body).Decode(user)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Invalid input format: "+err.Error())
+ return
+ }
+ // get user info
+ var (
+ name string = user.Name
+ email string = user.Email
+ password string = user.Password
+ )
+ // validate input
+ validate := validator.New()
+ if err := validate.Var(email, "email,min=10,max=30"); email != "" && err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Invalid input email: "+err.Error())
+ return
+ }
+
+ if err := validate.Var(name, "min=5,max=20"); name != "" && err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Invalid input name: "+err.Error())
+ return
+ }
+ err = user.GetUserById(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusInternalServerError, "Somethings went wrong. Please try again"+err.Error())
+ return
+ }
+ // confirm password
+ if password != user.Password {
+ u.FailureRespond(w, http.StatusUnauthorized, "Password incorrect. Please try again")
+ return
+ }
+ // if valid value => overwrite new value
+ if name != "" {
+ user.Name = name
+ }
+ if email != "" {
+ user.Email = email
+ }
+ // update me
+ err = user.UpdateUser(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusInternalServerError, "Something went wrong when update your account. Please try again"+err.Error())
+ return
+ }
+ // everything Ok
+ u.SuccessRespond(w, http.StatusOK, "Success update your account!", map[string]interface{}{
+ "name": user.Name,
+ "email": user.Email,
+ })
+}
+
+var DeleteMe = func(db *sql.DB, w http.ResponseWriter, r *http.Request) {
+ // get decoded token from middleware
+ decoded := r.Context().Value("user").(*models.Token)
+ user := &models.User{
+ ID: decoded.UserId,
+ IsActive: false,
+ }
+ // convert json -> user object
+ err := json.NewDecoder(r.Body).Decode(user)
+ if err != nil {
+ u.FailureRespond(w, http.StatusBadRequest, "Invalid input format: "+err.Error())
+ return
+ }
+ // get user info
+ inputPassword := user.Password
+ err = user.GetUserById(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusInternalServerError, "Something went wrong when collect your account. Please try again"+err.Error())
+ return
+ }
+ // if input password not equal to database password
+ if inputPassword != user.Password {
+ u.FailureRespond(w, http.StatusUnauthorized, "Password incorrect. Please try again")
+ return
+ }
+ // update field is_active to false
+ err = user.DeleteUser(db)
+ if err != nil {
+ u.FailureRespond(w, http.StatusInternalServerError, "Something went wrong when delete your account. Please try again"+err.Error())
+ return
+ }
+ // everything Ok
+ u.SuccessRespond(w, http.StatusNoContent, "Success delete your account!", nil)
+}
diff --git a/controllers/user_test.go b/controllers/user_test.go
new file mode 100644
index 000000000..7042d7e33
--- /dev/null
+++ b/controllers/user_test.go
@@ -0,0 +1,159 @@
+package controllers_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "testing"
+)
+
+// Pass ✅
+func TestSignUp(t *testing.T) {
+ _ = rollbackUser()
+ payload := []byte(`{
+ "name": "test_user",
+ "email": "test_user@gmail.com",
+ "password": "123456"
+ }`)
+ req, _ := http.NewRequest("POST", "/api/users/signup", bytes.NewBuffer(payload))
+ req.Header.Set("Content-Type", "application/json")
+ res := executeRequest(req)
+ json.Unmarshal(res.Body.Bytes(), &r)
+ checkResponseCode(t, http.StatusCreated, res.Code)
+ checkResponseStatus(t, "Success", r.Status)
+ checkResponseMessage(t, "Created Account", r.Message)
+
+ if r.Data["email"] != "test_user@gmail.com" {
+ t.Errorf("Expected type of Data to be 'test_user@gmail.com'. Got '%v'", r.Data["email"])
+ }
+
+ if r.Data["name"] != "test_user" {
+ t.Errorf("Expected type of Data to be 'test_user'. Got '%v'", r.Data["name"])
+ }
+}
+
+// Pass ✅
+func TestLogin(t *testing.T) {
+ signup()
+ payload := []byte(`{
+ "email": "test_user@gmail.com",
+ "password": "123456"
+ }`)
+ req, _ := http.NewRequest("POST", "/api/users/login", bytes.NewBuffer(payload))
+ req.Header.Set("Content-Type", "application/json")
+ res := executeRequest(req)
+
+ json.Unmarshal(res.Body.Bytes(), &r)
+
+ checkResponseCode(t, http.StatusOK, res.Code)
+ checkResponseStatus(t, "Success", r.Status)
+ checkResponseMessage(t, "Login Success", r.Message)
+
+ if r.Data["email"] != "test_user@gmail.com" {
+ t.Errorf("Expected type of Data to be 'test_user@gmail.com'. Got '%v'", r.Data["email"])
+ }
+
+ if r.Data["name"] != "test_user" {
+ t.Errorf("Expected type of Data to be 'test_user'. Got '%v'", r.Data["name"])
+ }
+}
+
+// Pass ✅
+func TestGetMe(t *testing.T) {
+ // get token from test user
+ signup()
+ token := getToken()
+ req, _ := http.NewRequest("GET", "/api/users/me", nil)
+ req.Header.Set("Authorization", token)
+ res := executeRequest(req)
+ json.Unmarshal(res.Body.Bytes(), &r)
+
+ checkResponseCode(t, http.StatusOK, res.Code)
+ checkResponseStatus(t, "Success", r.Status)
+ checkResponseMessage(t, "Success", r.Message)
+
+ // json.NewDecoder(res.Body).Decode(&m)
+
+ if r.Data["name"] != "test_user" {
+ t.Errorf("Expected user name is 'test_user'. Got '%v'", r.Data["name"])
+ }
+ if r.Data["email"] != "test_user@gmail.com" {
+ t.Errorf("Expected user email is 'test_user@gmail.com'. Got '%v'", r.Data["email"])
+ }
+ if r.Data["is_payment"] != false {
+ t.Errorf("Expected user is_payment field is 'true'. Got '%v'", r.Data["is_payment"])
+ }
+ if r.Data["limit_day_tasks"] != 10.0 {
+ t.Errorf("Expected user limit task field is '10'. Got '%v'", r.Data["limit_day_tasks"])
+ }
+}
+
+// Pass ✅
+func TestUpdateMe(t *testing.T) {
+ // get token from test user
+ signup()
+ token := getToken()
+ payload := []byte(`{
+ "name": "updated_test_user",
+ "password": "123456"
+ }`)
+ req, _ := http.NewRequest("PATCH", "/api/users/edit", bytes.NewBuffer(payload))
+ req.Header.Set("Authorization", token)
+ res := executeRequest(req)
+ json.Unmarshal(res.Body.Bytes(), &r)
+
+ checkResponseCode(t, http.StatusOK, res.Code)
+ checkResponseStatus(t, "Success", r.Status)
+ checkResponseMessage(t, "Success update your account!", r.Message)
+
+ // json.NewDecoder(res.Body).Decode(&m)
+
+ if r.Data["email"] != "test_user@gmail.com" {
+ t.Errorf("Expected field of Data email to be 'test_user@gmail.com'. Got '%v'", r.Data["email"])
+ }
+
+ if r.Data["name"] != "updated_test_user" {
+ t.Errorf("Expected field of Data name to be 'updated_test_user'. Got '%v'", r.Data["name"])
+ }
+}
+
+// Pass ✅
+func TestDeleteMe(t *testing.T) {
+ signup()
+ token := getToken()
+ payload := []byte(`{
+ "password": "123456"
+ }`)
+ req, _ := http.NewRequest("DELETE", "/api/users/delete", bytes.NewBuffer(payload))
+ req.Header.Set("Authorization", token)
+ res := executeRequest(req)
+ json.Unmarshal(res.Body.Bytes(), &r)
+
+ checkResponseCode(t, http.StatusNoContent, res.Code)
+ checkResponseStatus(t, "Success", r.Status)
+ checkResponseMessage(t, "Success delete your account!", r.Message)
+
+ if r.Data != nil {
+ t.Errorf("Expected field of Data to be 'nil'. Got '%v'", r.Data)
+ }
+ rollbackUser()
+ rollbackTask()
+}
+
+func getToken() string {
+ type predictResponse struct {
+ Status string
+ Message string
+ Data map[string]string
+ }
+ payload := []byte(`{
+ "email": "test_user@gmail.com",
+ "password": "123456"
+ }`)
+ req, _ := http.NewRequest("POST", "/api/users/login", bytes.NewBuffer(payload))
+ req.Header.Set("Content-Type", "application/json")
+ res := executeRequest(req)
+ var pr predictResponse
+ json.Unmarshal(res.Body.Bytes(), &pr)
+ return "Bearer " + pr.Data["token"]
+}
diff --git a/db.Dockerfile b/db.Dockerfile
new file mode 100644
index 000000000..6c7a6986a
--- /dev/null
+++ b/db.Dockerfile
@@ -0,0 +1,5 @@
+FROM postgres
+ENV POSTGRES_PASSWORD manabie
+ENV POSTGRES_DB togo
+COPY script.sql /docker-entrypoint-initdb.d/
+VOLUME ./postgres-data:/var/lib/postgresql/data
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..caaf1b5a3
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,24 @@
+version: "3.7"
+services:
+ database:
+ build:
+ context: .
+ dockerfile: db.Dockerfile
+ restart: always
+ env_file:
+ - .env
+ ports:
+ - "2345:5432"
+ server:
+ build:
+ context: .
+ dockerfile: server.Dockerfile
+ env_file:
+ - .env
+ restart: always
+ depends_on:
+ - database
+ networks:
+ - default
+ ports:
+ - "3000:3000"
diff --git a/go.mod b/go.mod
new file mode 100644
index 000000000..9335e7623
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,32 @@
+module github.com/manabie-com/togo
+
+go 1.18
+
+require (
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
+ github.com/go-pg/zerochecker v0.2.0 // indirect
+ github.com/go-playground/locales v0.14.0 // indirect
+ github.com/go-playground/universal-translator v0.18.0 // indirect
+ github.com/go-playground/validator/v10 v10.11.0 // indirect
+ github.com/gorilla/mux v1.8.0 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/joho/godotenv v1.4.0 // indirect
+ github.com/kr/fs v0.1.0 // indirect
+ github.com/kr/pretty v0.3.0 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/leodido/go-urn v1.2.1 // indirect
+ github.com/lib/pq v1.10.6 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/rogpeppe/go-internal v1.8.1 // indirect
+ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
+ github.com/vmihailenco/bufpool v0.1.11 // indirect
+ github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
+ github.com/vmihailenco/tagparser v0.1.2 // indirect
+ github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
+ golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
+ golang.org/x/text v0.3.7 // indirect
+ golang.org/x/tools v0.1.11 // indirect
+ mellium.im/sasl v0.2.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 000000000..8f2e97e36
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,191 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-pg/pg/v10 v10.10.6 h1:1vNtPZ4Z9dWUw/TjJwOfFUbF5nEq1IkR6yG8Mq/Iwso=
+github.com/go-pg/pg/v10 v10.10.6/go.mod h1:GLmFXufrElQHf5uzM3BQlcfwV3nsgnHue5uzjQ6Nqxg=
+github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
+github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
+github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
+github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
+github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
+github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
+github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
+github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
+github.com/tools/godep v0.0.0-20180126220526-ce0bfadeb516 h1:h4a8ZFxjlRVGsFGP4l/AdnoUYcF3pfxzyepS3oKZ8mE=
+github.com/tools/godep v0.0.0-20180126220526-ce0bfadeb516/go.mod h1:OGh2HQGYVW+2+ZdB+DgJhI75kivkKWtVcIxI/pesDsY=
+github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
+github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
+github.com/vmihailenco/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc=
+github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
+github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210923061019-b8560ed6a9b7 h1:c20P3CcPbopVp2f7099WLOqSNKURf30Z0uq66HpijZY=
+golang.org/x/sys v0.0.0-20210923061019-b8560ed6a9b7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
+golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
+golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0=
+gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=
+mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ=
diff --git a/main.go b/main.go
new file mode 100644
index 000000000..638a2a95f
--- /dev/null
+++ b/main.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "log"
+ "syscall"
+
+ "github.com/joho/godotenv"
+ _ "github.com/lib/pq"
+ "github.com/manabie-com/togo/app"
+)
+
+func main() {
+ if err := godotenv.Load(); err != nil {
+ log.Fatal("Error loading .env file")
+ }
+ port, ok := syscall.Getenv("PORT")
+ if !ok {
+ port = "8000"
+ }
+ CONNECT_STR, ok := syscall.Getenv("CONNECT_STR")
+ if !ok {
+ log.Fatal("Please set CONNECT_STR environment")
+ }
+ app := &app.App{}
+ app.Init(CONNECT_STR)
+ app.Run(":" + port)
+ defer app.DB.Close()
+}
diff --git a/models/task.go b/models/task.go
new file mode 100644
index 000000000..d1df9c04b
--- /dev/null
+++ b/models/task.go
@@ -0,0 +1,70 @@
+package models
+
+import (
+ "database/sql"
+ "time"
+)
+
+type Task struct {
+ ID uint32 `json:"id" validate:"omitempty"`
+ Name string `json:"name" validate:"required"`
+ Content string `json:"content"`
+ CreatedAt time.Time `json:"createdAt"`
+ UserId uint32 `json:"userId"`
+}
+
+func (t *Task) InsertTask(db *sql.DB) error {
+ err := db.QueryRow(`INSERT INTO tasks(name, content, user_id) VALUES($3, $2, $1) RETURNING id, name, content, created_at`, t.UserId, t.Content, t.Name).Scan(&t.ID, &t.Name, &t.Content, &t.CreatedAt)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (t *Task) GetTaskByUserId(db *sql.DB) error {
+ err := db.QueryRow(`SELECT name, content, created_at FROM tasks WHERE id = $1 AND user_id = $2 `, t.ID, t.UserId).Scan(&t.Name, &t.Content, &t.CreatedAt)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (t *Task) GetTasksByUserId(db *sql.DB) ([]*Task, error) {
+ var tasks []*Task
+ rows, err := db.Query(`SELECT * FROM tasks WHERE user_id = $1`, t.UserId)
+ if err != nil {
+ return tasks, err
+ }
+
+ for rows.Next() {
+ var task = &Task{}
+ rows.Scan(&task.ID, &task.Name, &task.Content, &task.CreatedAt, &task.UserId)
+ tasks = append(tasks, task)
+ }
+ return tasks, nil
+}
+
+func (t *Task) IsLimit(db *sql.DB, todayTasksLimit uint) bool {
+ // check limit day tasks
+ var tasksLength uint
+ _ = db.QueryRow(`SELECT COUNT(id)
+ FROM tasks
+ WHERE created_at >= NOW() - INTERVAL '24 HOURS' AND user_id = $1`, t.UserId).Scan(&tasksLength)
+ return tasksLength == todayTasksLimit
+}
+
+func (t *Task) DeleteTaskById(db *sql.DB) error {
+ _, err := db.Exec(`DELETE FROM tasks WHERE id = $1 AND user_id = $2`, t.ID, t.UserId)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (t *Task) UpdateTaskById(db *sql.DB) error {
+ _, err := db.Exec(`UPDATE tasks SET name = $3 WHERE id = $1 AND user_id = $2 RETURNING name, content, created_at`, t.ID, t.UserId, t.Name)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/models/token.go b/models/token.go
new file mode 100644
index 000000000..9c86d0975
--- /dev/null
+++ b/models/token.go
@@ -0,0 +1,19 @@
+package models
+
+import (
+ "os"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+type Token struct {
+ UserId uint32
+ LimitDayTasks uint
+ jwt.StandardClaims
+}
+
+func (t *Token) CreateToken() string {
+ token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), t)
+ tokenString, _ := token.SignedString([]byte(os.Getenv("SECRET_TOKEN")))
+ return tokenString
+}
diff --git a/models/user.go b/models/user.go
new file mode 100644
index 000000000..23d9e9527
--- /dev/null
+++ b/models/user.go
@@ -0,0 +1,72 @@
+package models
+
+import (
+ "database/sql"
+)
+
+type User struct {
+ ID uint32 `json:"id" validate:"omitempty"`
+ Email string `json:"email" validate:"required,email,min=10,max=40"`
+ Name string `json:"name" validate:"required,min=5,max=30"`
+ Password string `json:"password" validate:"required,min=6,max=20"`
+ IsPayment bool `json:"isPayment" validate:"omitempty"`
+ IsActive bool `json:"isActive"`
+ LimitDayTasks uint `json:"limitDayTasks" validate:"omitempty"`
+}
+
+func (u *User) InsertUser(db *sql.DB) error {
+ err := db.QueryRow(`INSERT INTO users(name, email, password) VALUES($1, $2, $3) RETURNING id, name, email`, u.Name, u.Email, u.Password).Scan(&u.ID, &u.Name, &u.Email)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (u *User) GetUserById(db *sql.DB) error {
+ err := db.QueryRow(`SELECT id, name, email, password, is_payment, is_active, limit_day_tasks FROM users WHERE id = $1`, u.ID).Scan(&u.ID, &u.Name, &u.Email, &u.Password, &u.IsPayment, &u.IsActive, &u.LimitDayTasks)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (u *User) GetUserByEmail(db *sql.DB) error {
+ err := db.QueryRow(`SELECT id, name, email, password, is_payment, is_active, limit_day_tasks FROM users WHERE email = $1`, u.Email).Scan(&u.ID, &u.Name, &u.Email, &u.Password, &u.IsPayment, &u.IsActive, &u.LimitDayTasks)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (u *User) ActiveUser(db *sql.DB) error {
+ _, err := db.Exec(`UPDATE users SET is_active = $1 WHERE id = $2`, true, u.ID)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (u *User) UpdateUser(db *sql.DB) error {
+ _, err := db.Exec(`UPDATE users SET name = $1, email = $2 WHERE id = $3`, u.Name, u.Email, u.ID)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// DeleteUser disable user active status => set is_active to false
+func (u *User) DeleteUser(db *sql.DB) error {
+ _, err := db.Exec(`UPDATE users SET is_active = $1 WHERE id = $2`, u.IsActive, u.ID)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (u *User) UpgradePremium(db *sql.DB) error {
+ err := db.QueryRow(`UPDATE users SET is_payment = $1, limit_day_tasks = $2 WHERE id = $3 RETURNING name, email`, u.IsPayment, u.LimitDayTasks, u.ID).Scan(&u.Name, &u.Email)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/script.sql b/script.sql
new file mode 100644
index 000000000..2cd310850
--- /dev/null
+++ b/script.sql
@@ -0,0 +1,29 @@
+CREATE TABLE users
+(
+ id serial NOT NULL,
+ email VARCHAR(40) NOT NULL,
+ name VARCHAR(30) NOT NULL,
+ password VARCHAR(20) NOT NULL,
+ is_payment boolean DEFAULT false,
+ limit_day_tasks smallint DEFAULT 10,
+ is_active boolean DEFAULT true,
+ CONSTRAINT users_pkey PRIMARY KEY (id),
+ CONSTRAINT email UNIQUE (email)
+);
+
+
+CREATE TABLE tasks
+(
+ id serial NOT NULL,
+ name VARCHAR(100) NOT NULL,
+ content text,
+ created_at timestamp without time zone NOT NULL DEFAULT now(),
+ user_id integer NOT NULL,
+ PRIMARY KEY (id),
+ FOREIGN KEY (user_id)
+ REFERENCES users (id) MATCH SIMPLE
+ ON UPDATE NO ACTION
+ ON DELETE NO ACTION
+ NOT VALID
+);
+
diff --git a/server.Dockerfile b/server.Dockerfile
new file mode 100644
index 000000000..4f8e9796c
--- /dev/null
+++ b/server.Dockerfile
@@ -0,0 +1,7 @@
+FROM golang:1.18-alpine
+WORKDIR /app
+COPY . .
+RUN go mod download
+RUN go build -o /togo
+EXPOSE 3000
+CMD [ "/togo" ]
diff --git a/utils/utils.go b/utils/utils.go
new file mode 100644
index 000000000..42e079957
--- /dev/null
+++ b/utils/utils.go
@@ -0,0 +1,40 @@
+package utils
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+)
+
+type response struct {
+ Status string
+ Message string
+ Data interface{}
+}
+
+func SuccessRespond(w http.ResponseWriter, statusCode int, message string, data interface{}) {
+ res := response{
+ Status: "Success",
+ Message: message,
+ Data: data,
+ }
+ w.WriteHeader(statusCode)
+ json.NewEncoder(w).Encode(res)
+}
+func FailureRespond(w http.ResponseWriter, statusCode int, message string) {
+ res := response{
+ Status: "Failure",
+ Message: message,
+ Data: nil,
+ }
+ w.WriteHeader(statusCode)
+ json.NewEncoder(w).Encode(res)
+}
+
+func Str2Uint32(str string) (uint32, error) {
+ u64, err := strconv.ParseUint(str, 10, 32)
+ if err != nil {
+ return 0, err
+ }
+ return uint32(u64), nil
+}
diff --git a/utils/utils_test.go b/utils/utils_test.go
new file mode 100644
index 000000000..040489bae
--- /dev/null
+++ b/utils/utils_test.go
@@ -0,0 +1,21 @@
+package utils
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestStr2Uint32(t *testing.T) {
+ var predict uint32 = 100
+ actual, err := Str2Uint32("100")
+
+ if err != nil {
+ t.Errorf("expected %v, but got %v", nil, err)
+ }
+ if predict != actual {
+ t.Errorf("expected %v, but got %v", predict, actual)
+ }
+ if reflect.TypeOf(actual) != reflect.TypeOf(predict) {
+ t.Errorf("expected %v, but got %v", "uint32", reflect.TypeOf(actual))
+ }
+}