Major refactor into the /handlers system and wrote a bit of the architecture in the README

main
Antonio De Lucreziis 3 years ago
parent 660d76a748
commit d973502d11

@ -11,3 +11,53 @@ $ cd _frontend
$ npm run build $ npm run build
$ go build $ go build
``` ```
## Architettura
Moduli principali (cose che vengono eseguite e fanno cose di _business logic_)
- [`/main.go`](./main.go)
Entry point principale del server, qui vengono inizializzate concretamente tutte le dipendenze dei vari moduli (come database, router http e servizio di autenticazione).
Per ora usiamo un semplice database ed un semplice servizio di autenticazione per le sessioni di accesso che tengono tutto in memoria (più avanti passeremo a SQLite, tanto basterà semplicemente scrivere un'altra implementazione per `database.Database`)
- [`/routes`](./routes)
Questo modulo dipende **molto** dal router HTTP in quanto contiene tutte le route del server. Oltre questo dipende solo dal modulo `/handlers` e non sa nulla di `/database` e `/auth`.
- [`/handlers`](./handlers)
Questo modulo permette di testare tutta l'applicazione in modo isolato dal resto in quanto non sa nulla dei router HTTP e dipende solo da `/auth` e `/database` (ed anche da `/events`, `/model` e `/util` ma questi sono moduli "puri") che possono essere facilmente _mocked_.
Moduli secondari (o boh "terminali" nel senso che dipendono solo da cose esterne a questo progetto)
- [`/events`](./events)
Questo modulo fornisce una struttura dati di "EventBus" che permette di mandare e ricevere eventi all'interno dell'applicazione.
- [`/model`](./model)
Questo modulo contiene i modelli di tutte le strutture usate nel database ed alcuni metodi di servizio come `User.PublicUser()` che converte un `User` in `PublicUser` che rappresenta la versione "sicura" senza i campi segreti (come l'hash della password) dell'utente.
- [`/database`](./database)
L'interfaccia principale è `Database` e contiene tutte le operazioni possibili da fare sul database. Per ora c'è solo un'implementazione in memoria data da `*memDB`
- [`/auth`](./auth)
L'interfaccia principale è `AuthService` e contiene alcuni metodi per autenticare e registrare gli utenti e creare token di sessione. Per ora l'unica implementazione è `*memAuth` e dipende da un'istanza di `database.Database` e tiene i token di sessione in memoria.
- [`/util`](./util)
Questo modulo contiene alcune funzioni di utility e per ora anche varie funzioni di validazione dei form che arrivano dalla frontend, come validazione di username e password per la registrazione degli utenti.
Ed infine c'è il modulo che si occupa del far avanzare le partite e della risoluzione automatica.
- [`/lupus`](./lupus)
Questo modulo non dipende da nulla, l'idea è che conterrà le strutture per gestire lo stato delle partite e gli algoritmi per la risoluzione automatica di quest'ultime.
Al massimo potrebbe contenere alcune informazioni su come serializzare lo stato delle partite però per ora tutte le strutture qui dentro sono annotate per essere serializzate a JSON ed anche quando passeremo ad SQLite forse converrà fare sempre così.
TODO: Al massimo potrebbe dipendere da `/model` ma forse non serve.

@ -1,7 +0,0 @@
package database
import "fmt"
func OnPartitaPlayersChange(partitaUid string) string {
return fmt.Sprintf("partita[uid=%q].players", partitaUid)
}

@ -1 +0,0 @@
package events

@ -0,0 +1,83 @@
package handlers
import (
"github.com/aziis98/lupus-lite/auth"
"github.com/aziis98/lupus-lite/database"
"github.com/aziis98/lupus-lite/events"
"github.com/aziis98/lupus-lite/model"
"github.com/aziis98/lupus-lite/util"
)
type Handler interface {
DebugDatabase() any
UserForSession(token string) (model.User, error)
Login(username, password string) (string, error)
Register(username, password, password2 string) error
Partite() PartiteHandler
}
type server struct {
DB database.Database
Auth auth.AuthService
partiteHandler *partiteHandler
}
func NewServer(
db database.Database,
authServiceFunc func(database.Database) auth.AuthService) Handler {
auth := authServiceFunc(db)
s := &server{
DB: db,
Auth: auth,
}
s.partiteHandler = &partiteHandler{
server: s,
eventBus: events.NewEventBus(),
}
return s
}
func (s *server) DebugDatabase() any {
return s.DB
}
func (s *server) UserForSession(token string) (model.User, error) {
username, err := s.Auth.UserForSession(token)
if err != nil {
return model.User{}, err
}
user, err := s.DB.GetUser(username)
if err != nil {
return model.User{}, err
}
return user, nil
}
func (s *server) Login(username, password string) (string, error) {
return s.Auth.Login(username, password)
}
func (s *server) Register(username, password, password2 string) error {
if err := util.ValidateUsername(username); err != nil {
return err
}
if err := util.ValidatePasswords(password, password2); err != nil {
return err
}
return s.Auth.Register(username, password)
}
func (s *server) Partite() PartiteHandler {
return s.partiteHandler
}

@ -0,0 +1,81 @@
package handlers
import (
"fmt"
"github.com/aziis98/lupus-lite/events"
"github.com/aziis98/lupus-lite/lupus"
"github.com/aziis98/lupus-lite/model"
)
func OnPlayerJoin(partitaUid string) string {
return fmt.Sprintf("@join partita[uid=%q]", partitaUid)
}
type PartitaConfig struct {
NumGiocatori int
NumLupi int
NumFattucchiere int
NumGuardie int
NumCacciatori int
NumMedium int
NumVeggenti int
}
type PartiteHandler interface {
EventBus() *events.EventBus
Create(ownerId string, cfg PartitaConfig) (model.Partita, error)
GetPlayers(partitaUid string) ([]string, error)
Join(partitaUid, username string) error
}
type partiteHandler struct {
*server
eventBus *events.EventBus
}
func (p *partiteHandler) Create(ownerId string, cfg PartitaConfig) (model.Partita, error) {
return p.DB.CreatePartita(ownerId, model.PartitaConfig{
NumeroGiocatori: cfg.NumGiocatori,
NumeroPerRuolo: map[string]int{
lupus.Lupo.Uid: cfg.NumLupi,
lupus.Fattucchiera.Uid: cfg.NumFattucchiere,
lupus.Guardia.Uid: cfg.NumGuardie,
lupus.Cacciatore.Uid: cfg.NumCacciatori,
lupus.Medium.Uid: cfg.NumMedium,
lupus.Veggente.Uid: cfg.NumVeggenti,
},
})
}
func (p *partiteHandler) GetPlayers(partitaUid string) ([]string, error) {
partita, err := p.DB.GetPartita(partitaUid)
if err != nil {
return nil, err
}
return partita.Players, nil
}
func (p *partiteHandler) Join(partitaUid, username string) error {
partita, err := p.DB.GetPartita(partitaUid)
if err != nil {
return err
}
partita.Players = append(partita.Players, username)
if err := p.DB.UpdatePartita(partita); err != nil {
return err
}
p.eventBus.Dispatch(OnPlayerJoin(partitaUid), partita.Players)
return nil
}
func (p *partiteHandler) EventBus() *events.EventBus {
return p.eventBus
}

@ -6,10 +6,14 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/aziis98/lupus-lite/auth"
"github.com/aziis98/lupus-lite/database"
"github.com/aziis98/lupus-lite/handlers"
"github.com/aziis98/lupus-lite/routes" "github.com/aziis98/lupus-lite/routes"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/websocket/v2"
) )
func main() { func main() {
@ -17,7 +21,12 @@ func main() {
app := fiber.New() app := fiber.New()
server := routes.NewServer() db, err := database.NewInMemoryDB()
if err != nil {
panic(err)
}
h := handlers.NewServer(db, auth.NewInMemoryAuthService)
app.Use(logger.New()) app.Use(logger.New())
app.Use(recover.New()) app.Use(recover.New())
@ -25,14 +34,36 @@ func main() {
// Static files // Static files
app.Static("/", "./_frontend/dist") app.Static("/", "./_frontend/dist")
app.Route("/", server.EventRoute) // Enable WebSocket on routes ending with "/ws"
app.Route("/", server.PageRoutes) app.Use("/*/ws", func(c *fiber.Ctx) error {
app.Route("/api", server.ApiRoutes) log.Printf("Upgrading to websocket connection")
if websocket.IsWebSocketUpgrade(c) {
c.Locals("allowed", true)
return c.Next()
}
return fiber.ErrUpgradeRequired
})
//
// Routes
//
app.Route("/", func(r fiber.Router) {
routes.PageRoutes(r, h)
})
app.Route("/api", func(r fiber.Router) {
routes.ApiRoutes(r, h)
})
//
// Starting the server
//
if strings.HasPrefix(mode, "dev") { if strings.HasPrefix(mode, "dev") {
log.Printf(`Running dev server for frontend: "npm run dev"`) log.Printf(`Running dev server for frontend: "npm run dev"`)
err := (exec.Command("sh", "-c", "cd _frontend/ && npm run dev").Start()) err := exec.Command("sh", "-c", "cd _frontend/ && npm run dev").Start()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

@ -3,19 +3,15 @@ package routes
import ( import (
"encoding/json" "encoding/json"
"log" "log"
"strconv"
"time" "time"
"github.com/aziis98/lupus-lite/database" "github.com/aziis98/lupus-lite/handlers"
"github.com/aziis98/lupus-lite/lupus"
"github.com/aziis98/lupus-lite/model"
"github.com/aziis98/lupus-lite/util"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
func (s *Server) ApiRoutes(api fiber.Router) { func ApiRoutes(r fiber.Router, h handlers.Handler) {
api.Get("/status", func(c *fiber.Ctx) error { r.Get("/status", func(c *fiber.Ctx) error {
s, err := json.MarshalIndent(s.db, "", " ") s, err := json.MarshalIndent(h.DebugDatabase(), "", " ")
if err != nil { if err != nil {
return err return err
} }
@ -25,17 +21,17 @@ func (s *Server) ApiRoutes(api fiber.Router) {
return c.SendString("ok") return c.SendString("ok")
}) })
api.Post("/login", func(c *fiber.Ctx) error { r.Post("/login", func(c *fiber.Ctx) error {
var loginForm struct { var form struct {
Username string `form:"username"` Username string `form:"username"`
Password string `form:"password"` Password string `form:"password"`
} }
if err := c.BodyParser(&loginForm); err != nil { if err := c.BodyParser(&form); err != nil {
return err return err
} }
token, err := s.auth.Login(loginForm.Username, loginForm.Password) token, err := h.Login(form.Username, form.Password)
if err != nil { if err != nil {
return err return err
} }
@ -50,7 +46,8 @@ func (s *Server) ApiRoutes(api fiber.Router) {
return c.Redirect("/") return c.Redirect("/")
}) })
api.Post("/logout", func(c *fiber.Ctx) error { r.Post("/logout",
func(c *fiber.Ctx) error {
c.Cookie(&fiber.Cookie{ c.Cookie(&fiber.Cookie{
Name: "sid", Name: "sid",
Value: "", Value: "",
@ -61,77 +58,59 @@ func (s *Server) ApiRoutes(api fiber.Router) {
return c.SendString("ok") return c.SendString("ok")
}) })
api.Post("/register", func(c *fiber.Ctx) error { r.Post("/register",
var loginForm struct { func(c *fiber.Ctx) error {
var form struct {
Username string `form:"username"` Username string `form:"username"`
Password string `form:"password"` Password string `form:"password"`
Password2 string `form:"password2"` Password2 string `form:"password2"`
} }
if err := c.BodyParser(&loginForm); err != nil { if err := c.BodyParser(&form); err != nil {
return err
}
if err := util.ValidateUsername(loginForm.Username); err != nil {
return err
}
if err := util.ValidatePasswords(loginForm.Password, loginForm.Password2); err != nil {
return err return err
} }
if err := s.auth.Register(loginForm.Username, loginForm.Password); err != nil { if err := h.Register(form.Username, form.Password, form.Password2); err != nil {
return err return err
} }
return c.Redirect("/login") return c.Redirect("/login")
}) })
api.Post("/crea-partita", s.requireLogged, func(c *fiber.Ctx) error { r.Get("/user",
user := requestUser(c) RequireLogged(h),
func(c *fiber.Ctx) error {
return c.JSON(requestUser(c).PublicUser())
})
var form struct { r.Route("/partite", func(r fiber.Router) {
NumGiocatori string `form:"numero-giocatori"` r.Use(RequireLogged(h))
NumLupi string `form:"numero-lupi"`
NumFattucchiere string `form:"numero-fattucchiere"`
NumGuardie string `form:"numero-guardie"`
NumCacciatori string `form:"numero-cacciatori"`
NumMedium string `form:"numero-medium"`
NumVeggenti string `form:"numero-veggenti"`
}
if err := c.BodyParser(&form); err != nil {
return err
}
cfg := model.PartitaConfig{ ApiPartite(r, h.Partite())
NumeroPerRuolo: map[string]int{}, })
} }
if err := util.AtoiInto(form.NumGiocatori, &cfg.NumeroGiocatori); err != nil { func ApiPartite(r fiber.Router, p handlers.PartiteHandler) {
return err r.Get("/ws", BindWebsocketToEventBus(p.EventBus()))
}
// Questo codice magico scorre su questa "tabella" con colonne ruolo e stringa del ruolo nel form e popola la mappa NumeroPerRuolo da uid ruolo a numero per ruolo. r.Post("/", func(c *fiber.Ctx) error {
for _, r := range []struct { user := requestUser(c)
ruolo lupus.Ruolo
num string var form struct {
}{ NumGiocatori int `form:"numero-giocatori"`
{lupus.Lupo, form.NumLupi}, NumLupi int `form:"numero-lupi"`
{lupus.Fattucchiera, form.NumFattucchiere}, NumFattucchiere int `form:"numero-fattucchiere"`
{lupus.Guardia, form.NumGuardie}, NumGuardie int `form:"numero-guardie"`
{lupus.Cacciatore, form.NumCacciatori}, NumCacciatori int `form:"numero-cacciatori"`
{lupus.Medium, form.NumMedium}, NumMedium int `form:"numero-medium"`
{lupus.Veggente, form.NumVeggenti}, NumVeggenti int `form:"numero-veggenti"`
} {
num, err := strconv.Atoi(r.num)
if err != nil {
return err
} }
cfg.NumeroPerRuolo[r.ruolo.Uid] = num if err := c.BodyParser(&form); err != nil {
return err
} }
partita, err := s.db.CreatePartita(user.Username, cfg) partita, err := p.Create(user.Username, handlers.PartitaConfig(form))
if err != nil { if err != nil {
return err return err
} }
@ -139,38 +118,25 @@ func (s *Server) ApiRoutes(api fiber.Router) {
return c.JSON(partita) return c.JSON(partita)
}) })
api.Get("/user", s.requireLogged, func(c *fiber.Ctx) error { r.Get("/:partita/players", func(c *fiber.Ctx) error {
return c.JSON(requestUser(c).PublicUser())
})
api.Get("/partita/:partita/players", func(c *fiber.Ctx) error {
partitaUid := c.Params("partita") partitaUid := c.Params("partita")
partita, err := s.db.GetPartita(partitaUid) players, err := p.GetPlayers(partitaUid)
if err != nil { if err != nil {
return err return err
} }
return c.JSON(partita.Players) return c.JSON(players)
}) })
api.Get("/partita/:partita/join-partita", s.requireLogged, func(c *fiber.Ctx) error { r.Get("/:partita/join", func(c *fiber.Ctx) error {
user := requestUser(c) user := requestUser(c)
partitaUid := c.Params("partita") partitaUid := c.Params("partita")
partita, err := s.db.GetPartita(partitaUid) if err := p.Join(partitaUid, user.Username); err != nil {
if err != nil {
return err return err
} }
partita.Players = append(partita.Players, user.Username)
if err := s.db.UpdatePartita(partita); err != nil {
return err
}
s.eventBus.Dispatch(database.OnPartitaPlayersChange(partitaUid), partita.Players)
return c.SendString("ok") return c.SendString("ok")
}) })
} }

@ -3,7 +3,7 @@ package routes
import ( import (
"fmt" "fmt"
"github.com/aziis98/lupus-lite/auth" "github.com/aziis98/lupus-lite/handlers"
"github.com/aziis98/lupus-lite/model" "github.com/aziis98/lupus-lite/model"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -11,26 +11,27 @@ import (
const UserKey = "github.com/aziis98/lupus-lite/user" const UserKey = "github.com/aziis98/lupus-lite/user"
func requestUser(c *fiber.Ctx) *model.User { func requestUser(c *fiber.Ctx) *model.User {
return c.Locals(UserKey).(*model.User) user, ok := c.Locals(UserKey).(*model.User) // TODO: Check semantics of casting "nil" to pointer to struct
if !ok {
return nil
} }
func RequireLoggedMiddleware(auth auth.AuthService) fiber.Handler { return user
}
func RequireLogged(server handlers.Handler) fiber.Handler {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
token := c.Cookies("sid") token := c.Cookies("sid")
if token == "" { if token == "" {
return fmt.Errorf(`request has no session token`) return fmt.Errorf(`request has no session token`)
} }
username, err := auth.UserForSession(token) user, err := server.UserForSession(token)
if err != nil {
return err
}
user, err := auth.GetUser(username)
if err != nil { if err != nil {
return err return err
} }
// this is wrapped in a pointer to let "nil" mean no user logged in when calling "requestUser()"
c.Locals(UserKey, &user) c.Locals(UserKey, &user)
return c.Next() return c.Next()

@ -1,25 +1,34 @@
package routes package routes
import "github.com/gofiber/fiber/v2" import (
"github.com/aziis98/lupus-lite/handlers"
"github.com/gofiber/fiber/v2"
)
func (s *Server) PageRoutes(r fiber.Router) { func PageRoutes(r fiber.Router, h handlers.Handler) {
r.Get("/", func(c *fiber.Ctx) error { r.Get("/",
func(c *fiber.Ctx) error {
return c.SendFile("_frontend/dist/index.html") return c.SendFile("_frontend/dist/index.html")
}) })
r.Get("/login", func(c *fiber.Ctx) error { r.Get("/login",
func(c *fiber.Ctx) error {
return c.SendFile("_frontend/dist/login.html") return c.SendFile("_frontend/dist/login.html")
}) })
r.Get("/register", func(c *fiber.Ctx) error { r.Get("/register",
func(c *fiber.Ctx) error {
return c.SendFile("_frontend/dist/register.html") return c.SendFile("_frontend/dist/register.html")
}) })
r.Get("/crea-partita", func(c *fiber.Ctx) error { r.Get("/crea-partita",
func(c *fiber.Ctx) error {
return c.SendFile("_frontend/dist/crea-partita.html") return c.SendFile("_frontend/dist/crea-partita.html")
}) })
r.Get("/p/:partita", s.requireLogged, func(c *fiber.Ctx) error { r.Get("/p/:partita",
RequireLogged(h),
func(c *fiber.Ctx) error {
return c.SendFile("_frontend/dist/partita.html") return c.SendFile("_frontend/dist/partita.html")
}) })
} }

@ -1,37 +0,0 @@
package routes
import (
"log"
"github.com/aziis98/lupus-lite/auth"
"github.com/aziis98/lupus-lite/database"
"github.com/aziis98/lupus-lite/events"
"github.com/gofiber/fiber/v2"
)
type Server struct {
db database.Database
auth auth.AuthService
// Live Updates
eventBus *events.EventBus
// Utilities
requireLogged fiber.Handler
}
func NewServer() *Server {
db, err := database.NewInMemoryDB()
if err != nil {
log.Fatal(err)
}
auth := auth.NewInMemoryAuthService(db)
return &Server{
db: db,
auth: auth,
eventBus: events.NewEventBus(),
requireLogged: RequireLoggedMiddleware(auth),
}
}

@ -9,103 +9,6 @@ import (
"github.com/gofiber/websocket/v2" "github.com/gofiber/websocket/v2"
) )
// func (s *Server) websocketHandler(handler func(c *fiber.Ctx) (interface{}, error)) fiber.Handler {
// return websocket.New(func(c *websocket.Conn) {
// clientId := util.GenerateRandomString(10)
// partitaUid := c.Params("partita")
// log.Printf("[%v] Connection started", clientId)
// done := make(chan error)
// clientListener := s.eventBus.Subscribe(
// database.OnPartitaPlayerJoin(partitaUid),
// func(e interface{}) {
// partita, err := s.db.GetPartita(partitaUid)
// if err != nil {
// done <- err
// return
// }
// log.Printf(`[%v] Sent message`, clientId)
// if err := c.WriteJSON(partita.Players); err != nil {
// done <- err
// return
// }
// },
// )
// defer func() {
// s.eventBus.Unsubscribe(clientListener)
// log.Printf("[%v] Connection closed", clientId)
// }()
// for {
// _, _, err := c.ReadMessage()
// if err != nil {
// if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
// log.Printf("[%v] Error: %v", clientId, err)
// }
// return
// }
// select {
// case err := <-done:
// if err != nil {
// log.Printf(`[%v] Error: %v`, clientId, err)
// }
// return
// case <-time.After(1 * time.Second):
// }
// }
// })
// }
// func (s *Server) registerEventBinding(event string, process func(c *fiber.Ctx, e any) (any, error)) fiber.Handler {
// return websocket.New(func(c *websocket.Conn) {
// clientId := util.GenerateRandomString(10)
// log.Printf("[%v] Connection started", clientId)
// done := make(chan error)
// clientListener := s.eventBus.Subscribe(
// event,
// func(e interface{}) {
// partita, err := s.db.GetPartita(partitaUid)
// if err != nil {
// done <- err
// return
// }
// log.Printf(`[%v] Sent message`, clientId)
// if err := c.WriteJSON(partita.Players); err != nil {
// done <- err
// return
// }
// },
// )
// defer func() {
// s.eventBus.Unsubscribe(clientListener)
// log.Printf("[%v] Connection closed", clientId)
// }()
// for {
// _, _, err := c.ReadMessage()
// if err != nil {
// if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
// log.Printf("[%v] Error: %v", clientId, err)
// }
// return
// }
// select {
// case err := <-done:
// if err != nil {
// log.Printf(`[%v] Error: %v`, clientId, err)
// }
// return
// case <-time.After(1 * time.Second):
// }
// }
// })
// }
type clientMessage struct { type clientMessage struct {
Type string `json:"type"` Type string `json:"type"`
Event string `json:"event"` Event string `json:"event"`
@ -116,8 +19,8 @@ type serverMessage struct {
Data any `json:"data"` Data any `json:"data"`
} }
// registerEventBinder creates a WebSocket handler that can subscribe the socket to some events generated on the server, for example a message from the client like { type: "subscribe", event: "foo" } binds the connection to the event "foo" and forwards events and the associated data to client as JSON. // BindWebsocketToEventBus creates a WebSocket handler that can subscribe the socket to some events generated on the server, for example a message from the client like { type: "subscribe", event: "foo" } binds the connection to the event "foo" and forwards events and the associated data to client as JSON.
func registerEventBinder(eb *events.EventBus) fiber.Handler { func BindWebsocketToEventBus(eb *events.EventBus) fiber.Handler {
return websocket.New(func(c *websocket.Conn) { return websocket.New(func(c *websocket.Conn) {
clientId := util.GenerateRandomString(10) clientId := util.GenerateRandomString(10)
@ -172,15 +75,3 @@ func registerEventBinder(eb *events.EventBus) fiber.Handler {
} }
}) })
} }
func (s *Server) EventRoute(r fiber.Router) {
r.Use("/ws", func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
c.Locals("allowed", true)
return c.Next()
}
return fiber.ErrUpgradeRequired
})
r.Get("/ws", registerEventBinder(s.eventBus))
}

Loading…
Cancel
Save