diff --git a/README Part II.md b/README Part II.md new file mode 100644 index 0000000..0cb00e9 --- /dev/null +++ b/README Part II.md @@ -0,0 +1,67 @@ +
+ +# **Project Report: Part II** + +COP5615: Distributed Operating Systems Principles +**Fall 2024** + +


+ +**Nitin Goyal** +Email: [nitin.goyal@ufl.edu](mailto:nitin.goyal@ufl.edu) +Electrical and Computer Engineering +University of Florida + +
+ +**Nov 24, 2024** + +
+ +## Overview + +Part 2 of the project implements a rest interface for the reddit actor system. The actor system is deployed on port 8080 and the rest interface is deployed on port 5678. Protocol buffers are used to do the communication with the reddit actor system. + +## Usage + +### Running the engine + +- checkout to `./engine` directory +- Run `go mod tidy` to install dependencies +- Run `go build .` to build the project +- Run `./reddit-clone` to start the engine + +This will spin up an HTTP server on port 5678. The server will be ready to accept requests from the client. + +### Running the client + +Client is any program that could send a REST request to the server. I have used Postman as the client interface and to perform operations. But this could be integrated with any number of applications - web or mobile. + +Here is the link to the postman collection that could be used to test the HTTP server - + +`https://app.getpostman.com/join-team?invite_code=b6a8e7bba6908d5aeea6b5c5cf25a0af&target_code=20d964e315481a33e923775920c5a50c` + +The engine uses JWT tokens for authentication. The client should first register a user and then login to get the token. + +I haven't used any authorization header, the token is sent with the request payload (body), you can follow the postman collection or the protocol buffer definitions to determine the appropriate payload for each request. + +Since the data is persisted in a SQLite database, multiple clients can be spawned to test the system. The system is designed to be stateless, so the clients can be run in parallel. + +If the SQLite file is not already present the engine will initialize it first. Here is the view of the database - + +SQlite schema + +## Demo Video + +I implemented and tested the following functions of the reddit - + +1. User Registration, Authentication and Authorization +2. Subreddit Creation and Subscribe, Unsubscribe +3. Post Creation, Fetch Posts by Subreddit, by User (feed ordered by creation time), Upvote a post and changes the author karma +4. Comment Creation + +Here is the link to the video of me running the server and client to demonstrate the functionality of the system - + +[Demo Video](https://www.loom.com/share/c1569cf0f72f4b4f8f6b0ab7dc0fc354?sid=b1c01f79-a547-4fca-8c6e-0745c54a6d5e) + +The video is also available as .mp4 file with the submission. diff --git a/actors/auth.actor.go b/actors/auth.actor.go index be54f5d..1ae09f1 100644 --- a/actors/auth.actor.go +++ b/actors/auth.actor.go @@ -65,10 +65,18 @@ func (auth *AuthActor) RegisterNewUser(context actor.Context, actorMsg *proto.Re func (auth *AuthActor) LoginUser(context actor.Context, actorMsg *proto.LoginRequest) { token, err := auth.service.Login(actorMsg.Username, actorMsg.Password) + response := &proto.LoginResponse{ + Token: "", + Error: "", + } if err != nil { - context.Respond(&proto.LoginResponse{Error: err.Error()}) + fmt.Printf("Login failed: %v\n", err) + response.Error = err.Error() + } else { + response.Token = token } - context.Respond(&proto.LoginResponse{Token: token}) + fmt.Printf("Login response: %+v\n", response) + context.Respond(response) } func (auth *AuthActor) ValidateToken(context actor.Context, actorMsg *proto.TokenValidationRequest) { diff --git a/actors/karma.actor.go b/actors/karma.actor.go index 3475c05..0816887 100644 --- a/actors/karma.actor.go +++ b/actors/karma.actor.go @@ -15,7 +15,7 @@ type KarmaActor struct { karmaService *services.KarmaService } -func NewKarmaActor(userRepo *repositories.SqliteUserRepository) *KarmaActor { +func NewKarmaActor(userRepo repositories.UserRepository) *KarmaActor { return &KarmaActor{ karmaService: services.NewKarmaService(userRepo), } @@ -52,7 +52,6 @@ func (karma *KarmaActor) UpdateKarma(context actor.Context, actorMsg *proto.Karm context.Respond(&proto.KarmaResponse{Error: "Invalid token"}) } else { fmt.Println("Token validated successfully") - // update karma if err := karma.karmaService.UpdateKarma(uint(actorMsg.UserId), int(actorMsg.Amount)); err != nil { context.Respond(&proto.KarmaResponse{Error: err.Error()}) } diff --git a/actors/user.actor.go b/actors/user.actor.go index 0737789..679e05b 100644 --- a/actors/user.actor.go +++ b/actors/user.actor.go @@ -75,11 +75,13 @@ func (user *UserActor) GetMessages(context actor.Context, actorMsg *proto.GetMes if !validationResponse.Valid || !ok { context.Respond(&proto.SendMessageResponse{Error: "Invalid token"}) } else { + print("\nToken validated successfully\n") if userMessages, err := user.messageService.GetMessages(validationResponse.Claims.UserId, actorMsg.ToId); err != nil { fmt.Printf("Error getting messages: %v\n", err) fmt.Printf("User ID: %v, To ID: %v\n", validationResponse.Claims.UserId, actorMsg.ToId) context.Respond(&proto.GetMessagesResponse{Error: err.Error()}) } else { + print("Messages retrieved successfully\n") protoMessages := make([]*proto.Message, len(userMessages)) for i, msg := range userMessages { protoMessages[i] = &proto.Message{ diff --git a/demo.mp4 b/demo.mp4 new file mode 100644 index 0000000..790e63a Binary files /dev/null and b/demo.mp4 differ diff --git a/handlers/auth.handlers.go b/handlers/auth.handlers.go index 9a3f574..1050b7c 100644 --- a/handlers/auth.handlers.go +++ b/handlers/auth.handlers.go @@ -57,14 +57,20 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request, rootConte http.Error(w, fmt.Sprintf("Error getting response: %v", err), http.StatusInternalServerError) return } - loginResponse, ok := result.(*proto.LoginResponse) + if !ok { http.Error(w, "Invalid response from actor", http.StatusInternalServerError) return } if loginResponse.Error != "" { - http.Error(w, loginResponse.Error, http.StatusInternalServerError) + if loginResponse.Error == "user not found" { + http.Error(w, loginResponse.Error, http.StatusNotFound) + } else if loginResponse.Error == "invalid password" { + http.Error(w, loginResponse.Error, http.StatusUnauthorized) + } else { + http.Error(w, loginResponse.Error, http.StatusInternalServerError) + } return } diff --git a/handlers/post.handlers.go b/handlers/post.handlers.go index daae6d4..c5eb043 100644 --- a/handlers/post.handlers.go +++ b/handlers/post.handlers.go @@ -75,7 +75,7 @@ func (h *Handler) GetPostsBySubredditHandler(w http.ResponseWriter, r *http.Requ postResponse, ok := res.(*proto.GetPostsBySubredditResponse) if ok && postResponse == nil { - http.Error(w, "Post creation failed.", http.StatusInternalServerError) + http.Error(w, "Failed to fetch posts.", http.StatusInternalServerError) return } else { json.NewEncoder(w).Encode(postResponse) diff --git a/handlers/user.handlers.go b/handlers/user.handlers.go index 3b6877f..6e26385 100644 --- a/handlers/user.handlers.go +++ b/handlers/user.handlers.go @@ -46,6 +46,7 @@ func (h* Handler) GetMessagesHandler(w http.ResponseWriter, r *http.Request, roo return } response, ok := res.(*proto.GetMessagesResponse) + // print the response if ok && response.Error != "" { http.Error(w, response.Error, http.StatusInternalServerError) return diff --git a/image-3.png b/image-3.png new file mode 100644 index 0000000..3677888 Binary files /dev/null and b/image-3.png differ diff --git a/main.go b/main.go index 26df1d1..eb306f1 100644 --- a/main.go +++ b/main.go @@ -39,21 +39,21 @@ func main() { commentRepo := repositories.NewCommentRepository(db) // setup actor system authProps := actor.PropsFromProducer(func() actor.Actor { - return actors.NewAuthActor(userRepo, "chanduKeChacha") - }) + return actors.NewAuthActor(userRepo, "chanduKeChacha") + }) karmaProps := actor.PropsFromProducer(func() actor.Actor { return actors.NewKarmaActor(userRepo) }) - userProps := actor.PropsFromProducer(func()actor.Actor { + userProps := actor.PropsFromProducer(func() actor.Actor { return actors.NewUserActor(msgRepo) }) - subProps := actor.PropsFromProducer(func()actor.Actor { + subProps := actor.PropsFromProducer(func() actor.Actor { return actors.NewSubredditActor(subRepo) }) - postProps := actor.PropsFromProducer(func()actor.Actor { + postProps := actor.PropsFromProducer(func() actor.Actor { return actors.NewPostActor(postRepo) }) - commentProps := actor.PropsFromProducer(func()actor.Actor { + commentProps := actor.PropsFromProducer(func() actor.Actor { return actors.NewCommentActor(commentRepo) }) // .. add more actor props here @@ -65,37 +65,42 @@ func main() { commentKind := cluster.NewKind("Comment", commentProps) // .. add more actor props here - kinds := []*cluster.Kind{authKind, karmaKind, userKind, subKind, postKind, commentKind} // append more kinds here + kinds := []*cluster.Kind{authKind, karmaKind, userKind, subKind, postKind, commentKind} // append more kinds here // Distributed hash lookup lookup := disthash.New() - - // New cluster definition + + // New cluster definition config := remote.Configure("127.0.0.1", 8080) provider := automanaged.NewWithConfig(1*time.Second, 6331, "localhost:6331") clusterConfig := cluster.Configure("reddit-cluster", provider, lookup, config, cluster.WithKinds(kinds...)) system := actor.NewActorSystem() cluster := cluster.New(system, clusterConfig) - cluster.StartMember() + cluster.StartMember() // shutdown later - defer cluster.Shutdown(true) + defer cluster.Shutdown(true) rootContext := system.Root // declare HTTP handler to use cluster instead of actor system - handler := handlers.NewHandler(cluster) + handler := handlers.NewHandler(cluster) http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.RegisterHandler(w, r, rootContext) }) http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.LoginHandler(w, r, rootContext) }) http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.LogoutHandler(w, r, rootContext) }) http.HandleFunc("/user/karma", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.KarmaHandler(w, r, rootContext) }) http.HandleFunc("/user/messages", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) fmt.Printf("Message request received: %s\n", r.Method) if r.Method == http.MethodGet { handler.GetMessagesHandler(w, r, rootContext) @@ -106,41 +111,47 @@ func main() { } }) http.HandleFunc("/user/subreddits", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.CreateSubredditHandler(w, r, rootContext) }) http.HandleFunc("/user/subreddits/subscribe", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.SubscribeSubredditHandler(w, r, rootContext) }) http.HandleFunc("/user/subreddits/unsubscribe", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.UnsubscribeSubredditHandler(w, r, rootContext) }) http.HandleFunc("/post/create", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.CreatePostHandler(w, r, rootContext) }) http.HandleFunc("/post/get", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.GetPostHandler(w, r, rootContext) }) http.HandleFunc("/post/get/user", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.GetPostsByUserHandler(w, r, rootContext) }) http.HandleFunc("/post/get/subreddit", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.GetPostsBySubredditHandler(w, r, rootContext) }) http.HandleFunc("/post/upvote", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.UpdatePostVoteHandler(w, r, rootContext) }) http.HandleFunc("/comment/create", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request received: %s %s\n", r.Method, r.URL.Path) handler.CreateCommentHandler(w, r, rootContext) }) - http.HandleFunc("/comment/get", func(w http.ResponseWriter, r *http.Request) { - handler.GetCommentHandler(w, r, rootContext) - }) - http.ListenAndServe(":5678", nil) + http.ListenAndServe(":5678", nil) // Run till a signal comes finish := make(chan os.Signal, 1) signal.Notify(finish, os.Interrupt, os.Kill) <-finish -} \ No newline at end of file +} diff --git a/models/post.models.go b/models/post.models.go index b8ae27a..2b9a50a 100644 --- a/models/post.models.go +++ b/models/post.models.go @@ -12,6 +12,7 @@ type Post struct { CreatedAt time.Time `gorm:"not null"` UpdatedAt time.Time `gorm:"not null"` CommentCount int64 `gorm:"default:0"` + Votes int64 `gorm:"default:0"` // Relationships Author User `gorm:"foreignKey:AuthorID"` diff --git a/models/user.models.go b/models/user.models.go index f08303f..11521d0 100644 --- a/models/user.models.go +++ b/models/user.models.go @@ -81,9 +81,12 @@ func (u *User) HashPassword() error { return nil } -func (u *User) CheckPassword(providedPassword string) bool { +func (u *User) CheckPassword(providedPassword string) (bool, error) { err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(providedPassword)) - return err == nil + if err != nil { + return false, err + } + return true, nil } func (u *User) SafeUser() map[string]interface{} { diff --git a/report_part_II.pdf b/report_part_II.pdf new file mode 100644 index 0000000..fabf906 Binary files /dev/null and b/report_part_II.pdf differ diff --git a/repositories/message.sqlite.repository.go b/repositories/message.sqlite.repository.go index 6d80f73..5af057f 100644 --- a/repositories/message.sqlite.repository.go +++ b/repositories/message.sqlite.repository.go @@ -24,7 +24,7 @@ func (r *SqliteMessageRepository) SendMessage(text string, fromId, toId uint64) func (r *SqliteMessageRepository) GetMessages(fromId, toId uint64) ([]*models.Message, error) { var messages []*models.Message - if err := r.db.Where("from_id = ? AND to_id = ?", fromId, toId).Find(&messages).Error; err != nil { + if err := r.db.Where("from_id = ? AND to_id = ? OR from_id = ? AND to_id = ?", fromId, toId, toId, fromId).Order("created_at desc").Find(&messages).Error; err != nil { return nil, err } return messages, nil diff --git a/repositories/user.sqlite.repository.go b/repositories/user.sqlite.repository.go index b393bd8..fefa9e6 100644 --- a/repositories/user.sqlite.repository.go +++ b/repositories/user.sqlite.repository.go @@ -68,7 +68,8 @@ func (r *SqliteUserRepository) CheckPassword(username string, password string) ( user, err := r.GetUserByUsername(username); if err != nil { return nil, errors.New("user not found") } - if !user.CheckPassword(password) { + isValid, err := user.CheckPassword(password) + if err != nil || !isValid { return nil, errors.New("invalid password") } // save timestamp of last login