feat: now simple users can only occupy one seat at a time

main
Antonio De Lucreziis 3 years ago
parent 66ea45b0a8
commit 9b6c7f4a55

@ -65,7 +65,7 @@ export async function createSeatWidget($roomGrid, roomId) {
})
// Listener for takeing or leaving a seat
$seat.addEventListener('click', () => {
$seat.addEventListener('click', async () => {
if (!user) {
location.href = '/login.html'
return
@ -76,14 +76,38 @@ export async function createSeatWidget($roomGrid, roomId) {
if (occupiedBy.length === 0) {
const confirmResponse = confirm('Occupare il posto?')
if (confirmResponse) {
$seat.classList.remove('libero')
Database.occupySeat(seatId)
try {
await Database.occupySeat(seatId)
$seat.classList.remove('libero')
$seat.classList.add('mio')
} catch (e) {
alert(e.toString())
}
}
} else if (occupiedBy.length === 1 && occupiedBy[0] === user.id) {
const answer = confirm('Lasciare veramente il posto?')
if (answer) {
$seat.classList.remove('mio')
Database.leaveSeat(seatId)
try {
await Database.leaveSeat(seatId)
$seat.classList.remove('mio')
$seat.classList.add('libero')
} catch (e) {
alert(e.toString())
}
}
} else if (
user.permissions.includes('admin') ||
user.permissions.includes('moderator')
) {
const answer = confirm(`Liberare veramente il posto di @${occupiedBy[0]}?`)
if (answer) {
try {
await Database.leaveSeat(seatId)
$seat.classList.remove('occupato')
$seat.classList.add('libero')
} catch (e) {
alert(e.toString())
}
}
} else {
alert('Posto già occupato!')

@ -26,9 +26,21 @@ export const Database = {
return seats
},
async occupySeat(seatId) {
await (await fetch(`/api/seat/occupy?id=${seatId}`, { method: 'POST' })).json()
const response = await fetch(`/api/seat/occupy?id=${seatId}`, { method: 'POST' })
if (!response.ok) {
throw new Error(await response.text())
}
await response.json()
},
async leaveSeat(seatId) {
await (await fetch(`/api/seat/leave?id=${seatId}`, { method: 'POST' })).json()
const response = await fetch(`/api/seat/leave?id=${seatId}`, { method: 'POST' })
if (!response.ok) {
throw new Error(await response.text())
}
await response.json()
},
}

@ -6,6 +6,9 @@ package db
import (
"errors"
"fmt"
"sync"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/util"
)
// ErrAlreadyExists is the error thrown from Store functions when an entity already exists.
@ -14,6 +17,11 @@ var ErrAlreadyExists = errors.New(`object already exists in database`)
// ErrDoesntExist is the error thrown from Store functions when an entity doesn't exist.
var ErrDoesntExist = errors.New(`object doesn't exist in database`)
const (
PermissionAdmin = "admin"
PermissionModerator = "moderator"
)
// Entities
// type FrontendUser struct {
@ -24,23 +32,12 @@ var ErrDoesntExist = errors.New(`object doesn't exist in database`)
// User represents a user in the database.
type User struct {
ID string `json:"id"`
Permissions []string `json:"permissions"`
ID string `json:"id"`
Permissions util.StringSet `json:"permissions"`
// passwordSaltHash string
}
// HasPermission checks if the user has a specific permission.
func (user User) HasPermission(neededPerm string) bool {
for _, perm := range user.Permissions {
if perm == neededPerm {
return true
}
}
return false
}
// Room represents a room in the database, a room has an id, a name and a collection of seatIDs of this room.
type Room struct {
ID string `json:"id"`
@ -49,6 +46,26 @@ type Room struct {
// gridRows, gridCols int
}
// Version 1:
//
// type SeatState struct {
// Occupied bool
// Anonymous bool
// UserID string
// }
// Version 2:
//
// type SeatState interface{ isSeatState() }
//
// type SeatNotOccupied struct{}
// type SeatOccupiedByUser struct{ UserID string }
// type SeatOccupiedByAnonymous struct{}
//
// func (s SeatNotOccupied) isSeatState()
// func (s SeatOccupiedByUser) isSeatState()
// func (s SeatOccupiedByAnonymous) isSeatState()
// Seat represents a seat in the database, it belongs to a single room and can be free or occupied by some user (referenced by userID).
type Seat struct {
ID string `json:"id"`
@ -81,6 +98,9 @@ type Seat struct {
// Store is the main interface for interacting with database implementations, for now the only implementation is "memDB".
type Store interface {
BeginTransaction()
EndTransaction()
CreateUser(user *User) error
CreateRoom(room *Room) error
CreateSeat(seat *Seat) error
@ -98,7 +118,7 @@ type Store interface {
GetRoomOccupiedSeats(roomID string) ([]string, error)
GetRoomFreeSeats(roomID string) ([]string, error)
GetUserSeat(userID string) ([]string, error)
GetUserSeats(userID string) (util.StringSet, error)
OccupySeat(seatID, userID string) error
FreeSeat(seatID string) error
@ -109,6 +129,8 @@ type Store interface {
// NewInMemoryStore creates an instance of memDB hidden behind the Store interface.
func NewInMemoryStore() Store {
db := &memDB{
&sync.Mutex{},
make(map[string]*User),
make(map[string]*Room),
make(map[string]*Seat),
@ -135,12 +157,12 @@ func NewInMemoryStore() Store {
db.users["aziis98"] = &User{
ID: "aziis98",
Permissions: []string{"admin"},
Permissions: util.NewStringSet(PermissionAdmin),
}
db.users["bachoseven"] = &User{
ID: "bachoseven",
Permissions: []string{"admin"},
Permissions: util.NewStringSet(PermissionAdmin),
}
return db

@ -1,17 +1,29 @@
package db
import "fmt"
import (
"fmt"
"sync"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/util"
)
// memDB is the first Store implementation used for testing.
type memDB struct {
// FIXME: Giusto per la cronaca fare le modifiche in questo modo alle mappe non è per niente thread safe, servirebbe come minimo usare un mutex per quando si scrive su una di queste variabili
// mutex *sync.Mutex
mutex *sync.Mutex
users map[string]*User
rooms map[string]*Room
seats map[string]*Seat
}
func (db *memDB) BeginTransaction() {
db.mutex.Lock()
}
func (db *memDB) EndTransaction() {
db.mutex.Unlock()
}
func (db *memDB) CreateUser(user *User) error {
if _, present := db.users[user.ID]; present {
return ErrAlreadyExists
@ -134,14 +146,14 @@ func (db *memDB) GetRoomFreeSeats(roomID string) ([]string, error) {
return seats, nil
}
func (db *memDB) GetUserSeat(userID string) ([]string, error) {
func (db *memDB) GetUserSeats(userID string) (util.StringSet, error) {
for _, seat := range db.seats {
if len(seat.OccupiedBy) > 0 && seat.OccupiedBy[0] == userID {
return []string{userID}, nil
return util.NewStringSet(seat.ID), nil
}
}
return []string{}, nil
return util.NewStringSet(), nil
}
func (db *memDB) OccupySeat(seatID, userID string) error {

@ -3,7 +3,6 @@ module git.phc.dm.unipi.it/aziis98/posti-dm/server
go 1.17
require (
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae
github.com/go-chi/chi/v5 v5.0.7
github.com/joho/godotenv v1.4.0
)

@ -1,5 +1,3 @@
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=

@ -37,7 +37,7 @@ func NewServer() *Server {
// FIXME: al momento quando la password è giusta creiamo tutti gli account necessari
err := database.CreateUser(&db.User{
ID: userID,
Permissions: []string{},
Permissions: make(util.StringSet),
})
if err != nil {
log.Printf(`got "%v" while trying to log as @%s`, err, userID)
@ -52,7 +52,7 @@ func NewServer() *Server {
return nil, err
}
return user.Permissions, nil
return user.Permissions.Elements(), nil
},
SessionTokenFromUser: func(userID string) (string, error) {
user, err := database.GetUser(userID)
@ -105,6 +105,10 @@ func (server *Server) setupRoutes() {
database := server.Database
auth := server.authService
// FIXME: in realtà tutte le routes che interagiscono con il db dovrebbero essere in transazione con
// database.BeginTransaction()
// defer database.EndTransaction()
// Authenticated Routes
api.Post("/login", func(w http.ResponseWriter, r *http.Request) {
@ -186,6 +190,9 @@ func (server *Server) setupRoutes() {
api.With(auth.LoggedMiddleware()).
Post("/seat/occupy", func(w http.ResponseWriter, r *http.Request) {
database.BeginTransaction()
defer database.EndTransaction()
userID, err := auth.RequestUser(r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
@ -210,8 +217,20 @@ func (server *Server) setupRoutes() {
return
}
// a seat can be occupied only if empty; simple users can occupy a seat only if they have occupied no other seat; admins and moderator (for now) can occupy even more than one seat (as occupied by an anonymous?) to fix the free seat count of the room
if len(seat.OccupiedBy) == 0 {
if err := database.OccupySeat(seatID[0], user.ID); err != nil {
userSeats, err := database.GetUserSeats(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !user.Permissions.HasAny(db.PermissionAdmin, db.PermissionModerator) && len(userSeats) > 0 {
http.Error(w, `you can occupy only one seat at a time`, http.StatusUnprocessableEntity)
return
}
if err := database.OccupySeat(seatID[0], userID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -249,10 +268,9 @@ func (server *Server) setupRoutes() {
return
}
// Check permissions
// Check permissions, if the place is occupied then only its owner, a moderator or an admin can clear it
if len(seat.OccupiedBy) > 0 {
if user.ID == seat.OccupiedBy[0] || user.HasPermission("admin") || user.HasPermission("moderator") {
if user.ID == seat.OccupiedBy[0] || user.Permissions.HasAny(db.PermissionAdmin, db.PermissionModerator) {
if err := database.FreeSeat(seatID[0]); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

@ -0,0 +1,77 @@
package util
import "encoding/json"
type present struct{}
// Waiting for Go 18...
// type Set[T comparable] map[T]present
type StringSet map[string]present
func NewStringSet(elements ...string) StringSet {
set := StringSet{}
for _, elem := range elements {
set.Add(elem)
}
return set
}
func (set StringSet) Has(value string) bool {
_, present := set[value]
return present
}
func (set StringSet) HasAny(values ...string) bool {
for _, value := range values {
if _, present := set[value]; present {
return true
}
}
return false
}
func (set StringSet) Contains(other StringSet) bool {
for v := range other {
if !set.Has(v) {
return false
}
}
return true
}
func (set StringSet) Add(value string) {
set[value] = present{}
}
func (set StringSet) Remove(value string) {
delete(set, value)
}
func (set StringSet) Elements() []string {
elements := []string{}
for elem := range set {
elements = append(elements, elem)
}
return elements
}
func (set StringSet) MarshalJSON() ([]byte, error) {
return json.Marshal(set.Elements())
}
func (set StringSet) UnmarshalJSON(data []byte) error {
elements := []string{}
if err := json.Unmarshal(data, &elements); err != nil {
return err
}
for _, v := range elements {
set.Add(v)
}
return nil
}
Loading…
Cancel
Save