feat: join room, rooms.Update transactional
This commit is contained in:
parent
b19dd2863b
commit
3a6fe28981
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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", "/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue