From 74df36cd0d79ecc3c5fdf54da2d1c143f8eab3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Thu, 26 Jun 2025 00:51:54 +0200 Subject: [PATCH] add membership --- go.mod | 8 +- go.sum | 11 ++ local/api/api.go | 30 +++--- local/controller/config.go | 12 +-- local/controller/controller.go | 10 ++ local/controller/lookup.go | 20 ++-- local/controller/membership.go | 142 +++++++++++++++++++++++++ local/controller/server.go | 90 +++++++++------- local/controller/stateHistory.go | 10 +- local/middleware/auth.go | 60 +++++++++++ local/model/permission.go | 19 ++++ local/model/permissions.go | 53 ++++++++++ local/model/role.go | 20 ++++ local/model/server.go | 2 +- local/model/user.go | 70 +++++++++++++ local/repository/membership.go | 142 +++++++++++++++++++++++++ local/repository/repository.go | 1 + local/service/membership.go | 173 +++++++++++++++++++++++++++++++ local/service/server.go | 4 +- local/service/service.go | 1 + local/service/state_history.go | 2 +- local/utl/common/common.go | 9 +- local/utl/db/db.go | 3 + local/utl/jwt/jwt.go | 54 ++++++++++ 24 files changed, 863 insertions(+), 83 deletions(-) create mode 100644 local/controller/membership.go create mode 100644 local/middleware/auth.go create mode 100644 local/model/permission.go create mode 100644 local/model/permissions.go create mode 100644 local/model/role.go create mode 100644 local/model/user.go create mode 100644 local/repository/membership.go create mode 100644 local/service/membership.go create mode 100644 local/utl/jwt/jwt.go diff --git a/go.mod b/go.mod index b555a89..8215b5c 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/swaggo/swag v1.16.3 go.uber.org/dig v1.17.1 golang.org/x/sync v0.15.0 - golang.org/x/text v0.16.0 + golang.org/x/text v0.26.0 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.11 ) @@ -23,6 +23,7 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -37,7 +38,8 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/tools v0.23.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/tools v0.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7bc537d..06dd301 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0 github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ= github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -65,18 +67,27 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/local/api/api.go b/local/api/api.go index 0b2ccca..ba48c07 100644 --- a/local/api/api.go +++ b/local/api/api.go @@ -2,13 +2,14 @@ package api import ( "acc-server-manager/local/controller" + "acc-server-manager/local/service" "acc-server-manager/local/utl/common" "acc-server-manager/local/utl/configs" "acc-server-manager/local/utl/logging" - "os" + "context" + "fmt" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/basicauth" "go.uber.org/dig" ) @@ -18,25 +19,28 @@ import ( // Args: // *fiber.App: Fiber Application func Init(di *dig.Container, app *fiber.App) { + // Setup initial data for membership + di.Invoke(func(membershipService *service.MembershipService) { + if err := membershipService.SetupInitialData(context.Background()); err != nil { + logging.Panic(fmt.Sprintf("failed to setup initial data: %v", err)) + } + }) + + // Protected routes groups := app.Group(configs.Prefix) - basicAuthConfig := basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": os.Getenv("PASSWORD"), - }, - }) + serverIdGroup := groups.Group("/server/:id") routeGroups := &common.RouteGroups{ - Api: groups.Group("/api"), - Server: groups.Group("/server"), - Config: serverIdGroup.Group("/config"), - Lookup: groups.Group("/lookup"), + Api: groups.Group("/api"), + Auth: app.Group("/auth"), + Server: groups.Group("/server"), + Config: serverIdGroup.Group("/config"), + Lookup: groups.Group("/lookup"), StateHistory: serverIdGroup.Group("/state-history"), } - groups.Use(basicAuthConfig) - err := di.Provide(func() *common.RouteGroups { return routeGroups }) diff --git a/local/controller/config.go b/local/controller/config.go index 1799b1c..ec53cb8 100644 --- a/local/controller/config.go +++ b/local/controller/config.go @@ -27,9 +27,9 @@ func NewConfigController(as *service.ConfigService, routeGroups *common.RouteGro apiService: as2, } - routeGroups.Config.Put("/:file", ac.updateConfig) - routeGroups.Config.Get("/:file", ac.getConfig) - routeGroups.Config.Get("/", ac.getConfigs) + routeGroups.Config.Put("/:file", ac.UpdateConfig) + routeGroups.Config.Get("/:file", ac.GetConfig) + routeGroups.Config.Get("/", ac.GetConfigs) return ac } @@ -44,7 +44,7 @@ func NewConfigController(as *service.ConfigService, routeGroups *common.RouteGro // @Tags Config // @Success 200 {array} string // @Router /v1/server/{id}/config/{file} [put] -func (ac *ConfigController) updateConfig(c *fiber.Ctx) error { +func (ac *ConfigController) UpdateConfig(c *fiber.Ctx) error { restart := c.QueryBool("restart") serverID, _ := c.ParamsInt("id") c.Locals("serverId", serverID) @@ -79,7 +79,7 @@ func (ac *ConfigController) updateConfig(c *fiber.Ctx) error { // @Tags Config // @Success 200 {array} string // @Router /v1/server/{id}/config/{file} [get] -func (ac *ConfigController) getConfig(c *fiber.Ctx) error { +func (ac *ConfigController) GetConfig(c *fiber.Ctx) error { Model, err := ac.service.GetConfig(c) if err != nil { logging.Error(err.Error()) @@ -96,7 +96,7 @@ func (ac *ConfigController) getConfig(c *fiber.Ctx) error { // @Tags Config // @Success 200 {array} string // @Router /v1/server/{id}/config [get] -func (ac *ConfigController) getConfigs(c *fiber.Ctx) error { +func (ac *ConfigController) GetConfigs(c *fiber.Ctx) error { Model, err := ac.service.GetConfigs(c) if err != nil { logging.Error(err.Error()) diff --git a/local/controller/controller.go b/local/controller/controller.go index 11ed95e..a2b21dc 100644 --- a/local/controller/controller.go +++ b/local/controller/controller.go @@ -1,6 +1,7 @@ package controller import ( + "acc-server-manager/local/middleware" "acc-server-manager/local/service" "acc-server-manager/local/utl/logging" @@ -15,6 +16,10 @@ import ( func InitializeControllers(c *dig.Container) { service.InitializeServices(c) + if err := c.Provide(middleware.NewAuthMiddleware); err != nil { + logging.Panic("unable to initialize auth middleware") + } + err := c.Invoke(NewApiController) if err != nil { logging.Panic("unable to initialize api controller") @@ -39,4 +44,9 @@ func InitializeControllers(c *dig.Container) { if err != nil { logging.Panic("unable to initialize stateHistory controller") } + + err = c.Invoke(NewMembershipController) + if err != nil { + logging.Panic("unable to initialize membership controller") + } } diff --git a/local/controller/lookup.go b/local/controller/lookup.go index 35b7ef4..66ec317 100644 --- a/local/controller/lookup.go +++ b/local/controller/lookup.go @@ -23,11 +23,11 @@ func NewLookupController(as *service.LookupService, routeGroups *common.RouteGro ac := &LookupController{ service: as, } - routeGroups.Lookup.Get("/tracks", ac.getTracks) - routeGroups.Lookup.Get("/car-models", ac.getCarModels) - routeGroups.Lookup.Get("/driver-categories", ac.getDriverCategories) - routeGroups.Lookup.Get("/cup-categories", ac.getCupCategories) - routeGroups.Lookup.Get("/session-types", ac.getSessionTypes) + routeGroups.Lookup.Get("/tracks", ac.GetTracks) + routeGroups.Lookup.Get("/car-models", ac.GetCarModels) + routeGroups.Lookup.Get("/driver-categories", ac.GetDriverCategories) + routeGroups.Lookup.Get("/cup-categories", ac.GetCupCategories) + routeGroups.Lookup.Get("/session-types", ac.GetSessionTypes) return ac } @@ -39,7 +39,7 @@ func NewLookupController(as *service.LookupService, routeGroups *common.RouteGro // @Tags Lookup // @Success 200 {array} string // @Router /v1/lookup/tracks [get] -func (ac *LookupController) getTracks(c *fiber.Ctx) error { +func (ac *LookupController) GetTracks(c *fiber.Ctx) error { result, err := ac.service.GetTracks(c) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ @@ -56,7 +56,7 @@ func (ac *LookupController) getTracks(c *fiber.Ctx) error { // @Tags Lookup // @Success 200 {array} string // @Router /v1/lookup/car-models [get] -func (ac *LookupController) getCarModels(c *fiber.Ctx) error { +func (ac *LookupController) GetCarModels(c *fiber.Ctx) error { result, err := ac.service.GetCarModels(c) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ @@ -73,7 +73,7 @@ func (ac *LookupController) getCarModels(c *fiber.Ctx) error { // @Tags Lookup // @Success 200 {array} string // @Router /v1/lookup/driver-categories [get] -func (ac *LookupController) getDriverCategories(c *fiber.Ctx) error { +func (ac *LookupController) GetDriverCategories(c *fiber.Ctx) error { result, err := ac.service.GetDriverCategories(c) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ @@ -90,7 +90,7 @@ func (ac *LookupController) getDriverCategories(c *fiber.Ctx) error { // @Tags Lookup // @Success 200 {array} string // @Router /v1/lookup/cup-categories [get] -func (ac *LookupController) getCupCategories(c *fiber.Ctx) error { +func (ac *LookupController) GetCupCategories(c *fiber.Ctx) error { result, err := ac.service.GetCupCategories(c) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ @@ -107,7 +107,7 @@ func (ac *LookupController) getCupCategories(c *fiber.Ctx) error { // @Tags Lookup // @Success 200 {array} string // @Router /v1/lookup/session-types [get] -func (ac *LookupController) getSessionTypes(c *fiber.Ctx) error { +func (ac *LookupController) GetSessionTypes(c *fiber.Ctx) error { result, err := ac.service.GetSessionTypes(c) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ diff --git a/local/controller/membership.go b/local/controller/membership.go new file mode 100644 index 0000000..5d535ea --- /dev/null +++ b/local/controller/membership.go @@ -0,0 +1,142 @@ +package controller + +import ( + "acc-server-manager/local/middleware" + "acc-server-manager/local/model" + "acc-server-manager/local/service" + "acc-server-manager/local/utl/common" + "acc-server-manager/local/utl/jwt" + + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +// MembershipController handles API requests for membership. +type MembershipController struct { + service *service.MembershipService + auth *middleware.AuthMiddleware +} + +// NewMembershipController creates a new MembershipController. +func NewMembershipController(service *service.MembershipService, auth *middleware.AuthMiddleware, routeGroups *common.RouteGroups) *MembershipController { + mc := &MembershipController{ + service: service, + auth: auth, + } + + routeGroups.Auth.Post("/login", mc.Login) + + usersGroup := routeGroups.Api.Group("/users", mc.auth.Authenticate) + usersGroup.Post("/", mc.auth.HasPermission(model.MembershipCreate), mc.CreateUser) + usersGroup.Get("/", mc.auth.HasPermission(model.MembershipView), mc.ListUsers) + usersGroup.Get("/:id", mc.auth.HasPermission(model.MembershipView), mc.GetUser) + usersGroup.Put("/:id", mc.auth.HasPermission(model.MembershipEdit), mc.UpdateUser) + + routeGroups.Api.Get("/me", mc.auth.Authenticate, mc.GetMe) + + return mc +} + +// Login handles user login. +func (c *MembershipController) Login(ctx *fiber.Ctx) error { + type request struct { + Username string `json:"username"` + Password string `json:"password"` + } + + var req request + if err := ctx.BodyParser(&req); err != nil { + return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) + } + + token, err := c.service.Login(ctx.UserContext(), req.Username, req.Password) + if err != nil { + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": err.Error()}) + } + + return ctx.JSON(fiber.Map{"token": token}) +} + +// CreateUser creates a new user. +func (mc *MembershipController) CreateUser(c *fiber.Ctx) error { + type request struct { + Username string `json:"username"` + Password string `json:"password"` + Role string `json:"role"` + } + + var req request + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) + } + + user, err := mc.service.CreateUser(c.UserContext(), req.Username, req.Password, req.Role) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(user) +} + +// ListUsers lists all users. +func (mc *MembershipController) ListUsers(c *fiber.Ctx) error { + users, err := mc.service.ListUsers(c.UserContext()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(users) +} + +// GetUser gets a single user by ID. +func (mc *MembershipController) GetUser(c *fiber.Ctx) error { + id, err := uuid.Parse(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid user ID"}) + } + + user, err := mc.service.GetUser(c.UserContext(), id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"}) + } + + return c.JSON(user) +} + +// GetMe returns the currently authenticated user's details. +func (mc *MembershipController) GetMe(c *fiber.Ctx) error { + claims, ok := c.Locals("user").(*jwt.Claims) + if !ok || claims == nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"}) + } + + user, err := mc.service.GetUserWithPermissions(c.UserContext(), claims.UserID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"}) + } + + // Sanitize the user object to not expose password + user.Password = "" + + return c.JSON(user) +} + +// UpdateUser updates a user. +func (mc *MembershipController) UpdateUser(c *fiber.Ctx) error { + id, err := uuid.Parse(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid user ID"}) + } + + var req service.UpdateUserRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) + } + + user, err := mc.service.UpdateUser(c.UserContext(), id, req) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(user) +} diff --git a/local/controller/server.go b/local/controller/server.go index d126d1a..06c8e84 100644 --- a/local/controller/server.go +++ b/local/controller/server.go @@ -1,6 +1,7 @@ package controller import ( + "acc-server-manager/local/middleware" "acc-server-manager/local/model" "acc-server-manager/local/service" "acc-server-manager/local/utl/common" @@ -12,34 +13,26 @@ type ServerController struct { service *service.ServerService } -// NewServerController -// Initializes ServerController. -// -// Args: -// *services.ServerService: Server service -// *Fiber.RouterGroup: Fiber Router Group -// Returns: -// *ServerController: Controller for "Server" interactions -func NewServerController(as *service.ServerService, routeGroups *common.RouteGroups,) *ServerController { +// NewServerController initializes ServerController. +func NewServerController(ss *service.ServerService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *ServerController { ac := &ServerController{ - service: as, + service: ss, } - routeGroups.Server.Get("/", ac.getAll) - routeGroups.Server.Get("/:id", ac.getById) - routeGroups.Server.Post("/", ac.createServer) + serverRoutes := routeGroups.Server + serverRoutes.Use(auth.Authenticate) + + serverRoutes.Get("/", auth.HasPermission(model.ServerView), ac.GetAll) + serverRoutes.Get("/:id", auth.HasPermission(model.ServerView), ac.GetById) + serverRoutes.Post("/", auth.HasPermission(model.ServerCreate), ac.CreateServer) + serverRoutes.Put("/:id", auth.HasPermission(model.ServerUpdate), ac.UpdateServer) + serverRoutes.Delete("/:id", auth.HasPermission(model.ServerDelete), ac.DeleteServer) return ac } -// getAll returns Servers -// -// @Summary Return Servers -// @Description Return Servers -// @Tags Server -// @Success 200 {array} string -// @Router /v1/server [get] -func (ac *ServerController) getAll(c *fiber.Ctx) error { - var filter model.ServerFilter +// GetAll returns Servers +func (ac *ServerController) GetAll(c *fiber.Ctx) error { + var filter model.ServerFilter if err := common.ParseQueryFilter(c, &filter); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": err.Error(), @@ -52,14 +45,8 @@ func (ac *ServerController) getAll(c *fiber.Ctx) error { return c.JSON(ServerModel) } -// getById returns Servers -// -// @Summary Return Servers -// @Description Return Servers -// @Tags Server -// @Success 200 {array} string -// @Router /v1/server [get] -func (ac *ServerController) getById(c *fiber.Ctx) error { +// GetById returns a single server by its ID +func (ac *ServerController) GetById(c *fiber.Ctx) error { serverID, _ := c.ParamsInt("id") ServerModel, err := ac.service.GetById(c, serverID) if err != nil { @@ -68,14 +55,8 @@ func (ac *ServerController) getById(c *fiber.Ctx) error { return c.JSON(ServerModel) } -// createServer creates a new server -// -// @Summary Create a new server -// @Description Create a new server -// @Tags Server -// @Success 200 {array} string -// @Router /v1/server [post] -func (ac *ServerController) createServer(c *fiber.Ctx) error { +// CreateServer creates a new server +func (ac *ServerController) CreateServer(c *fiber.Ctx) error { server := new(model.Server) if err := c.BodyParser(server); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ @@ -89,4 +70,37 @@ func (ac *ServerController) createServer(c *fiber.Ctx) error { }) } return c.JSON(server) +} + +// UpdateServer updates an existing server +func (ac *ServerController) UpdateServer(c *fiber.Ctx) error { + serverID, _ := c.ParamsInt("id") + server := new(model.Server) + if err := c.BodyParser(server); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + server.ID = uint(serverID) + + if err := ac.service.UpdateServer(c, server); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + return c.JSON(server) +} + +// DeleteServer deletes a server +func (ac *ServerController) DeleteServer(c *fiber.Ctx) error { + serverID, err := c.ParamsInt("id") + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid server ID"}) + } + + if err := ac.service.DeleteServer(c, serverID); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.SendStatus(204) } \ No newline at end of file diff --git a/local/controller/stateHistory.go b/local/controller/stateHistory.go index 865dbb1..f6faa90 100644 --- a/local/controller/stateHistory.go +++ b/local/controller/stateHistory.go @@ -25,20 +25,20 @@ func NewStateHistoryController(as *service.StateHistoryService, routeGroups *com service: as, } - routeGroups.StateHistory.Get("/", ac.getAll) - routeGroups.StateHistory.Get("/statistics", ac.getStatistics) + routeGroups.StateHistory.Get("/", ac.GetAll) + routeGroups.StateHistory.Get("/statistics", ac.GetStatistics) return ac } -// getAll returns StateHistorys +// GetAll returns StateHistorys // // @Summary Return StateHistorys // @Description Return StateHistorys // @Tags StateHistory // @Success 200 {array} string // @Router /v1/state-history [get] -func (ac *StateHistoryController) getAll(c *fiber.Ctx) error { +func (ac *StateHistoryController) GetAll(c *fiber.Ctx) error { var filter model.StateHistoryFilter if err := common.ParseQueryFilter(c, &filter); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ @@ -63,7 +63,7 @@ func (ac *StateHistoryController) getAll(c *fiber.Ctx) error { // @Tags StateHistory // @Success 200 {array} string // @Router /v1/state-history/statistics [get] -func (ac *StateHistoryController) getStatistics(c *fiber.Ctx) error { +func (ac *StateHistoryController) GetStatistics(c *fiber.Ctx) error { var filter model.StateHistoryFilter if err := common.ParseQueryFilter(c, &filter); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ diff --git a/local/middleware/auth.go b/local/middleware/auth.go new file mode 100644 index 0000000..37f27c0 --- /dev/null +++ b/local/middleware/auth.go @@ -0,0 +1,60 @@ +package middleware + +import ( + "acc-server-manager/local/service" + "acc-server-manager/local/utl/jwt" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +// AuthMiddleware provides authentication and permission middleware. +type AuthMiddleware struct { + membershipService *service.MembershipService +} + +// NewAuthMiddleware creates a new AuthMiddleware. +func NewAuthMiddleware(ms *service.MembershipService) *AuthMiddleware { + return &AuthMiddleware{ + membershipService: ms, + } +} + +// Authenticate is a middleware for JWT authentication. +func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error { + authHeader := ctx.Get("Authorization") + if authHeader == "" { + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing or malformed JWT"}) + } + + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing or malformed JWT"}) + } + + claims, err := jwt.ValidateToken(parts[1]) + if err != nil { + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired JWT"}) + } + + ctx.Locals("userID", claims.UserID) + return ctx.Next() +} + +// HasPermission is a middleware for checking user permissions. +func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler { + return func(ctx *fiber.Ctx) error { + userID, ok := ctx.Locals("userID").(uuid.UUID) + if !ok { + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"}) + } + + has, err := m.membershipService.HasPermission(ctx.UserContext(), userID, requiredPermission) + if err != nil || !has { + return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Forbidden"}) + } + + return ctx.Next() + } +} diff --git a/local/model/permission.go b/local/model/permission.go new file mode 100644 index 0000000..3e3022d --- /dev/null +++ b/local/model/permission.go @@ -0,0 +1,19 @@ +package model + +import ( + "github.com/google/uuid" + "gorm.io/gorm" +) + +// Permission represents an action that can be performed in the system. +type Permission struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"` + Name string `json:"name" gorm:"unique_index;not null"` +} + +// BeforeCreate is a GORM hook that runs before creating new credentials +func (s *Permission) BeforeCreate(tx *gorm.DB) error { + s.ID = uuid.New() + + return nil +} \ No newline at end of file diff --git a/local/model/permissions.go b/local/model/permissions.go new file mode 100644 index 0000000..b96eff0 --- /dev/null +++ b/local/model/permissions.go @@ -0,0 +1,53 @@ +package model + +// Permission constants +const ( + ServerView = "server.view" + ServerCreate = "server.create" + ServerUpdate = "server.update" + ServerDelete = "server.delete" + ServerStart = "server.start" + ServerStop = "server.stop" + + ConfigView = "config.view" + ConfigUpdate = "config.update" + + UserView = "user.view" + UserCreate = "user.create" + UserUpdate = "user.update" + UserDelete = "user.delete" + + RoleView = "role.view" + RoleCreate = "role.create" + RoleUpdate = "role.update" + RoleDelete = "role.delete" + + MembershipCreate = "membership.create" + MembershipView = "membership.view" + MembershipEdit = "membership.edit" +) + +// AllPermissions returns a slice of all permission strings. +func AllPermissions() []string { + return []string{ + ServerView, + ServerCreate, + ServerUpdate, + ServerDelete, + ServerStart, + ServerStop, + ConfigView, + ConfigUpdate, + UserView, + UserCreate, + UserUpdate, + UserDelete, + RoleView, + RoleCreate, + RoleUpdate, + RoleDelete, + MembershipCreate, + MembershipView, + MembershipEdit, + } +} diff --git a/local/model/role.go b/local/model/role.go new file mode 100644 index 0000000..9d48842 --- /dev/null +++ b/local/model/role.go @@ -0,0 +1,20 @@ +package model + +import ( + "github.com/google/uuid" + "gorm.io/gorm" +) + +// Role represents a user role in the system. +type Role struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"` + Name string `json:"name" gorm:"unique_index;not null"` + Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"` +} + +// BeforeCreate is a GORM hook that runs before creating new credentials +func (s *Role) BeforeCreate(tx *gorm.DB) error { + s.ID = uuid.New() + + return nil +} \ No newline at end of file diff --git a/local/model/server.go b/local/model/server.go index bdb3d51..6c8b47f 100644 --- a/local/model/server.go +++ b/local/model/server.go @@ -24,7 +24,7 @@ type Server struct { Port int `gorm:"not null" json:"-"` Path string `gorm:"not null" json:"path"` // e.g. "/acc/servers/server1/" ServiceName string `gorm:"not null" json:"serviceName"` // Windows service name - State ServerState `gorm:"-" json:"state"` + State *ServerState `gorm:"-" json:"state"` DateCreated time.Time `json:"dateCreated"` FromSteamCMD bool `gorm:"not null; default:true" json:"-"` } diff --git a/local/model/user.go b/local/model/user.go new file mode 100644 index 0000000..1c3c688 --- /dev/null +++ b/local/model/user.go @@ -0,0 +1,70 @@ +package model + +import ( + "errors" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// User represents a user account in the system. +type User struct { + ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"` + Username string `json:"username" gorm:"unique_index;not null"` + Password string `json:"password" gorm:"not null"` + RoleID uuid.UUID `json:"role_id" gorm:"type:uuid"` + Role Role `json:"role"` +} + + +// BeforeCreate is a GORM hook that runs before creating new credentials +func (s *User) BeforeCreate(tx *gorm.DB) error { + s.ID = uuid.New() + // Encrypt password before saving + encrypted, err := EncryptPassword(s.Password) + if err != nil { + return err + } + s.Password = encrypted + + return nil +} + +// BeforeUpdate is a GORM hook that runs before updating credentials +func (s *User) BeforeUpdate(tx *gorm.DB) error { + + // Only encrypt if password field is being updated + if tx.Statement.Changed("Password") { + encrypted, err := EncryptPassword(s.Password) + if err != nil { + return err + } + s.Password = encrypted + } + + return nil +} + +// AfterFind is a GORM hook that runs after fetching credentials +func (s *User) AfterFind(tx *gorm.DB) error { + // Decrypt password after fetching + if s.Password != "" { + decrypted, err := DecryptPassword(s.Password) + if err != nil { + return err + } + s.Password = decrypted + } + return nil +} + +// Validate checks if the credentials are valid +func (s *User) Validate() error { + if s.Username == "" { + return errors.New("username is required") + } + if s.Password == "" { + return errors.New("password is required") + } + return nil +} \ No newline at end of file diff --git a/local/repository/membership.go b/local/repository/membership.go new file mode 100644 index 0000000..3beac60 --- /dev/null +++ b/local/repository/membership.go @@ -0,0 +1,142 @@ +package repository + +import ( + "acc-server-manager/local/model" + "context" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// MembershipRepository handles database operations for users, roles, and permissions. +type MembershipRepository struct { + db *gorm.DB +} + +// NewMembershipRepository creates a new MembershipRepository. +func NewMembershipRepository(db *gorm.DB) *MembershipRepository { + return &MembershipRepository{db: db} +} + +// FindUserByUsername finds a user by their username. +// It preloads the user's role and the role's permissions. +func (r *MembershipRepository) FindUserByUsername(ctx context.Context, username string) (*model.User, error) { + var user model.User + db := r.db.WithContext(ctx) + err := db.Preload("Role.Permissions").Where("username = ?", username).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// FindUserByIDWithPermissions finds a user by their ID and preloads Role and Permissions. +func (r *MembershipRepository) FindUserByIDWithPermissions(ctx context.Context, userID uuid.UUID) (*model.User, error) { + var user model.User + db := r.db.WithContext(ctx) + err := db.Preload("Role.Permissions").First(&user, "id = ?", userID).Error + if err != nil { + return nil, err + } + return &user, nil +} + + +// CreateUser creates a new user. +func (r *MembershipRepository) CreateUser(ctx context.Context, user *model.User) error { + db := r.db.WithContext(ctx) + return db.Create(user).Error +} + +// FindRoleByName finds a role by its name. +func (r *MembershipRepository) FindRoleByName(ctx context.Context, name string) (*model.Role, error) { + var role model.Role + db := r.db.WithContext(ctx) + err := db.Where("name = ?", name).First(&role).Error + if err != nil { + return nil, err + } + return &role, nil +} + +// CreateRole creates a new role. +func (r *MembershipRepository) CreateRole(ctx context.Context, role *model.Role) error { + db := r.db.WithContext(ctx) + return db.Create(role).Error +} + +// FindPermissionByName finds a permission by its name. +func (r *MembershipRepository) FindPermissionByName(ctx context.Context, name string) (*model.Permission, error) { + var permission model.Permission + db := r.db.WithContext(ctx) + err := db.Where("name = ?", name).First(&permission).Error + if err != nil { + return nil, err + } + return &permission, nil +} + +// CreatePermission creates a new permission. +func (r *MembershipRepository) CreatePermission(ctx context.Context, permission *model.Permission) error { + db := r.db.WithContext(ctx) + return db.Create(permission).Error +} + +// AssignPermissionsToRole assigns a set of permissions to a role. +func (r *MembershipRepository) AssignPermissionsToRole(ctx context.Context, role *model.Role, permissions []model.Permission) error { + db := r.db.WithContext(ctx) + return db.Model(role).Association("Permissions").Replace(permissions) +} + +// GetUserPermissions retrieves all permissions for a given user ID. +func (r *MembershipRepository) GetUserPermissions(ctx context.Context, userID uuid.UUID) ([]string, error) { + var user model.User + db := r.db.WithContext(ctx) + + if err := db.Preload("Role.Permissions").First(&user, "id = ?", userID).Error; err != nil { + return nil, err + } + + permissions := make([]string, len(user.Role.Permissions)) + for i, p := range user.Role.Permissions { + permissions[i] = p.Name + } + + return permissions, nil +} + +// ListUsers retrieves all users. +func (r *MembershipRepository) ListUsers(ctx context.Context) ([]*model.User, error) { + var users []*model.User + db := r.db.WithContext(ctx) + err := db.Preload("Role").Find(&users).Error + return users, err +} + +// FindUserByID finds a user by their ID. +func (r *MembershipRepository) FindUserByID(ctx context.Context, userID uuid.UUID) (*model.User, error) { + var user model.User + db := r.db.WithContext(ctx) + err := db.Preload("Role").First(&user, "id = ?", userID).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// UpdateUser updates a user's details in the database. +func (r *MembershipRepository) UpdateUser(ctx context.Context, user *model.User) error { + db := r.db.WithContext(ctx) + return db.Save(user).Error +} + +// FindRoleByID finds a role by its ID. +func (r *MembershipRepository) FindRoleByID(ctx context.Context, roleID uuid.UUID) (*model.Role, error) { + var role model.Role + db := r.db.WithContext(ctx) + err := db.First(&role, "id = ?", roleID).Error + if err != nil { + return nil, err + } + return &role, nil +} diff --git a/local/repository/repository.go b/local/repository/repository.go index 86960f1..d7c3467 100644 --- a/local/repository/repository.go +++ b/local/repository/repository.go @@ -17,4 +17,5 @@ func InitializeRepositories(c *dig.Container) { c.Provide(NewLookupRepository) c.Provide(NewSteamCredentialsRepository) c.Provide(NewSystemConfigRepository) + c.Provide(NewMembershipRepository) } diff --git a/local/service/membership.go b/local/service/membership.go new file mode 100644 index 0000000..c8bdbf2 --- /dev/null +++ b/local/service/membership.go @@ -0,0 +1,173 @@ +package service + +import ( + "acc-server-manager/local/model" + "acc-server-manager/local/repository" + "acc-server-manager/local/utl/jwt" + "context" + "errors" + "os" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +// MembershipService provides business logic for membership-related operations. +type MembershipService struct { + repo *repository.MembershipRepository +} + +// NewMembershipService creates a new MembershipService. +func NewMembershipService(repo *repository.MembershipRepository) *MembershipService { + return &MembershipService{repo: repo} +} + +// Login authenticates a user and returns a JWT. +func (s *MembershipService) Login(ctx context.Context, username, password string) (string, error) { + user, err := s.repo.FindUserByUsername(ctx, username) + if err != nil { + return "", errors.New("invalid credentials") + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return "", errors.New("invalid credentials") + } + + return jwt.GenerateToken(user) +} + +// CreateUser creates a new user. +func (s *MembershipService) CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error) { + + role, err := s.repo.FindRoleByName(ctx, roleName) + if err != nil { + return nil, errors.New("role not found") + } + + user := &model.User{ + Username: username, + Password: password, + RoleID: role.ID, + } + + if err := s.repo.CreateUser(ctx, user); err != nil { + return nil, err + } + + return user, nil +} + +// ListUsers retrieves all users. +func (s *MembershipService) ListUsers(ctx context.Context) ([]*model.User, error) { + return s.repo.ListUsers(ctx) +} + +// GetUser retrieves a single user by ID. +func (s *MembershipService) GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error) { + return s.repo.FindUserByID(ctx, userID) +} + +// GetUserWithPermissions retrieves a single user by ID with their role and permissions. +func (s *MembershipService) GetUserWithPermissions(ctx context.Context, userID uuid.UUID) (*model.User, error) { + return s.repo.FindUserByIDWithPermissions(ctx, userID) +} + +// UpdateUserRequest defines the request body for updating a user. +type UpdateUserRequest struct { + Username *string `json:"username"` + Password *string `json:"password"` + RoleID *uuid.UUID `json:"roleId"` +} + +// UpdateUser updates a user's details. +func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error) { + user, err := s.repo.FindUserByID(ctx, userID) + if err != nil { + return nil, errors.New("user not found") + } + + if req.Username != nil { + user.Username = *req.Username + } + + if req.Password != nil && *req.Password != "" { + user.Password = *req.Password + } + + if req.RoleID != nil { + // Check if role exists + _, err := s.repo.FindRoleByID(ctx, *req.RoleID) + if err != nil { + return nil, errors.New("role not found") + } + user.RoleID = *req.RoleID + } + + if err := s.repo.UpdateUser(ctx, user); err != nil { + return nil, err + } + + return user, nil +} + +// HasPermission checks if a user has a specific permission. +func (s *MembershipService) HasPermission(ctx context.Context, userID uuid.UUID, permissionName string) (bool, error) { + user, err := s.repo.FindUserByIDWithPermissions(ctx, userID) + if err != nil { + return false, err + } + + // Super admin has all permissions + if user.Role.Name == "Super Admin" { + return true, nil + } + + for _, p := range user.Role.Permissions { + if p.Name == permissionName { + return true, nil + } + } + + return false, nil +} + +// SetupInitialData creates the initial roles and permissions. +func (s *MembershipService) SetupInitialData(ctx context.Context) error { + // Define all permissions + permissions := model.AllPermissions() + + createdPermissions := make([]model.Permission, 0) + for _, pName := range permissions { + perm, err := s.repo.FindPermissionByName(ctx, pName) + if err != nil { // Assuming error means not found + perm = &model.Permission{Name: pName} + if err := s.repo.CreatePermission(ctx, perm); err != nil { + return err + } + } + createdPermissions = append(createdPermissions, *perm) + } + + // Create Super Admin role with all permissions + superAdminRole, err := s.repo.FindRoleByName(ctx, "Super Admin") + if err != nil { + superAdminRole = &model.Role{Name: "Super Admin"} + if err := s.repo.CreateRole(ctx, superAdminRole); err != nil { + return err + } + } + if err := s.repo.AssignPermissionsToRole(ctx, superAdminRole, createdPermissions); err != nil { + return err + } + + // Create a default admin user if one doesn't exist + _, err = s.repo.FindUserByUsername(ctx, "admin") + if err != nil { + _, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed + if err != nil { + return err + } + } + + return nil +} diff --git a/local/service/server.go b/local/service/server.go index bcd19e1..aef0b4f 100644 --- a/local/service/server.go +++ b/local/service/server.go @@ -294,7 +294,7 @@ func (s *ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]m } else { serverInstance := instance.(*tracking.AccServerInstance) if serverInstance.State != nil { - (*server).State = *serverInstance.State + server.State = serverInstance.State } } } @@ -325,7 +325,7 @@ func (as *ServerService) GetById(ctx *fiber.Ctx, serverID int) (*model.Server, e } else { serverInstance := instance.(*tracking.AccServerInstance) if (serverInstance.State != nil) { - (*server).State = *serverInstance.State + server.State = serverInstance.State } } diff --git a/local/service/service.go b/local/service/service.go index ec2edd0..626c064 100644 --- a/local/service/service.go +++ b/local/service/service.go @@ -27,6 +27,7 @@ func InitializeServices(c *dig.Container) { c.Provide(NewSteamService) c.Provide(NewWindowsService) c.Provide(NewFirewallService) + c.Provide(NewMembershipService) logging.Debug("Initializing service dependencies") err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, systemConfig *SystemConfigService) { diff --git a/local/service/state_history.go b/local/service/state_history.go index 88c0920..60f98d1 100644 --- a/local/service/state_history.go +++ b/local/service/state_history.go @@ -3,7 +3,7 @@ package service import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" - "acc-server-manager/pkg/logging" + "acc-server-manager/local/utl/logging" "sync" "github.com/gofiber/fiber/v2" diff --git a/local/utl/common/common.go b/local/utl/common/common.go index 675de32..ebef20b 100644 --- a/local/utl/common/common.go +++ b/local/utl/common/common.go @@ -17,10 +17,11 @@ import ( ) type RouteGroups struct { - Api fiber.Router - Server fiber.Router - Config fiber.Router - Lookup fiber.Router + Api fiber.Router + Auth fiber.Router + Server fiber.Router + Config fiber.Router + Lookup fiber.Router StateHistory fiber.Router } diff --git a/local/utl/db/db.go b/local/utl/db/db.go index fbaf655..bee0456 100644 --- a/local/utl/db/db.go +++ b/local/utl/db/db.go @@ -44,6 +44,9 @@ func Migrate(db *gorm.DB) { &model.StateHistory{}, &model.SteamCredentials{}, &model.SystemConfig{}, + &model.User{}, + &model.Role{}, + &model.Permission{}, ) if err != nil { diff --git a/local/utl/jwt/jwt.go b/local/utl/jwt/jwt.go new file mode 100644 index 0000000..35ef34d --- /dev/null +++ b/local/utl/jwt/jwt.go @@ -0,0 +1,54 @@ +package jwt + +import ( + "acc-server-manager/local/model" + "errors" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" +) + +// SecretKey is the secret key for signing the JWT. +// It is recommended to use a long, complex string for this. +// In a production environment, this should be loaded from a secure configuration source. +var SecretKey = []byte("your-secret-key") + +// Claims represents the JWT claims. +type Claims struct { + UserID uuid.UUID `json:"user_id"` + jwt.RegisteredClaims +} + +// GenerateToken generates a new JWT for a given user. +func GenerateToken(user *model.User) (string, error) { + expirationTime := time.Now().Add(24 * time.Hour) + claims := &Claims{ + UserID: user.ID, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(SecretKey) +} + +// ValidateToken validates a JWT and returns the claims if the token is valid. +func ValidateToken(tokenString string) (*Claims, error) { + claims := &Claims{} + + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return SecretKey, nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, errors.New("invalid token") + } + + return claims, nil +}