diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..a8bb703 Binary files /dev/null and b/.DS_Store differ diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..d586b11 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index bf418f3..0000000 --- a/README.md +++ /dev/null @@ -1,33 +0,0 @@ -Assignment 1: - -Database - -Mỗi bài tập sẽ lưu trên file sol-{number}.sql - -1. Giải bài tập trên leetcode: - -https://leetcode.com/problems/capital-gainloss/description/ (gợi ý: sử dụng CASE) - -https://leetcode.com/problems/count-salary-categories/ (ngoài các cách trên leetcode, hãy nghĩ cách để giúp câu query này nhanh hơn, kể cả thay đổi cấu trúc bản) - -2. Bạn hãy viết một script để tạo các bản cho hệ thống với cấu trúc ở dưới - -![img.png](img.png) - -hệ thống bao gồm: - -- class -- professor: quan hệ one-many với class -- student: quan hệ many-many với class -- course: quan hệ one-many với class -- room: quan hệ one-one với class - -3. Hãy viết câu query để tìm: -- những cặp student-professor có dạy học nhau và số lớp mà họ có liên quan -- những course (distinct) mà 1 professor cụ thể đang dạy -- những course (distinct) mà 1 student cụ thể đang học -- điểm số là A, B, C, D, E, F tương đương với 10, 8, 6, 4, 2, 0 -- điểm số trung bình của 1 học sinh cụ thể (quy ra lại theo chữ cái, và xếp loại học lực (weak nếu avg < 5, average nếu >=5 < 8, good nếu >=8 ) -- điểm số trung bình của các class (quy ra lại theo chữ cái) -- điểm số trung bình của các course (quy ra lại theo chữ cái) - \ No newline at end of file diff --git a/application/app.go b/application/app.go new file mode 100644 index 0000000..758e8c7 --- /dev/null +++ b/application/app.go @@ -0,0 +1,53 @@ +package application + +import ( + "context" + "fmt" + "github.com/redis/go-redis/v9" + "net/http" + "time" +) + +type App struct { + router http.Handler + rbd *redis.Client +} + +func New() *App { + app := &App{rbd: redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0})} + app.loadRoutes() + return app +} + +func (a *App) Start(ctx context.Context) error { + server := &http.Server{ + Addr: ":3000", + Handler: a.router, + } + err := a.rbd.Ping(ctx).Err() + if err != nil { + return fmt.Errorf("Fail to connect redis: %w", err) + } + + fmt.Println("Starting server") + ch := make(chan error, 1) + go func() { + err = server.ListenAndServe() + if err != nil { + ch <- fmt.Errorf("Failed to start server: %w", err) + } + close(ch) + }() + + select { + case err = <-ch: + return err + case <-ctx.Done(): + timeout, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + return server.Shutdown(timeout) + } +} diff --git a/application/routes.go b/application/routes.go new file mode 100644 index 0000000..e40285f --- /dev/null +++ b/application/routes.go @@ -0,0 +1,44 @@ +package application + +import ( + "assignment/handler" + "assignment/repository" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "net/http" +) + +func (a *App) loadRoutes() { + router := chi.NewRouter() + router.Use(middleware.Logger) + + router.Get("/", func(w http.ResponseWriter, request *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + router.Route("/users", a.loadUserRoutes) + router.Route("/pings", a.loadPingRoutes) + a.router = router +} + +func (a *App) loadPingRoutes(route chi.Router) { + pingHandler := &handler.Ping{ + Repo: repository.RedisRepo{ + Client: a.rbd, + }} + route.Post("/", pingHandler.PingEndPoint) + route.Get("/top_ten", pingHandler.GetTopTenCount) + route.Get("/{username}", pingHandler.GetPingCount) +} + +func (a *App) loadUserRoutes(route chi.Router) { + userHandler := &handler.User{ + Repo: repository.RedisRepo{ + Client: a.rbd, + }} + + route.Post("/", userHandler.SignIn) + route.Get("/", userHandler.List) + route.Get("/user/{username}", userHandler.GetUserById) + route.Get("/session/{session}", userHandler.LoginBySession) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a5d7da8 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module assignment + +go 1.21 + +require ( + github.com/bsm/redislock v0.9.4 + github.com/go-chi/chi/v5 v5.0.10 + github.com/go-redis/redis_rate/v10 v10.0.1 + github.com/google/uuid v1.4.0 + github.com/redis/go-redis/v9 v9.3.0 +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f5dc490 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw= +github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-redis/redis_rate/v10 v10.0.1 h1:calPxi7tVlxojKunJwQ72kwfozdy25RjA0bCj1h0MUo= +github.com/go-redis/redis_rate/v10 v10.0.1/go.mod h1:EMiuO9+cjRkR7UvdvwMO7vbgqJkltQHtwbdIQvaBKIU= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= +github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler/ping.go b/handler/ping.go new file mode 100644 index 0000000..9ff0114 --- /dev/null +++ b/handler/ping.go @@ -0,0 +1,103 @@ +package handler + +import ( + "assignment/model" + "assignment/repository" + "encoding/json" + "errors" + "fmt" + "github.com/go-chi/chi/v5" + "net/http" +) + +type Ping struct { + Repo repository.RedisRepo +} + +func (p *Ping) PingEndPoint( + w http.ResponseWriter, + r *http.Request, +) { + err := p.Repo.PingEntryPoint(r.Context()) + if errors.Is(err, repository.CannotPingMultiply) { + fmt.Errorf("Cannot ping mutiply times: %w", err) + w.WriteHeader(http.StatusAlreadyReported) + return + } + + var username model.User + if err := json.NewDecoder(r.Body).Decode(&username); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + err = p.Repo.RateLimit(r.Context(), username.Username) + if errors.Is(err, repository.RedisError) { + fmt.Println("redis error:", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + if errors.Is(err, repository.TooManyRequest) { + fmt.Println("Too many request:", err) + w.WriteHeader(http.StatusTooManyRequests) + return + } + + err = p.Repo.CountPing(r.Context(), username.Username) + if err != nil { + fmt.Println("failed to count:", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + data, err := json.Marshal("Ping successfully!") + if err != nil { + fmt.Println("failed to marshal:", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(data) +} + +func (p *Ping) GetTopTenCount( + w http.ResponseWriter, + r *http.Request, +) { + topTen, err := p.Repo.GetTopCount(r.Context()) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + data, err := json.Marshal(topTen) + if err != nil { + fmt.Println("failed to marshal:", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(data) +} + +func (p *Ping) GetPingCount( + w http.ResponseWriter, + r *http.Request, +) { + username := chi.URLParam(r, "username") + count, err := p.Repo.GetPingCount(r.Context(), username) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if len(count) != 1 { + w.WriteHeader(http.StatusNotFound) + return + } + + data, err := json.Marshal(count) + if err != nil { + fmt.Println("failed to marshal:", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(data) +} diff --git a/handler/users.go b/handler/users.go new file mode 100644 index 0000000..fa65d31 --- /dev/null +++ b/handler/users.go @@ -0,0 +1,73 @@ +package handler + +import ( + "assignment/model" + "assignment/repository" + "encoding/json" + "fmt" + "github.com/go-chi/chi/v5" + "net/http" +) + +type User struct { + Repo repository.RedisRepo +} + +func (u *User) List( + w http.ResponseWriter, + r *http.Request, +) { + data, err := json.Marshal(model.Users) + if err != nil { + fmt.Errorf("error while encode users: %w", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(data) +} + +func (u *User) SignIn( + w http.ResponseWriter, + r *http.Request) { + var user *model.User + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + session, err := u.Repo.SignIn(r.Context(), user) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + data, err := json.Marshal(session) + if err != nil { + fmt.Println("failed to marshal:", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(data) +} + +func (u *User) LoginBySession(w http.ResponseWriter, r *http.Request) { + session := chi.URLParam(r, "session") + user, err := u.Repo.GetUserBySession(r.Context(), session) + if err != nil { + fmt.Println("Authenticated failed: ", err) + w.WriteHeader(http.StatusUnauthorized) + return + } + + data, err := json.Marshal(user) + if err != nil { + fmt.Println("Failed to marshal: ", err) + w.WriteHeader(http.StatusInternalServerError) + } + w.Write(data) +} + +func (u *User) GetUserById( + w http.ResponseWriter, + r *http.Request) { +} diff --git a/img.png b/img.png deleted file mode 100644 index 71bf231..0000000 Binary files a/img.png and /dev/null differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..7394809 --- /dev/null +++ b/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "assignment/application" + "context" + "fmt" + "os" + "os/signal" +) + +func main() { + app := application.New() + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + err := app.Start(ctx) + if err != nil { + fmt.Println("failed to start app:", err) + } +} diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..5bfd69d --- /dev/null +++ b/model/user.go @@ -0,0 +1,11 @@ +package model + +type User struct { + Username string `json:"username"` + Password string `json:"password"` +} + +var Users = []User{ + {Username: "user", Password: "123456"}, + {Username: "admin", Password: "admin"}, +} diff --git a/repository/redis.go b/repository/redis.go new file mode 100644 index 0000000..d932a56 --- /dev/null +++ b/repository/redis.go @@ -0,0 +1,106 @@ +package repository + +import ( + "assignment/model" + "context" + "errors" + "fmt" + "github.com/bsm/redislock" + "github.com/go-redis/redis_rate/v10" + "github.com/google/uuid" + "github.com/redis/go-redis/v9" + "slices" + "time" +) + +type RedisRepo struct { + Client *redis.Client +} + +var UserIsNotExist = errors.New("user is not exist") + +func (r *RedisRepo) SignIn(ctx context.Context, user *model.User) (string, error) { + index := slices.IndexFunc(model.Users, func(u model.User) bool { + return user.Password == u.Password && user.Username == u.Username + }) + if index == -1 { + return "", UserIsNotExist + } + session := uuid.NewString() + _, err := r.Client.SetNX(ctx, session, model.Users[index].Username, 600*time.Second).Result() + + if err != nil { + return "", fmt.Errorf("failed to set: %w", err) + } + return session, nil +} + +var CannotPingMultiply = errors.New("cannot ping multiply times") + +func (r *RedisRepo) PingEntryPoint(ctx context.Context) error { + redisLocker := redislock.New(r.Client) + lock, err := redisLocker.Obtain(ctx, "ping-api", 10*time.Second, nil) + if errors.Is(err, redislock.ErrNotObtained) { + return CannotPingMultiply + } + //r.Client.PFAdd(ctx) + defer lock.Release(ctx) + + time.Sleep(5 * time.Second) + return nil +} + +var RedisError = errors.New("Redis server is wrong") +var TooManyRequest = errors.New("Two many request") + +func (r *RedisRepo) RateLimit(ctx context.Context, username string) error { + limiter := redis_rate.NewLimiter(r.Client) + res, err := limiter.Allow(ctx, username, redis_rate.PerMinute(2)) + if err != nil { + return RedisError + } + + if res.Allowed == 0 { + return TooManyRequest + } + return nil +} + +func (r *RedisRepo) CountPing(ctx context.Context, username string) error { + err := r.Client.ZIncrBy(ctx, "ping_counter", 1, username).Err() + return err +} + +func (r *RedisRepo) GetPingCount(ctx context.Context, username string) ([]float64, error) { + count, err := r.Client.ZMScore(ctx, "ping_counter", username).Result() + return count, err +} + +func (r *RedisRepo) GetTopCount(ctx context.Context) ([]redis.Z, error) { + usernames, err := r.Client.ZRevRangeWithScores(ctx, "ping_counter", 0, 10).Result() + return usernames, err +} + +func (r *RedisRepo) GetUserBySession(ctx context.Context, session string) (model.User, error) { + value, err := r.Client.Get(ctx, session).Result() + if errors.Is(err, redis.Nil) { + return model.User{}, UserIsNotExist + } else if err != nil { + return model.User{}, fmt.Errorf("get user: %w", err) + } + + index := slices.IndexFunc(model.Users, func(u model.User) bool { + return value == u.Username + }) + user := model.Users[index] + return user, nil +} + +type FindAllPages struct { + Size uint + Offset uint +} + +func (r *RedisRepo) List(ctx context.Context) ([]model.User, error) { + return model.Users, nil +}