add membership
This commit is contained in:
8
go.mod
8
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
|
||||
)
|
||||
|
||||
11
go.sum
11
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=
|
||||
|
||||
@@ -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"),
|
||||
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
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
142
local/controller/membership.go
Normal file
142
local/controller/membership.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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,33 +13,25 @@ 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 {
|
||||
// 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{
|
||||
@@ -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{
|
||||
@@ -90,3 +71,36 @@ 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)
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
60
local/middleware/auth.go
Normal file
60
local/middleware/auth.go
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
19
local/model/permission.go
Normal file
19
local/model/permission.go
Normal file
@@ -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
|
||||
}
|
||||
53
local/model/permissions.go
Normal file
53
local/model/permissions.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
20
local/model/role.go
Normal file
20
local/model/role.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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:"-"`
|
||||
}
|
||||
|
||||
70
local/model/user.go
Normal file
70
local/model/user.go
Normal file
@@ -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
|
||||
}
|
||||
142
local/repository/membership.go
Normal file
142
local/repository/membership.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -17,4 +17,5 @@ func InitializeRepositories(c *dig.Container) {
|
||||
c.Provide(NewLookupRepository)
|
||||
c.Provide(NewSteamCredentialsRepository)
|
||||
c.Provide(NewSystemConfigRepository)
|
||||
c.Provide(NewMembershipRepository)
|
||||
}
|
||||
|
||||
173
local/service/membership.go
Normal file
173
local/service/membership.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
type RouteGroups struct {
|
||||
Api fiber.Router
|
||||
Auth fiber.Router
|
||||
Server fiber.Router
|
||||
Config fiber.Router
|
||||
Lookup fiber.Router
|
||||
|
||||
@@ -44,6 +44,9 @@ func Migrate(db *gorm.DB) {
|
||||
&model.StateHistory{},
|
||||
&model.SteamCredentials{},
|
||||
&model.SystemConfig{},
|
||||
&model.User{},
|
||||
&model.Role{},
|
||||
&model.Permission{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
54
local/utl/jwt/jwt.go
Normal file
54
local/utl/jwt/jwt.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user