Compare commits

...

7 Commits

Author SHA1 Message Date
78a8e46b3e implement endpoint /roles 2025-12-19 14:27:17 +02:00
69281f3337 fmt 2025-12-19 14:27:01 +02:00
07ec64b1bb add acl service 2025-12-19 14:26:37 +02:00
cd465d42a3 add init functions 2025-12-19 14:26:28 +02:00
c0a187d461 implement basic acl operations and tests 2025-12-19 14:26:05 +02:00
5a34a445cf add acl service 2025-12-19 14:25:43 +02:00
e12b4dea12 add legacy route and new 2025-12-19 09:23:03 +02:00
13 changed files with 594 additions and 18 deletions

122
api/acl_admin/handle.go Normal file
View File

@@ -0,0 +1,122 @@
package api_acladmin
import (
"encoding/json"
"net/http"
"git.oblat.lv/alex/triggerssmith/internal/acl"
"git.oblat.lv/alex/triggerssmith/internal/auth"
"git.oblat.lv/alex/triggerssmith/internal/config"
"git.oblat.lv/alex/triggerssmith/internal/server"
"github.com/go-chi/chi/v5"
)
type aclAdminHandler struct {
cfg *config.Config
a *acl.Service
auth *auth.Service
}
func MustRoute(config *config.Config, aclService *acl.Service, authService *auth.Service) func(chi.Router) {
if config == nil {
panic("config is nil")
}
if aclService == nil {
panic("aclService is nil")
}
if authService == nil {
panic("authService is nil")
}
h := &aclAdminHandler{
cfg: config,
a: aclService,
auth: authService,
}
return func(r chi.Router) {
r.Get("/roles", h.getRoles)
r.Post("/create-role", h.createRole)
r.Post("/assign-role", h.assignRoleToUser)
r.Get("/user-roles", h.getUserRoles)
r.Post("/remove-role", h.removeRoleFromUser)
r.Get("/resources", h.getResources)
r.Post("/create-resource", h.createResource)
r.Post("/assign-resource", h.assignResourceToRole)
r.Get("/role-resources", h.getRoleResources)
r.Post("/remove-resource", h.removeResourceFromRole)
r.Get("/permissions", h.getResources) // legacy support
r.Post("/create-permissions", h.createResource) // legacy support
r.Post("/assign-permissions", h.assignResourceToRole) // legacy support
r.Get("/role-permissions", h.getRoleResources) // legacy support
r.Post("/remove-permissions", h.removeResourceFromRole) // legacy support
}
}
type rolesResponse []struct {
ID uint `json:"id"`
Name string `json:"name"`
}
func (h *aclAdminHandler) getRoles(w http.ResponseWriter, r *http.Request) {
roles, err := h.a.GetRoles()
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(func() rolesResponse {
// Transform acl.Role to rolesResponse
resp := make(rolesResponse, 0, len(roles))
for _, role := range roles {
resp = append(resp, struct {
ID uint `json:"id"`
Name string `json:"name"`
}{
ID: role.ID,
Name: role.Name,
})
}
return resp
}())
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
}
func (h *aclAdminHandler) createRole(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}
func (h *aclAdminHandler) assignRoleToUser(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}
func (h *aclAdminHandler) getUserRoles(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}
func (h *aclAdminHandler) removeRoleFromUser(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}
func (h *aclAdminHandler) getResources(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}
func (h *aclAdminHandler) createResource(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}
func (h *aclAdminHandler) assignResourceToRole(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}
func (h *aclAdminHandler) getRoleResources(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}
func (h *aclAdminHandler) removeResourceFromRole(w http.ResponseWriter, r *http.Request) {
server.NotImplemented(w)
}

View File

@@ -3,8 +3,8 @@
package api_auth package api_auth
import ( import (
"fmt"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"time" "time"

View File

@@ -8,8 +8,10 @@ import (
"path/filepath" "path/filepath"
"time" "time"
api_acladmin "git.oblat.lv/alex/triggerssmith/api/acl_admin"
api_auth "git.oblat.lv/alex/triggerssmith/api/auth" api_auth "git.oblat.lv/alex/triggerssmith/api/auth"
api_block "git.oblat.lv/alex/triggerssmith/api/block" api_block "git.oblat.lv/alex/triggerssmith/api/block"
"git.oblat.lv/alex/triggerssmith/internal/acl"
"git.oblat.lv/alex/triggerssmith/internal/auth" "git.oblat.lv/alex/triggerssmith/internal/auth"
"git.oblat.lv/alex/triggerssmith/internal/config" "git.oblat.lv/alex/triggerssmith/internal/config"
"git.oblat.lv/alex/triggerssmith/internal/vars" "git.oblat.lv/alex/triggerssmith/internal/vars"
@@ -23,11 +25,14 @@ type Router struct {
cfg *config.Config cfg *config.Config
authService *auth.Service authService *auth.Service
aclService *acl.Service
} }
type RouterDependencies struct { type RouterDependencies struct {
AuthService *auth.Service AuthService *auth.Service
Configuration *config.Config Configuration *config.Config
ACLService *acl.Service
} }
func NewRouter(deps RouterDependencies) *Router { func NewRouter(deps RouterDependencies) *Router {
@@ -37,11 +42,15 @@ func NewRouter(deps RouterDependencies) *Router {
if deps.Configuration == nil { if deps.Configuration == nil {
panic("Configuration is required") panic("Configuration is required")
} }
if deps.ACLService == nil {
panic("ACLService is required")
}
r := chi.NewRouter() r := chi.NewRouter()
return &Router{ return &Router{
r: r, r: r,
cfg: deps.Configuration, cfg: deps.Configuration,
authService: deps.AuthService, authService: deps.AuthService,
aclService: deps.ACLService,
} }
} }
@@ -74,7 +83,12 @@ func (r *Router) MustRoute() chi.Router {
r.r.Route("/api", func(api chi.Router) { r.r.Route("/api", func(api chi.Router) {
api.Route("/block", api_block.MustRoute(r.cfg)) api.Route("/block", api_block.MustRoute(r.cfg))
api.Route("/users", api_auth.MustRoute(r.cfg, r.authService)) authRoute := api_auth.MustRoute(r.cfg, r.authService)
api.Route("/auth", authRoute)
api.Route("/users", authRoute) // legacy support
aclAdminRoute := api_acladmin.MustRoute(r.cfg, r.aclService, r.authService)
api.Route("/acl", aclAdminRoute)
api.Route("/acl-admin", aclAdminRoute) // legacy support
}) })
r.r.Get("/health", func(w http.ResponseWriter, r *http.Request) { r.r.Get("/health", func(w http.ResponseWriter, r *http.Request) {

View File

@@ -12,6 +12,7 @@ import (
"time" "time"
"git.oblat.lv/alex/triggerssmith/api" "git.oblat.lv/alex/triggerssmith/api"
"git.oblat.lv/alex/triggerssmith/internal/acl"
application "git.oblat.lv/alex/triggerssmith/internal/app" application "git.oblat.lv/alex/triggerssmith/internal/app"
"git.oblat.lv/alex/triggerssmith/internal/auth" "git.oblat.lv/alex/triggerssmith/internal/auth"
"git.oblat.lv/alex/triggerssmith/internal/config" "git.oblat.lv/alex/triggerssmith/internal/config"
@@ -99,10 +100,10 @@ var serveCmd = &cobra.Command{
} else { } else {
defer f.Close() defer f.Close()
slog.Debug("flushing stack in to panic.log") slog.Debug("flushing stack in to panic.log")
fmt.Fprintln(f, "\n--------------------------------------------------------\n") fmt.Fprintf(f, "\n--------------------------------------------------------\n")
fmt.Fprintf(f, "Time: %s\n", time.Now().Format(time.RFC3339)) fmt.Fprintf(f, "Time: %s\n", time.Now().Format(time.RFC3339))
fmt.Fprintln(f, "If this is unexpected, please report: https://git.oblat.lv/alex/triggerssmith/issues") fmt.Fprintln(f, "If this is unexpected, please report: https://git.oblat.lv/alex/triggerssmith/issues")
fmt.Fprintln(f, "\n--------------------------------------------------------\n") fmt.Fprintf(f, "\n--------------------------------------------------------\n")
fmt.Fprintf(f, "Panic: %v\n", r) fmt.Fprintf(f, "Panic: %v\n", r)
f.Write(stack) f.Write(stack)
f.WriteString("\n\n") f.WriteString("\n\n")
@@ -179,34 +180,63 @@ var serveCmd = &cobra.Command{
slog.Error("Failed to open token database", slog.String("error", err.Error())) slog.Error("Failed to open token database", slog.String("error", err.Error()))
return return
} }
err = tokenDb.AutoMigrate(&token.Token{}) // err = tokenDb.AutoMigrate(&token.Token{})
if err != nil { // if err != nil {
slog.Error("Failed to migrate token database", slog.String("error", err.Error())) // slog.Error("Failed to migrate token database", slog.String("error", err.Error()))
return // return
} // }
tokenStore, err := token.NewSQLiteTokenStore(tokenDb) tokenStore, err := token.NewSQLiteTokenStore(tokenDb)
if err != nil { if err != nil {
slog.Error("Failed to create token store", slog.String("error", err.Error())) slog.Error("Failed to create token store", slog.String("error", err.Error()))
return return
} }
tokenService, err := token.NewTokenService(&cfg.Auth, tokenStore) tokenService, err := token.NewTokenService(&cfg.Auth, tokenStore)
if err != nil {
slog.Error("Failed to create token service", slog.String("error", err.Error()))
return
}
err = tokenService.Init()
if err != nil {
slog.Error("Failed to initialize token service", slog.String("error", err.Error()))
return
}
userDb, err := gorm.Open(sqlite.Open(filepath.Join(cfg.Data.DataPath, "users.sqlite3")), &gorm.Config{}) // also acl !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
userData, err := gorm.Open(sqlite.Open(filepath.Join(cfg.Data.DataPath, "user_data.sqlite3")), &gorm.Config{})
if err != nil { if err != nil {
slog.Error("Failed to open user database", slog.String("error", err.Error())) slog.Error("Failed to open user database", slog.String("error", err.Error()))
return return
} }
err = userDb.AutoMigrate(&user.User{}) // err =
if err != nil { // if err != nil {
slog.Error("Failed to migrate user database", slog.String("error", err.Error())) // slog.Error("Failed to migrate user database", slog.String("error", err.Error()))
return // return
} // }
userStore, err := user.NewGormUserStore(userDb) userStore, err := user.NewGormUserStore(userData)
if err != nil { if err != nil {
slog.Error("Failed to create user store", slog.String("error", err.Error())) slog.Error("Failed to create user store", slog.String("error", err.Error()))
return return
} }
userService, err := user.NewService(userStore) userService, err := user.NewService(userStore)
if err != nil {
slog.Error("Failed to create user service", slog.String("error", err.Error()))
return
}
err = userService.Init()
if err != nil {
slog.Error("Failed to initialize user service", slog.String("error", err.Error()))
return
}
aclService, err := acl.NewService(userData)
if err != nil {
slog.Error("Failed to create acl service", slog.String("error", err.Error()))
return
}
err = aclService.Init()
if err != nil {
slog.Error("Failed to initialize acl service", slog.String("error", err.Error()))
return
}
authService, err := auth.NewAuthService(auth.AuthServiceDependencies{ authService, err := auth.NewAuthService(auth.AuthServiceDependencies{
Configuration: cfg, Configuration: cfg,
@@ -223,6 +253,7 @@ var serveCmd = &cobra.Command{
router := api.NewRouter(api.RouterDependencies{ router := api.NewRouter(api.RouterDependencies{
AuthService: authService, AuthService: authService,
Configuration: cfg, Configuration: cfg,
ACLService: aclService,
}) })
srv.SetHandler(router.MustRoute()) srv.SetHandler(router.MustRoute())

30
internal/acl/models.go Normal file
View File

@@ -0,0 +1,30 @@
package acl
type UserRole struct {
UserID uint `gorm:"primaryKey" json:"userId"`
RoleID uint `gorm:"primaryKey" json:"roleId"`
Role Role `gorm:"constraint:OnDelete:CASCADE;foreignKey:RoleID;references:ID" json:"role"`
//User user.User `gorm:"constraint:OnDelete:CASCADE;foreignKey:UserID;references:ID"`
}
type Resource struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Key string `gorm:"unique;not null" json:"key"`
}
type Role struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"unique;not null" json:"name"`
Resources []Resource `gorm:"many2many:role_resources" json:"resources"`
//Users []user.User `gorm:"many2many:user_roles"`
}
type RoleResource struct {
RoleID uint `gorm:"primaryKey" json:"roleId"`
ResourceID uint `gorm:"primaryKey" json:"resourceId"`
Role Role `gorm:"constraint:OnDelete:CASCADE;foreignKey:RoleID;references:ID" json:"role"`
Resource Resource `gorm:"constraint:OnDelete:CASCADE;foreignKey:ResourceID;references:ID" json:"resource"`
}

143
internal/acl/service.go Normal file
View File

@@ -0,0 +1,143 @@
package acl
import (
"fmt"
"gorm.io/gorm"
)
type Service struct {
initialized bool
db *gorm.DB
}
func NewService(db *gorm.DB) (*Service, error) {
if db == nil {
return nil, fmt.Errorf("db is required")
}
return &Service{
db: db,
}, nil
}
func (s *Service) isInitialized() bool {
return s.initialized
}
func (s *Service) Init() error {
if s.isInitialized() {
return nil
}
// AutoMigrate models
err := s.db.AutoMigrate(&UserRole{}, &Resource{}, &Role{}, &RoleResource{})
if err != nil {
return fmt.Errorf("failed to migrate ACL models: %w", err)
}
s.initialized = true
return nil
}
// Admin crud functions
// CreateRole creates a new role with the given name
func (s *Service) CreateRole(name string) error {
if !s.isInitialized() {
return fmt.Errorf("acl service is not initialized")
}
role := Role{Name: name}
return s.db.FirstOrCreate(&role, &Role{Name: name}).Error
}
// CreateResource creates a new resource with the given key
func (s *Service) CreateResource(key string) error {
if !s.isInitialized() {
return fmt.Errorf("acl service is not initialized")
}
res := Resource{Key: key}
return s.db.FirstOrCreate(&res, &Resource{Key: key}).Error
}
// AssignResourceToRole assigns a resource to a role
func (s *Service) AssignResourceToRole(roleID, resourceID uint) error {
if !s.isInitialized() {
return fmt.Errorf("acl service is not initialized")
}
rr := RoleResource{
RoleID: roleID,
ResourceID: resourceID,
}
return s.db.FirstOrCreate(&rr, RoleResource{RoleID: roleID, ResourceID: resourceID}).Error
}
// AssignRoleToUser assigns a role to a user
func (s *Service) AssignRoleToUser(roleID, userID uint) error {
if !s.isInitialized() {
return fmt.Errorf("acl service is not initialized")
}
ur := UserRole{
UserID: userID,
RoleID: roleID,
}
return s.db.FirstOrCreate(&ur, UserRole{UserID: userID, RoleID: roleID}).Error
}
// RemoveResourceFromRole removes a resource from a role
func (s *Service) RemoveResourceFromRole(roleID, resourceID uint) error {
if !s.isInitialized() {
return fmt.Errorf("acl service is not initialized")
}
return s.db.Where("role_id = ? AND resource_id = ?", roleID, resourceID).Delete(&RoleResource{}).Error
}
// RemoveRoleFromUser removes a role from a user
func (s *Service) RemoveRoleFromUser(roleID, userID uint) error {
if !s.isInitialized() {
return fmt.Errorf("acl service is not initialized")
}
return s.db.Where("role_id = ? AND user_id = ?", roleID, userID).Delete(&UserRole{}).Error
}
// GetRoles returns all roles
func (s *Service) GetRoles() ([]Role, error) {
if !s.isInitialized() {
return nil, fmt.Errorf("acl service is not initialized")
}
var roles []Role
err := s.db.Preload("Resources").Order("id").Find(&roles).Error
return roles, err
}
// GetPermissions returns all permissions
func (s *Service) GetPermissions() ([]Resource, error) {
if !s.isInitialized() {
return nil, fmt.Errorf("acl service is not initialized")
}
var resources []Resource
err := s.db.Order("id").Find(&resources).Error
return resources, err
}
// GetRoleResources returns all resources for a given role
func (s *Service) GetRoleResources(roleID uint) ([]Resource, error) {
if !s.isInitialized() {
return nil, fmt.Errorf("acl service is not initialized")
}
var resources []Resource
err := s.db.Joins("JOIN role_resources rr ON rr.resource_id = resources.id").
Where("rr.role_id = ?", roleID).Find(&resources).Error
return resources, err
}
// GetUserRoles returns all roles for a given user
func (s *Service) GetUserRoles(userID uint) ([]Role, error) {
if !s.isInitialized() {
return nil, fmt.Errorf("acl service is not initialized")
}
var roles []Role
err := s.db.Joins("JOIN user_roles ur ON ur.role_id = roles.id").
Where("ur.user_id = ?", userID).Find(&roles).Error
return roles, err
}

View File

@@ -0,0 +1,156 @@
package acl_test
import (
"os"
"path/filepath"
"testing"
"git.oblat.lv/alex/triggerssmith/internal/acl"
"git.oblat.lv/alex/triggerssmith/internal/user"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func openTestDB(t *testing.T) *gorm.DB {
t.Helper()
// Путь к файлу базы
dbPath := filepath.Join("testdata", "test.db")
// Удаляем старую базу, если есть
os.Remove(dbPath)
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open test db: %v", err)
}
// Миграция таблицы User для связи с ACL
if err := db.AutoMigrate(&user.User{}); err != nil {
t.Fatalf("failed to migrate User: %v", err)
}
return db
}
func TestACLService_CRUD(t *testing.T) {
db := openTestDB(t)
// Создаём сервис ACL
svc, err := acl.NewService(db)
if err != nil {
t.Fatalf("failed to create ACL service: %v", err)
}
if err := svc.Init(); err != nil {
t.Fatalf("failed to init ACL service: %v", err)
}
// Создаём роли
if err := svc.CreateRole("admin"); err != nil {
t.Fatalf("CreateRole failed: %v", err)
}
if err := svc.CreateRole("guest"); err != nil {
t.Fatalf("CreateRole failed: %v", err)
}
roles, err := svc.GetRoles()
if err != nil {
t.Fatalf("GetRoles failed: %v", err)
}
if len(roles) != 2 {
t.Fatalf("expected 2 roles, got %d", len(roles))
}
// Создаём ресурсы
if err := svc.CreateResource("*"); err != nil {
t.Fatalf("CreateResource failed: %v", err)
}
if err := svc.CreateResource("html.view.*"); err != nil {
t.Fatalf("CreateResource failed: %v", err)
}
resources, err := svc.GetPermissions()
if err != nil {
t.Fatalf("GetPermissions failed: %v", err)
}
if len(resources) != 2 {
t.Fatalf("expected 2 resources, got %d", len(resources))
}
// 1. Создаём сервис user
store, err := user.NewGormUserStore(db)
if err != nil {
t.Fatalf("failed to create user store: %v", err)
}
userSvc, err := user.NewService(store)
if err != nil {
t.Fatalf("failed to create user service: %v", err)
}
// 2. Инициализируем
if err := userSvc.Init(); err != nil {
t.Fatalf("failed to init user service: %v", err)
}
user := &user.User{
Username: "testuser",
Email: "testuser@example.com",
Password: "secret",
}
u := user
// 3. Создаём пользователя через сервис
err = userSvc.Create(user)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Привязываем роль к пользователю
adminRoleID := roles[0].ID
if err := svc.AssignRoleToUser(adminRoleID, uint(u.ID)); err != nil {
t.Fatalf("AssignRoleToUser failed: %v", err)
}
userRoles, err := svc.GetUserRoles(uint(u.ID))
if err != nil {
t.Fatalf("GetUserRoles failed: %v", err)
}
if len(userRoles) != 1 || userRoles[0].ID != adminRoleID {
t.Fatalf("expected user to have admin role")
}
// Привязываем ресурсы к роли
for _, res := range resources {
if err := svc.AssignResourceToRole(adminRoleID, res.ID); err != nil {
t.Fatalf("AssignResourceToRole failed: %v", err)
}
}
roleResources, err := svc.GetRoleResources(adminRoleID)
if err != nil {
t.Fatalf("GetRoleResources failed: %v", err)
}
if len(roleResources) != 2 {
t.Fatalf("expected role to have 2 resources")
}
// Удаляем ресурс из роли
if err := svc.RemoveResourceFromRole(adminRoleID, resources[0].ID); err != nil {
t.Fatalf("RemoveResourceFromRole failed: %v", err)
}
roleResources, _ = svc.GetRoleResources(adminRoleID)
if len(roleResources) != 1 {
t.Fatalf("expected 1 resource after removal")
}
// Удаляем роль у пользователя
if err := svc.RemoveRoleFromUser(adminRoleID, uint(u.ID)); err != nil {
t.Fatalf("RemoveRoleFromUser failed: %v", err)
}
userRoles, _ = svc.GetUserRoles(uint(u.ID))
if len(userRoles) != 0 {
t.Fatalf("expected user to have 0 roles after removal")
}
}

View File

@@ -10,9 +10,13 @@ import (
type TokenStore interface { type TokenStore interface {
revoke(tokenID string, expiresAt time.Time) error revoke(tokenID string, expiresAt time.Time) error
isRevoked(tokenID string) (bool, error) isRevoked(tokenID string) (bool, error)
init() error
} }
type Service struct { type Service struct {
initialized bool
cfg *config.Auth cfg *config.Auth
store TokenStore store TokenStore
} }
@@ -27,6 +31,24 @@ func NewTokenService(cfg *config.Auth, store TokenStore) (*Service, error) {
return &Service{cfg: cfg, store: store}, nil return &Service{cfg: cfg, store: store}, nil
} }
func (s *Service) isInitialized() bool {
return s.initialized
}
func (s *Service) Init() error {
if s.isInitialized() {
return nil
}
err := s.store.init()
if err != nil {
return fmt.Errorf("failed to initialize token store: %w", err)
}
s.initialized = true
return nil
}
func (s *Service) Revoke(jti string, exp time.Time) error { func (s *Service) Revoke(jti string, exp time.Time) error {
return s.store.revoke(jti, exp) return s.store.revoke(jti, exp)
} }

View File

@@ -43,3 +43,13 @@ func (s *SQLiteTokenStore) isRevoked(tokenID string) (bool, error) {
} }
return count > 0, nil return count > 0, nil
} }
func (s *SQLiteTokenStore) init() error {
// AutoMigrate models
err := s.db.AutoMigrate(&Token{})
if err != nil {
return fmt.Errorf("failed to migrate Token model: %w", err)
}
return nil
}

View File

@@ -43,3 +43,13 @@ func (s *GormUserStore) Update(user *User) error {
func (s *GormUserStore) Delete(id int64) error { func (s *GormUserStore) Delete(id int64) error {
return s.db.Delete(&User{}, id).Error return s.db.Delete(&User{}, id).Error
} }
func (s *GormUserStore) init() error {
// AutoMigrate models
err := s.db.AutoMigrate(&User{})
if err != nil {
return fmt.Errorf("failed to migrate User model: %w", err)
}
return nil
}

View File

@@ -1,11 +1,15 @@
package user package user
import "gorm.io/gorm" import (
"git.oblat.lv/alex/triggerssmith/internal/acl"
"gorm.io/gorm"
)
type User struct { type User struct {
ID int64 `gorm:"primaryKey"` ID int64 `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex;not null"` Username string `gorm:"uniqueIndex;not null"`
Email string `gorm:"uniqueIndex;not null"` Email string `gorm:"uniqueIndex;not null"`
Password string `gorm:"not null"` Password string `gorm:"not null"`
Roles []acl.Role `gorm:"many2many:user_roles"`
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
} }

View File

@@ -3,6 +3,8 @@ package user
import "fmt" import "fmt"
type Service struct { type Service struct {
initialized bool
store UserCRUD store UserCRUD
} }
@@ -15,18 +17,48 @@ func NewService(store UserCRUD) (*Service, error) {
}, nil }, nil
} }
func (s *Service) isInitialized() bool {
return s.initialized
}
func (s *Service) Init() error {
if s.isInitialized() {
return nil
}
err := s.store.init()
if err != nil {
return fmt.Errorf("failed to initialize user store: %w", err)
}
s.initialized = true
return nil
}
func (s *Service) Create(user *User) error { func (s *Service) Create(user *User) error {
if !s.isInitialized() {
return fmt.Errorf("user service is not initialized")
}
return s.store.Create(user) return s.store.Create(user)
} }
func (s *Service) GetBy(by, value string) (*User, error) { func (s *Service) GetBy(by, value string) (*User, error) {
if !s.isInitialized() {
return nil, fmt.Errorf("user service is not initialized")
}
return s.store.GetBy(by, value) return s.store.GetBy(by, value)
} }
func (s *Service) Update(user *User) error { func (s *Service) Update(user *User) error {
if !s.isInitialized() {
return fmt.Errorf("user service is not initialized")
}
return s.store.Update(user) return s.store.Update(user)
} }
func (s *Service) Delete(id int64) error { func (s *Service) Delete(id int64) error {
if !s.isInitialized() {
return fmt.Errorf("user service is not initialized")
}
return s.store.Delete(id) return s.store.Delete(id)
} }

View File

@@ -5,4 +5,6 @@ type UserCRUD interface {
GetBy(by, value string) (*User, error) GetBy(by, value string) (*User, error)
Update(user *User) error Update(user *User) error
Delete(id int64) error Delete(id int64) error
init() error
} }