feat: roles system backend

This commit is contained in:
Florian Sylvain committed 2026-10-26 05:02:49 +01:00
1 parent 44cca46d5e
commit 8c6c6456da
50 files changed
+2578 -22

No files matched your search

+7
View File
@@ -12,6 +12,7 @@ import (
domainInvite "RenewCMS/internal/domain/invite"
domainMail "RenewCMS/internal/domain/mail"
domainPurge "RenewCMS/internal/domain/purge"
domainRole "RenewCMS/internal/domain/role"
domainSession "RenewCMS/internal/domain/session"
domainSite "RenewCMS/internal/domain/site"
domainTransaction "RenewCMS/internal/domain/transaction"
@@ -62,6 +63,8 @@ type UseCases struct {
UpdateAuthorUseCase *useCases.UpdateAuthorUseCase
DeleteAuthorUseCase *useCases.DeleteAuthorUseCase
GetMyAuthorUseCase *useCases.GetMyAuthorUseCase
GetMyAccessUseCase *useCases.GetMyAccessUseCase
ListPermissionsUseCase *useCases.ListPermissionsUseCase
ListSitesUseCase *useCases.ListSitesUseCase
CreateSiteUseCase *useCases.CreateSiteUseCase
UpdateSiteUseCase *useCases.UpdateSiteUseCase
@@ -161,6 +164,7 @@ type Repositories struct {
EntryRepo domainEntry.Repository
AuthorRepo domainAuthor.Repository
UserRepo domainUser.Repository
RoleRepo domainRole.Repository
ImageRepo domainImage.Repository
Transactor domainTransaction.Transactor
VariantRepo domainImage.VariantRepository
@@ -305,6 +309,7 @@ func getRepositories(connection *database.Connection, settings config.Settings)
EntryRepo: persistence.NewEntryRepository(db),
AuthorRepo: persistence.NewAuthorRepository(db),
UserRepo: persistence.NewUserRepository(db),
RoleRepo: persistence.NewRoleRepository(db),
ImageRepo: persistence.NewImageRepository(db),
Transactor: persistence.NewTransactor(db),
VariantRepo: persistence.NewVariantRepository(db),
@@ -366,6 +371,8 @@ func getUseCases(repos *Repositories, imageStorage domainImage.Storage, settings
UpdateAuthorUseCase: useCases.NewUpdateAuthorUseCase(repos.AuthorRepo, repos.UserRepo, repos.ImageRepo, recorder),
DeleteAuthorUseCase: useCases.NewDeleteAuthorUseCase(repos.AuthorRepo, recorder),
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(repos.AuthorRepo),
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(repos.RoleRepo),
ListPermissionsUseCase: useCases.NewListPermissionsUseCase(),
ListSitesUseCase: useCases.NewListSitesUseCase(repos.SiteRepo),
CreateSiteUseCase: useCases.NewCreateSiteUseCase(repos.SiteRepo, repos.ImageRepo, recorder),
UpdateSiteUseCase: useCases.NewUpdateSiteUseCase(repos.SiteRepo, repos.ImageRepo, recorder),
+31 -2
View File
@@ -5,6 +5,7 @@ import (
"RenewCMS/api/pkg/claims"
"RenewCMS/api/pkg/publicuser"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
@@ -15,12 +16,14 @@ import (
type MeHandler struct {
GetUseCase *useCases.GetUserUseCase
GetAuthorUseCase *useCases.GetMyAuthorUseCase
GetAccessUseCase *useCases.GetMyAccessUseCase
}
func NewMeHandler(getUser *useCases.GetUserUseCase, getAuthor *useCases.GetMyAuthorUseCase) *MeHandler {
func NewMeHandler(getUser *useCases.GetUserUseCase, getAuthor *useCases.GetMyAuthorUseCase, getAccess *useCases.GetMyAccessUseCase) *MeHandler {
return &MeHandler{
GetUseCase: getUser,
GetAuthorUseCase: getAuthor,
GetAccessUseCase: getAccess,
}
}
@@ -29,9 +32,29 @@ type meAuthor struct {
ArticleCount int `json:"article_count"`
}
type meRole struct {
ID uint32 `json:"id"`
Name string `json:"name"`
BuiltIn bool `json:"built_in"`
Locked bool `json:"locked"`
}
type meAccess struct {
Role meRole `json:"role"`
Permissions []role.Grant `json:"permissions"`
}
type meResponse struct {
publicuser.PublicUser
Author *meAuthor `json:"author"`
Access meAccess `json:"access"`
}
func toMeAccess(access useCases.Access) meAccess {
return meAccess{
Role: meRole{ID: access.Role.ID, Name: access.Role.Name, BuiltIn: access.Role.BuiltIn, Locked: access.Role.Locked},
Permissions: access.Permissions,
}
}
func toMeAuthor(found *author.Author) *meAuthor {
@@ -64,7 +87,13 @@ func (h *MeHandler) Me(w http.ResponseWriter, r *http.Request) {
return
}
body, err := json.Marshal(meResponse{PublicUser: publicuser.New(loadedUser), Author: toMeAuthor(linked)})
access, err := h.GetAccessUseCase.Get(loadedUser)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
body, err := json.Marshal(meResponse{PublicUser: publicuser.New(loadedUser), Author: toMeAuthor(linked), Access: toMeAccess(access)})
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
@@ -0,0 +1,53 @@
package permission
import (
"RenewCMS/internal/domain/role"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"net/http"
)
type Handler struct {
ListUseCase *useCases.ListPermissionsUseCase
}
func NewHandler(list *useCases.ListPermissionsUseCase) *Handler {
return &Handler{ListUseCase: list}
}
type permissionItem struct {
Key string `json:"key"`
ScopeKind role.Kind `json:"scope_kind"`
OwnerOnly bool `json:"owner_only"`
PerType bool `json:"per_type"`
}
type permissionGroup struct {
Key string `json:"key"`
Items []permissionItem `json:"items"`
}
type permissionResponse struct {
Groups []permissionGroup `json:"groups"`
}
func toResponse(groups []useCases.PermissionGroup) permissionResponse {
response := permissionResponse{Groups: make([]permissionGroup, 0, len(groups))}
for _, group := range groups {
items := make([]permissionItem, 0, len(group.Items))
for _, permission := range group.Items {
items = append(items, permissionItem{Key: permission.Key, ScopeKind: permission.Kind, OwnerOnly: permission.OwnerOnly, PerType: permission.PerType()})
}
response.Groups = append(response.Groups, permissionGroup{Key: group.Key, Items: items})
}
return response
}
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
body, err := json.Marshal(toResponse(h.ListUseCase.List()))
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
_, _ = w.Write(body)
}
+17 -1
View File
@@ -13,6 +13,7 @@ import (
"RenewCMS/api/handlers/imagevariant"
"RenewCMS/api/handlers/llms"
"RenewCMS/api/handlers/media"
"RenewCMS/api/handlers/permission"
"RenewCMS/api/handlers/settings"
"RenewCMS/api/handlers/site"
"RenewCMS/api/handlers/staticimages"
@@ -92,7 +93,7 @@ func NewAuthRouter(container *UseCases, isProduction bool, rateLimit config.Auth
loginHandler := auth.NewLoginHandler(container.Token, container.CreateUserUseCase, container.AuthenticateUserUseCase, isProduction)
verificationHandler := auth.NewVerificationHandler(container.VerifyUserUseCase, container.CancelRegistrationUseCase, isProduction)
meHandler := auth.NewMeHandler(container.GetUserUseCase, container.GetMyAuthorUseCase)
meHandler := auth.NewMeHandler(container.GetUserUseCase, container.GetMyAuthorUseCase, container.GetMyAccessUseCase)
passwordResetHandler := auth.NewPasswordResetHandler(container.RequestPasswordResetUseCase, container.ResetPasswordUseCase)
acceptInviteHandler := auth.NewAcceptInviteHandler(container.GetInviteUseCase, container.AcceptInviteUseCase, container.Token, isProduction)
@@ -375,6 +376,20 @@ func NewAPIKeyRouter(container *UseCases, trustedProxies []string) http.Handler
return r
}
func NewPermissionRouter(container *UseCases) http.Handler {
r := chi.NewRouter()
permissionHandler := permission.NewHandler(container.ListPermissionsUseCase)
r.Use(authtoken.Verifier(container.Token))
r.Use(container.apiKeyAuth())
r.Use(authtoken.Authenticator)
r.Use(middleware.RequireCurrentToken(container.GetUserUseCase))
r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase))
r.Get("/", permissionHandler.List)
return r
}
func NewMediaRouter(container *UseCases, trustedProxies []string) http.Handler {
r := chi.NewRouter()
@@ -457,6 +472,7 @@ func InitBackendRoutes(container *UseCases, settings config.Settings) *chi.Mux {
r.Mount("/media", NewMediaRouter(container, settings.TrustedProxies))
r.Mount("/image-variants", NewImageVariantRouter(container, settings.TrustedProxies))
r.Mount("/api-key", NewAPIKeyRouter(container, settings.TrustedProxies))
r.Mount("/permission", NewPermissionRouter(container))
r.Mount("/system", NewSystemRouter(container, settings.TrustedProxies))
r.Mount("/auth", NewAuthRouter(container, settings.IsProduction(), settings.AuthRateLimit, settings.TrustedProxies))