diff --git a/rooms/rooms_manager.go b/rooms/rooms_manager.go index 7292feb..1831e44 100644 --- a/rooms/rooms_manager.go +++ b/rooms/rooms_manager.go @@ -3,6 +3,7 @@ package rooms import ( "context" "encoding/json" + "errors" "fmt" "log" @@ -37,11 +38,10 @@ func (r *Room) UnmarshalBinary(data []byte) error { // let's check whether it will be possible to save nested structs -var ctx = context.Background() - type RoomManager interface { Get(roomName string) (Room, bool, error) Save(room Room) error + Update(ctx context.Context, roomName string, f func(fromRoom Room) (toRoom Room)) error } const roomRedisPrefix = "room" func roomNameToRedisId(roomName string) string { @@ -55,7 +55,7 @@ type RedisRM struct { func (redisRM RedisRM) Get(roomName string) (Room, bool, error) { var readRoom Room - err := redisRM.Rdb.Get(ctx, roomNameToRedisId(roomName)).Scan(&readRoom) + err := redisRM.Rdb.Get(context.TODO(), roomNameToRedisId(roomName)).Scan(&readRoom) if err == redis.Nil { return Room{}, false, nil } @@ -67,7 +67,43 @@ func (redisRM RedisRM) Get(roomName string) (Room, bool, error) { return readRoom, true, nil } +var maxRetries int = 20 +func (redisRM RedisRM) Update(ctx context.Context, roomName string, f func(fromRoom Room) (toRoom Room)) error { + // transactional function + roomKey := roomNameToRedisId(roomName) + txf := func (tx *redis.Tx) error { + var savedRoom Room + err := tx.Get(ctx, roomKey).Scan(&savedRoom) + if err != nil { + return err + } + + room := f(savedRoom) + + _, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.Set(ctx, roomKey, &room, 0) + return nil + }) + + return err + } + for i := 0; i < maxRetries; i++ { + err := redisRM.Rdb.Watch(ctx, txf, roomKey) + if err == nil { + return nil // success + } + if err == redis.TxFailedErr { + // optimistic lock will keep spinning + continue + } + // non tx errror are returned, including redis.Nil + return err + } + + return errors.New("update reached maximum amount of retries") +} + func (redisRM RedisRM) Save(room Room) error { - err := redisRM.Rdb.Set(ctx, roomNameToRedisId(room.Name), &room, 0).Err() // maybe even set expiration? + err := redisRM.Rdb.Set(context.TODO(), roomNameToRedisId(room.Name), &room, 0).Err() // maybe even set expiration? return err } diff --git a/routes/login_page.go b/routes/login_page.go index 0b632a3..3276e5a 100644 --- a/routes/login_page.go +++ b/routes/login_page.go @@ -33,6 +33,10 @@ const authCookieName = "auth" const loginPath = "/login" type contextKey string +func getContextSession(ctx context.Context) sessions.SessionData { + return ctx.Value(contextKey("session")).(sessions.SessionData) +} + // checks sessionId from cookie // when non-zero session found - pass to next http.Hander // when no session available - render same as login page and redirect to / @@ -168,35 +172,88 @@ func joinRoomHandler( templateFs *embed.FS, w.WriteHeader(http.StatusBadRequest) } - rn := r.PostFormValue("roomName") - rp := r.PostFormValue("roomPassword") - pn := r.PostFormValue("personalName") - pp := r.PostFormValue("personalPassword") + roomName := r.PostFormValue("roomName") + roomPass := r.PostFormValue("roomPassword") + personName := r.PostFormValue("personalName") + personPass := r.PostFormValue("personalPassword") - room, _, err := roomsM.Get(rn) + // a) get room data + room, _, err := roomsM.Get(roomName) if err != nil { - log.Printf("/login/submit error getting room %s", rn) - // return i guess + log.Printf("/login/join error getting room %s", roomName) + w.WriteHeader(http.StatusBadRequest) + // TODO render error to be put in error place + return } else { log.Printf("/login/submit found room %+v", room) } - roomId := "room-name-actually" // would be taken from rooms interface from redis - // would be either taken from room info on correct person pass or created - personId := 111 - id, err := sessionSM.Save(roomId, personId) + // b) check if room password OK + if room.PasswordHash != roomPass { + log.Printf("/login/join bad room pass for %+v", room) + w.WriteHeader(http.StatusForbidden) + // TODO render error to be put in error place + return + } + + var person rooms.Person + for _, participant := range room.Paricipants { + if participant.Name == personName { + person = participant + } + } + // c) check if such person exists, + // knownPerson, found := + // check the password + if (person != rooms.Person{}) && person.PasswordHash != personPass { + log.Printf("/login/join bad person pass for %+s", person.Name) + w.WriteHeader(http.StatusForbidden) + // TODO render error to be put in error place + return + } + // person joining for thethe first time + if (person == rooms.Person{}) { + log.Printf("/login/join room pass correct, new person joins") + // creating a new person with provided password hash + person = rooms.Person{ + Name: personName, + PasswordHash: personPass, + PersonId: rand.Int(), + } + err := roomsM.Update(context.TODO(), room.Name, func(fromRoom rooms.Room) (toRoom rooms.Room) { + toRoom = fromRoom + toRoom.Paricipants = append(toRoom.Paricipants, person) + return toRoom + }) + if err != nil { + log.Printf("/login/join problem adding person to room", person.Name) + w.WriteHeader(http.StatusInternalServerError) + // TODO render error to be put in error place + // with message try again + return + } + } + // TODO handle context and cancells, with separate function that writeds new updated room + // now we have room and person, can create a session + // and we've checked password + + newSessionId, err := sessionSM.Save(room.Name, person.PersonId) if err != nil { log.Printf("/login/submit > error saving session %s", err) } - - fmt.Fprintf(w, "is is %d. room things %s & %s, personal things %s and %s. \n found room %+v", id, rn, rp, pn, pp, room) - // i suppose here i'll need to - // a) check if room password OK - // b) get room data - // c) check if such person exists, - // either create one, or check password - // d) how should i monitor sessions? - space in redis - // so save session to redis and add cookie with sessionId + http.SetCookie(w, &http.Cookie{ + Name: authCookieName, + Value: fmt.Sprint(newSessionId), + Secure: true, + HttpOnly: true, + Path: "/", + }) + log.Printf("is is %d. room things %s & %s, personal things %s and %s. \n found room %+v", + newSessionId, roomName, roomPass, personName, personPass, room, + ) + // TODO render what? index page with some data passed? + // or, what? i could just redirect to / for now + w.Header().Add("HX-Redirect", "/") } } diff --git a/routes/routes.go b/routes/routes.go index 7f7385d..74f913a 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -2,6 +2,7 @@ package routes import ( "embed" + "fmt" "html/template" "log" "net/http" @@ -16,17 +17,18 @@ var templateFs embed.FS //go:embed static var staticFilesFs embed.FS -func RegisterRoutes(sessions sessions.SessionManagement, rooms rooms.RoomManager) { +func RegisterRoutes(sessionsM sessions.SessionManagement, rooms rooms.RoomManager) { // login page - registerLoginRoutes(&templateFs, sessions, rooms) + registerLoginRoutes(&templateFs, sessionsM, rooms) // main page template http.Handle("/", authedPageMiddleware( - sessions, + sessionsM, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var templFile = "templates/index.gohtml" + session := getContextSession(r.Context()) tmpl := template.Must(template.ParseFS(templateFs, templFile)) - err := tmpl.Execute(w, nil) + err := tmpl.Execute(w, fmt.Sprintf("%+v", session)) if err != nil { log.Printf("my error in executing template, huh\n %s", err) } diff --git a/routes/templates/index.gohtml b/routes/templates/index.gohtml index 8446c1a..553d718 100644 --- a/routes/templates/index.gohtml +++ b/routes/templates/index.gohtml @@ -31,5 +31,6 @@

Hello

This is index

+

Your session is {{ . }}

diff --git a/routes/templates/login.gohtml b/routes/templates/login.gohtml index 7e48b28..7ca604a 100644 --- a/routes/templates/login.gohtml +++ b/routes/templates/login.gohtml @@ -34,6 +34,7 @@
@@ -82,6 +83,11 @@ {{ end }} {{ end }} +