Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README Part II.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<div align="center">

# **Project Report: Part II**

COP5615: Distributed Operating Systems Principles
**Fall 2024**

<br><br><br>

**Nitin Goyal**
Email: [nitin.goyal@ufl.edu](mailto:nitin.goyal@ufl.edu)
Electrical and Computer Engineering
University of Florida

<br>

**Nov 24, 2024**

</div>

## 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 -

<img src="image-3.png" alt="SQlite schema" width="600" height="400">

## 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.
12 changes: 10 additions & 2 deletions actors/auth.actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 1 addition & 2 deletions actors/karma.actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down Expand Up @@ -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()})
}
Expand Down
2 changes: 2 additions & 0 deletions actors/user.actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Binary file added demo.mp4
Binary file not shown.
10 changes: 8 additions & 2 deletions handlers/auth.handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion handlers/post.handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions handlers/user.handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added image-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 28 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
}
}
1 change: 1 addition & 0 deletions models/post.models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
7 changes: 5 additions & 2 deletions models/user.models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{} {
Expand Down
Binary file added report_part_II.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion repositories/message.sqlite.repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion repositories/user.sqlite.repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down