diff --git a/server/auth/auth.go b/server/auth/auth.go index 561a461..216ad1c 100644 --- a/server/auth/auth.go +++ b/server/auth/auth.go @@ -1,18 +1,21 @@ +// This package provides a utility for managing session cookies. +// +// The main struct is "AuthService" that provides a pluggable system for login and logout of users, creating and storing their session tokens and middlewares for accepting only logged users or ones with some specified permissions. package auth import ( "errors" "net/http" "time" - - "git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil" ) var ( ErrNoUserForSession = errors.New(`no user for session token`) ) + var SessionCookieName = "session" // TODO: Make configurable +// AuthMiddlewareConfig configures the middleware to only accept logged users (if "RequireLogged" is true) and with certain permissions (user must have all permissions inside "WithPermissions") type AuthMiddlewareConfig struct { // RequireLogged rejects not logged users if true RequireLogged bool @@ -21,52 +24,52 @@ type AuthMiddlewareConfig struct { WithPermissions []string } -type Authenticator interface { - // Login checks user credentials and adds a session cookie to the user if successfull - Login(w http.ResponseWriter, r *http.Request, userID, password string) - // Logout clears the user session cookies (by setting the session cookie timeout to 0) - Logout(w http.ResponseWriter) +// // Authenticator is the spec of this library +// type Authenticator interface { +// // Login checks user credentials and adds a session cookie to the user if successfull +// Login(w http.ResponseWriter, r *http.Request, userID, password string) +// // Logout clears the user session cookies (by setting the session cookie timeout to 0) +// Logout(w http.ResponseWriter) - // Middleware is a configurable middleware to authenticate http routes based on logged status and permissions - Middleware(*AuthMiddlewareConfig) func(http.Handler) http.Handler - // LoggedMiddleware accepts all logged users without checking for specific permissions - LoggedMiddleware() func(http.Handler) http.Handler +// // Middleware is a configurable middleware to authenticate http routes based on logged status and permissions +// Middleware(*AuthMiddlewareConfig) func(http.Handler) http.Handler +// // LoggedMiddleware accepts all logged users without checking for specific permissions +// LoggedMiddleware() func(http.Handler) http.Handler - // RequestUser returns the userID for this cookie session token - RequestUser(r *http.Request) (string, error) -} +// // RequestUser returns the userID for this cookie session token +// RequestUser(r *http.Request) (string, error) +// } -var _ Authenticator = &AuthService{} +// var _ Authenticator = &AuthService{} +// AuthService handles cookies, authentication and authorization of http routes by providing middlewares, logint/logout methods, user sessions and retriving the userID of an authenticated request. type AuthService struct { - // CheckUserPassword + // CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser" CheckUserPassword func(userID string, password string) error // GetUserPermissions gets the list of permissions for this user UserPermissions func(userID string) ([]string, error) - // SessionTokenFromUser returns a session token that represents this user + // SessionTokenFromUser returns a (new) session token that represents this user SessionTokenFromUser func(userID string) (string, error) // UserFromSessionToken returns the corresponing user for this session token or "auth.ErrNoUserForSession" UserFromSessionToken func(session string) (string, error) // AuthenticationFailed handles failed authentications AuthenticationFailed func(error) http.Handler - // OtherError handles other errors OtherError func(error) http.Handler } -func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, password string) { +// Login tries to login a user given its id and password +func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, password string) error { if err := auth.CheckUserPassword(userID, password); err != nil { - auth.AuthenticationFailed(err).ServeHTTP(w, r) - return + return err } token, err := auth.SessionTokenFromUser(userID) if err != nil { - auth.OtherError(err).ServeHTTP(w, r) - return + return err } http.SetCookie(w, &http.Cookie{ @@ -76,9 +79,10 @@ func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, p Expires: time.Now().Add(7 * 24 * time.Hour), // TODO: Make configurable }) - httputil.WriteJSON(w, "ok") + return nil } +// Logout clears the session cookie from a request effectivly logging out the user for future requests func (auth *AuthService) Logout(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: SessionCookieName, @@ -86,11 +90,9 @@ func (auth *AuthService) Logout(w http.ResponseWriter) { Value: "", Expires: time.Now(), }) - - httputil.WriteJSON(w, "ok") } -// Middleware checks if the user is logged or not and if it has all fo the permissions set in "config.WithPermissions" +// Middleware checks if the user is logged or not and if the user has all the permissions set in "config.WithPermissions" func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -160,7 +162,11 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand } } -// LoggedMiddleware is a shortcut for "Middleware(*AuthMiddlewareConfig)" that checks if a user is logged, no extra permissions are checked +// LoggedMiddleware is a shortcut for +// +// Middleware(*AuthMiddlewareConfig) +// +// that checks if a user is logged, no extra permissions are checked func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler { return auth.Middleware(&AuthMiddlewareConfig{ RequireLogged: true, @@ -168,7 +174,12 @@ func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler { }) } -// RequestUser retrives the "userID" from the given request based on the cookie session token +// RequestUser retrives the "userID" from the given request based on the cookie session token. +// When generics arrive this will become something like +// +// func (auth *AuthService[U]) RequestUser(r *http.Request) *U +// +// and will just return nil if no user is present. func (auth *AuthService) RequestUser(r *http.Request) (string, error) { cookie, err := r.Cookie(SessionCookieName) if err != nil { diff --git a/server/config/config.go b/server/config/config.go index 6efc7b3..bd11427 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,3 +1,4 @@ +// Loads ".env" file and provides environment variables as global values. package config import ( @@ -8,11 +9,16 @@ import ( "github.com/joho/godotenv" ) +// Mode represents the MODE environment variable var Mode string +// Host represents the HOST environment variable var Host string + +// Url represents the URL environment variable var Url string +// Email represents the EMAIL environment variable var Email string func loadEnv(target *string, name, defaultValue string) { @@ -25,6 +31,7 @@ func loadEnv(target *string, name, defaultValue string) { log.Printf("%s = %v", name, *target) } +// Load calls "godotenv.Load()" and sets all the above exported variables func Load() { godotenv.Load() diff --git a/server/db/database.go b/server/db/database.go index a27412f..21ade98 100644 --- a/server/db/database.go +++ b/server/db/database.go @@ -1,3 +1,6 @@ +// This package contains the database interface and an in memory implementation. +// +// For the in-memory implementation see the "NewInMemoryStore()" function. package db import ( @@ -5,16 +8,29 @@ import ( "fmt" ) +// ErrAlreadyExists is the error thrown from Store functions when an entity already exists. 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`) // Entities +// type FrontendUser struct { +// Anonymous bool `json:"anonymous,omitempty"` +// ID string `json:"id,omitempty"` +// Permissions []string `json:"permissions,omitempty"` +// } + +// User represents a user in the database. type User struct { ID string `json:"id"` Permissions []string `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 { @@ -25,6 +41,7 @@ func (user User) HasPermission(neededPerm string) bool { 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"` SeatIDs []string `json:"seatIds"` @@ -32,6 +49,7 @@ type Room struct { // gridRows, gridCols int } +// 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"` RoomID string `json:"roomId"` @@ -42,8 +60,26 @@ type Seat struct { // x, y, w, h int } +// type FrontendSeat struct { +// ID string `json:"id"` +// RoomID string `json:"roomId"` + +// // OccupiedBy is an empty list or a singleton of a userID +// OccupiedBy *FrontendUser `json:"occupiedBy"` + +// DiagramRect struct { +// X int `json:"x"` +// Y int `json:"y"` +// Width int `json:"width"` +// Height int `json:"height"` +// } `json:"diagramRect"` +// } + +// // Database Interfaces +// +// Store is the main interface for interacting with database implementations, for now the only implementation is "memDB". type Store interface { CreateUser(user *User) error CreateRoom(room *Room) error @@ -70,12 +106,17 @@ type Store interface { // TODO: Create an SQLite implementation +// 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 + users map[string]*User rooms map[string]*Room seats map[string]*Seat } +// NewInMemoryStore creates an instance of memDB hidden behind the Store interface. func NewInMemoryStore() Store { db := &memDB{ make(map[string]*User), @@ -115,6 +156,10 @@ func NewInMemoryStore() Store { return db } +// +// memDB Implementation +// + func (db *memDB) CreateUser(user *User) error { if _, present := db.users[user.ID]; present { return ErrAlreadyExists diff --git a/server/server.go b/server/server.go index 0b79113..fb67f64 100644 --- a/server/server.go +++ b/server/server.go @@ -115,12 +115,19 @@ func (server *Server) setupRoutes() { return } - auth.Login(w, r, credentials.Username, credentials.Password) + err := auth.Login(w, r, credentials.Username, credentials.Password) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + httputil.WriteJSON(w, "ok") }) api.With(auth.LoggedMiddleware()). Post("/logout", func(w http.ResponseWriter, r *http.Request) { auth.Logout(w) + httputil.WriteJSON(w, "ok") }) api.Get("/user", func(w http.ResponseWriter, r *http.Request) {