diff --git a/main/main.go b/main/main.go index 53923ee..c2f5987 100644 --- a/main/main.go +++ b/main/main.go @@ -42,6 +42,14 @@ func main() { webServer.Initialize() defer webServer.Close() + authenticator := server.Authenticator{} + authenticator.SetUserManager(&userManager) + + userApiHandler := server.UsersApiHandler{} + userApiHandler.SetUserManager(&userManager) + userApiHandler.SetRouter(webServer.Router()) + userApiHandler.Initialize(&authenticator) + var wg sync.WaitGroup wg.Add(1) go func() { diff --git a/server/authenticator.go b/server/authenticator.go new file mode 100644 index 0000000..65c97b2 --- /dev/null +++ b/server/authenticator.go @@ -0,0 +1,66 @@ +package server + +import ( + "fmt" + d "playback-device-server/data" + m "playback-device-server/management" + "strings" + + "github.com/labstack/echo/v4" + "github.com/rs/zerolog/log" +) + +type AuthContext struct { + echo.Context + User *d.User + Session *d.Session +} + +type Authenticator struct { + userManager *m.UserManager +} + +func (r *Authenticator) SetUserManager(userManager *m.UserManager) { + r.userManager = userManager +} + +func (r *Authenticator) Authenticate(path string, exceptions []string) func(next echo.HandlerFunc) echo.HandlerFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(context echo.Context) error { + requestURI := context.Request().RequestURI + if !strings.HasPrefix(requestURI, path) { + return next(context) + } + for _, exception := range exceptions { + if strings.HasPrefix(requestURI, exception) { + return next(context) + } + } + cookie, err := context.Cookie("token") + if err != nil { + SendError(401, context, "no session token found") + return err + } + session, error := r.userManager.GetSession(cookie.Value) + if error != nil { + SendError(401, context, fmt.Sprintf("session not found: %s", cookie.Value)) + return fmt.Errorf("session not found: %s", cookie.Value) + } + + user, error := r.userManager.GetUserById(session.UserID) + if error != nil { + log.Error().Err(error).Msg("error getting user by id") + SendError(401, context, "no user found for given session") + return error + } + if user == nil { + SendError(401, context, "no user found for given session") + return fmt.Errorf("no user found for session '%s'", cookie.Value) + } + + authContext := AuthContext{Context: context, User: user, Session: session} + + return next(authContext) + } + } +} diff --git a/server/user_api_handler.go b/server/user_api_handler.go new file mode 100644 index 0000000..ddb512e --- /dev/null +++ b/server/user_api_handler.go @@ -0,0 +1,262 @@ +package server + +import ( + "fmt" + "net/http" + d "playback-device-server/data" + m "playback-device-server/management" + + "github.com/labstack/echo/v4" + "github.com/rs/zerolog/log" +) + +type UsersApiHandler struct { + router *echo.Echo + userManager *m.UserManager +} + +func (r *UsersApiHandler) Initialize(authenticator *Authenticator) { + r.router.Use(authenticator.Authenticate("/api/users", []string{"/api/users/login"})) + usersApi := r.router.Group("/api/users") + usersApi.POST("/login", r.handleLogin) + usersApi.POST("/logout", r.handleLogout) + usersApi.GET("/session/info", r.handleGetSessionInfo) + usersApi.POST("/newpassword", r.handleChangePassword) + usersApi.PUT("/:id", r.handleUpdateConfig) + usersApi.GET("", r.handleGetUsers) + usersApi.POST("", r.handleCreateUser) + usersApi.DELETE("/:id", r.handleDeleteUser) +} + +func (r UsersApiHandler) handleLogin(context echo.Context) error { + var loginData struct { + Username string `json:"username"` + Password string `json:"password"` + } + + var errorResponse struct { + Error string `json:"error"` + } + + error := context.Bind(&loginData) + + if error != nil { + log.Info().Msgf("failed to bind login data: %s", error) + errorResponse.Error = fmt.Sprintf("%s", error) + context.JSON(400, errorResponse) + return error + } + + token, error := r.userManager.Login(loginData.Username, loginData.Password) + + if error != nil { + log.Info().Msgf("failed to login: %s", error) + errorResponse.Error = fmt.Sprintf("%s", error) + context.JSON(400, errorResponse) + return error + } + + thirdyDays := 30 * 24 * 60 * 60 + + cookie := &http.Cookie{ + Name: "token", + Value: token, + Path: "/", + HttpOnly: true, + MaxAge: thirdyDays, + } + + context.SetCookie(cookie) + + return context.JSON(200, "") +} + +func (r UsersApiHandler) handleLogout(context echo.Context) error { + r.removeTokenCookie(&context) + context.JSON(200, "") + + authContext := context.(AuthContext) + + session := authContext.Session + r.userManager.DeleteSession(session.Token) + + return nil +} + +func (r UsersApiHandler) handleChangePassword(context echo.Context) error { + authContext := context.(AuthContext) + user := authContext.User + + var data struct { + CurrentPassword string `json:"currentPassword"` + NewPassword string `json:"newPassword"` + } + + error := context.Bind(&data) + + if error != nil { + SendError(500, context, fmt.Sprintf("Failed to change password: %s", error)) + return error + } + + error = r.userManager.UpdatePassword(data.CurrentPassword, data.NewPassword, user) + + if error != nil { + SendError(500, context, fmt.Sprintf("Failed to change password: %s", error)) + } + return error +} + +func (r UsersApiHandler) handleGetSessionInfo(context echo.Context) error { + authContext := context.(AuthContext) + user := authContext.User + + response := struct { + UserId string `json:"user_id"` + Username string `json:"username"` + IsAdmin bool `json:"is_admin"` + }{ + UserId: user.ID, + Username: user.Username, + IsAdmin: user.IsAdmin, + } + + return context.JSON(200, response) +} + +func (r UsersApiHandler) handleGetUsers(context echo.Context) error { + users, error := r.userManager.GetUsers() + if error != nil { + SendError(500, context, fmt.Sprintf("failed to get user list: %s", error)) + } + + return context.JSON(200, users) +} + +func (r UsersApiHandler) handleCreateUser(context echo.Context) error { + authContext := context.(AuthContext) + user := authContext.User + + if !user.IsAdmin { + SendError(403, context, "permission denied") + log.Info().Msg("failed to delete user: permission denied") + return fmt.Errorf("permission denied") + } + + var newUser d.User + error := context.Bind(&newUser) + + if error != nil { + SendError(500, context, fmt.Sprintf("Failed to create user: %s", error)) + return error + } + + id, error := r.userManager.CreateUser(&newUser) + + if error != nil { + SendError(500, context, fmt.Sprintf("Failed to create user: %s", error)) + return error + } + + user.ID = id + + return context.JSON(200, user) +} + +func (r UsersApiHandler) handleDeleteUser(context echo.Context) error { + authContext := context.(AuthContext) + user := authContext.User + + if !user.IsAdmin { + SendError(403, context, "permission denied") + log.Info().Msg("failed to delete user: permission denied") + return fmt.Errorf("permission denied") + } + + id := context.Param("id") + + exists, error := r.userManager.UserIdExists(id) + + if error != nil { + SendError(500, context, fmt.Sprintf("Failed to delete user: %s", error)) + return error + } + + if !exists { + SendError(404, context, fmt.Sprintf("Failed to delete user: Could not find user id %s", id)) + return fmt.Errorf("not found") + } + + error = r.userManager.DeleteUser(id) + if error != nil { + SendError(500, context, fmt.Sprintf("Failed to delete user: %s", error)) + return error + } + + return context.JSON(200, "") +} + +func (r *UsersApiHandler) removeTokenCookie(context *echo.Context) { + cookie := &http.Cookie{ + Name: "token", + Path: "/", + HttpOnly: true, + MaxAge: -1, + } + + (*context).SetCookie(cookie) +} + +func (r *UsersApiHandler) handleUpdateConfig(context echo.Context) error { + authContext := context.(AuthContext) + user := authContext.User + + if !user.IsAdmin { + SendError(403, context, "permission denied") + log.Info().Msg("failed to delete user: permission denied") + return fmt.Errorf("permission denied") + } + + id := context.Param("id") + + data := struct { + IsAdmin int `json:"is_admin"` + }{ + IsAdmin: -1, + } + + error := context.Bind(&data) + + if error != nil { + SendError(500, context, fmt.Sprintf("Failed to update user config: %s", error)) + return error + } + + updatingUser, error := r.userManager.GetUserById(id) + + if error != nil { + SendError(500, context, fmt.Sprintf("Failed to update user config: %s", error)) + return error + } + + if user.ID != id && data.IsAdmin != -1 { + updatingUser.IsAdmin = data.IsAdmin == 1 + } + + error = r.userManager.UpdateUser(updatingUser) + + if error != nil { + SendError(500, context, fmt.Sprintf("Failed to update user config: %s", error)) + return error + } + + return context.JSON(200, updatingUser) +} + +func (r *UsersApiHandler) SetRouter(router *echo.Echo) { + r.router = router +} + +func (r *UsersApiHandler) SetUserManager(userManager *m.UserManager) { + r.userManager = userManager +}