feat: join room, rooms.Update transactional

This commit is contained in:
efim 2023-11-04 13:07:28 +00:00
parent b19dd2863b
commit 3a6fe28981
5 changed files with 130 additions and 28 deletions

View File

@ -3,6 +3,7 @@ package rooms
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log" "log"
@ -37,11 +38,10 @@ func (r *Room) UnmarshalBinary(data []byte) error {
// let's check whether it will be possible to save nested structs // let's check whether it will be possible to save nested structs
var ctx = context.Background()
type RoomManager interface { type RoomManager interface {
Get(roomName string) (Room, bool, error) Get(roomName string) (Room, bool, error)
Save(room Room) error Save(room Room) error
Update(ctx context.Context, roomName string, f func(fromRoom Room) (toRoom Room)) error
} }
const roomRedisPrefix = "room" const roomRedisPrefix = "room"
func roomNameToRedisId(roomName string) string { func roomNameToRedisId(roomName string) string {
@ -55,7 +55,7 @@ type RedisRM struct {
func (redisRM RedisRM) Get(roomName string) (Room, bool, error) { func (redisRM RedisRM) Get(roomName string) (Room, bool, error) {
var readRoom Room 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 { if err == redis.Nil {
return Room{}, false, nil return Room{}, false, nil
} }
@ -67,7 +67,43 @@ func (redisRM RedisRM) Get(roomName string) (Room, bool, error) {
return readRoom, true, nil 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 { 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 return err
} }

View File

@ -33,6 +33,10 @@ const authCookieName = "auth"
const loginPath = "/login" const loginPath = "/login"
type contextKey string type contextKey string
func getContextSession(ctx context.Context) sessions.SessionData {
return ctx.Value(contextKey("session")).(sessions.SessionData)
}
// checks sessionId from cookie // checks sessionId from cookie
// when non-zero session found - pass to next http.Hander // when non-zero session found - pass to next http.Hander
// when no session available - render same as login page and redirect to / // 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) w.WriteHeader(http.StatusBadRequest)
} }
rn := r.PostFormValue("roomName") roomName := r.PostFormValue("roomName")
rp := r.PostFormValue("roomPassword") roomPass := r.PostFormValue("roomPassword")
pn := r.PostFormValue("personalName") personName := r.PostFormValue("personalName")
pp := r.PostFormValue("personalPassword") personPass := r.PostFormValue("personalPassword")
room, _, err := roomsM.Get(rn) // a) get room data
room, _, err := roomsM.Get(roomName)
if err != nil { if err != nil {
log.Printf("/login/submit error getting room %s", rn) log.Printf("/login/join error getting room %s", roomName)
// return i guess w.WriteHeader(http.StatusBadRequest)
// TODO render error to be put in error place
return
} else { } else {
log.Printf("/login/submit found room %+v", room) log.Printf("/login/submit found room %+v", room)
} }
roomId := "room-name-actually" // would be taken from rooms interface from redis // b) check if room password OK
// would be either taken from room info on correct person pass or created if room.PasswordHash != roomPass {
personId := 111 log.Printf("/login/join bad room pass for %+v", room)
id, err := sessionSM.Save(roomId, personId) 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 { if err != nil {
log.Printf("/login/submit > error saving session %s", err) log.Printf("/login/submit > error saving session %s", err)
} }
http.SetCookie(w, &http.Cookie{
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) Name: authCookieName,
// i suppose here i'll need to Value: fmt.Sprint(newSessionId),
// a) check if room password OK Secure: true,
// b) get room data HttpOnly: true,
// c) check if such person exists, Path: "/",
// either create one, or check password })
// d) how should i monitor sessions? - space in redis log.Printf("is is %d. room things %s & %s, personal things %s and %s. \n found room %+v",
// so save session to redis and add cookie with sessionId 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", "/")
} }
} }

View File

@ -2,6 +2,7 @@ package routes
import ( import (
"embed" "embed"
"fmt"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
@ -16,17 +17,18 @@ var templateFs embed.FS
//go:embed static //go:embed static
var staticFilesFs embed.FS var staticFilesFs embed.FS
func RegisterRoutes(sessions sessions.SessionManagement, rooms rooms.RoomManager) { func RegisterRoutes(sessionsM sessions.SessionManagement, rooms rooms.RoomManager) {
// login page // login page
registerLoginRoutes(&templateFs, sessions, rooms) registerLoginRoutes(&templateFs, sessionsM, rooms)
// main page template // main page template
http.Handle("/", authedPageMiddleware( http.Handle("/", authedPageMiddleware(
sessions, sessionsM,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var templFile = "templates/index.gohtml" var templFile = "templates/index.gohtml"
session := getContextSession(r.Context())
tmpl := template.Must(template.ParseFS(templateFs, templFile)) tmpl := template.Must(template.ParseFS(templateFs, templFile))
err := tmpl.Execute(w, nil) err := tmpl.Execute(w, fmt.Sprintf("%+v", session))
if err != nil { if err != nil {
log.Printf("my error in executing template, huh\n %s", err) log.Printf("my error in executing template, huh\n %s", err)
} }

View File

@ -31,5 +31,6 @@
<h1>Hello</h1> <h1>Hello</h1>
<p>This is index</p> <p>This is index</p>
<p>Your session is {{ . }}</p>
</body> </body>
</html> </html>

View File

@ -34,6 +34,7 @@
</header> </header>
<section class="h-full grid place-content-center"> <section class="h-full grid place-content-center">
<form <form
id="loginForm"
class="grid grid-cols-2 place-content-center border border-black rounded p-4 gap-6" class="grid grid-cols-2 place-content-center border border-black rounded p-4 gap-6"
> >
<div id="roomInput" class="flex flex-col gap-4"> <div id="roomInput" class="flex flex-col gap-4">
@ -82,6 +83,11 @@
{{ end }} {{ end }}
{{ end }} {{ end }}
</form> </form>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('loginForm').reset();
});
</script>
</section> </section>
</main> </main>
</body> </body>