feat: now simple users can only occupy one seat at a time
This commit is contained in:
@@ -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!')
|
||||
|
||||
+14
-2
@@ -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()
|
||||
},
|
||||
}
|
||||
|
||||
+38
-16
@@ -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
|
||||
|
||||
+18
-6
@@ -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=
|
||||
|
||||
+24
-6
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user