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)) + } +}