mirror of
https://github.com/btouchard/ackify-ce.git
synced 2026-02-28 10:48:47 -06:00
refacto(backend): extract coreapp packages for DI and authorization
- Add pkg/coreapp/ with service interfaces and dependency injection - Add DocumentAuthorizer for document access control - Add ExpectedSignerService for expected signers management - Simplify router and handlers by using coreapp dependencies
This commit is contained in:
@@ -121,6 +121,27 @@ func (f *fakeDocumentRepository) FindByReference(_ context.Context, ref string,
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeDocumentRepository) List(_ context.Context, limit, offset int) ([]*models.Document, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeDocumentRepository) Search(_ context.Context, query string, limit, offset int) ([]*models.Document, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeDocumentRepository) Count(_ context.Context, searchQuery string) (int, error) {
|
||||
return len(f.documents), nil
|
||||
}
|
||||
|
||||
func (f *fakeDocumentRepository) CreateOrUpdate(ctx context.Context, docID string, input models.DocumentInput, createdBy string) (*models.Document, error) {
|
||||
return f.Create(ctx, docID, input, createdBy)
|
||||
}
|
||||
|
||||
func (f *fakeDocumentRepository) Delete(_ context.Context, docID string) error {
|
||||
delete(f.documents, docID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestChecksumService_ValidateChecksumFormat(t *testing.T) {
|
||||
service := NewChecksumService(newFakeVerificationRepository(), newFakeDocumentRepository())
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ type documentRepository interface {
|
||||
Create(ctx context.Context, docID string, input models.DocumentInput, createdBy string) (*models.Document, error)
|
||||
GetByDocID(ctx context.Context, docID string) (*models.Document, error)
|
||||
FindByReference(ctx context.Context, ref string, refType string) (*models.Document, error)
|
||||
List(ctx context.Context, limit, offset int) ([]*models.Document, error)
|
||||
Search(ctx context.Context, query string, limit, offset int) ([]*models.Document, error)
|
||||
Count(ctx context.Context, searchQuery string) (int, error)
|
||||
CreateOrUpdate(ctx context.Context, docID string, input models.DocumentInput, createdBy string) (*models.Document, error)
|
||||
Delete(ctx context.Context, docID string) error
|
||||
}
|
||||
|
||||
// DocumentService handles document metadata operations and unique ID generation
|
||||
@@ -442,3 +447,33 @@ func (s *DocumentService) FindOrCreateDocument(ctx context.Context, ref string)
|
||||
|
||||
return doc, true, nil
|
||||
}
|
||||
|
||||
// List returns documents with pagination
|
||||
func (s *DocumentService) List(ctx context.Context, limit, offset int) ([]*models.Document, error) {
|
||||
return s.repo.List(ctx, limit, offset)
|
||||
}
|
||||
|
||||
// Search searches documents by query with pagination
|
||||
func (s *DocumentService) Search(ctx context.Context, query string, limit, offset int) ([]*models.Document, error) {
|
||||
return s.repo.Search(ctx, query, limit, offset)
|
||||
}
|
||||
|
||||
// Count returns the total count of documents, optionally filtered by search query
|
||||
func (s *DocumentService) Count(ctx context.Context, searchQuery string) (int, error) {
|
||||
return s.repo.Count(ctx, searchQuery)
|
||||
}
|
||||
|
||||
// GetByDocID retrieves a document by its ID
|
||||
func (s *DocumentService) GetByDocID(ctx context.Context, docID string) (*models.Document, error) {
|
||||
return s.repo.GetByDocID(ctx, docID)
|
||||
}
|
||||
|
||||
// CreateOrUpdate creates a new document or updates an existing one
|
||||
func (s *DocumentService) CreateOrUpdate(ctx context.Context, docID string, input models.DocumentInput, createdBy string) (*models.Document, error) {
|
||||
return s.repo.CreateOrUpdate(ctx, docID, input, createdBy)
|
||||
}
|
||||
|
||||
// Delete removes a document by its ID
|
||||
func (s *DocumentService) Delete(ctx context.Context, docID string) error {
|
||||
return s.repo.Delete(ctx, docID)
|
||||
}
|
||||
|
||||
@@ -67,6 +67,27 @@ func (m *mockDocRepo) FindByReference(ctx context.Context, ref string, refType s
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockDocRepo) List(ctx context.Context, limit, offset int) ([]*models.Document, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockDocRepo) Search(ctx context.Context, query string, limit, offset int) ([]*models.Document, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockDocRepo) Count(ctx context.Context, searchQuery string) (int, error) {
|
||||
return len(m.documents), nil
|
||||
}
|
||||
|
||||
func (m *mockDocRepo) CreateOrUpdate(ctx context.Context, docID string, input models.DocumentInput, createdBy string) (*models.Document, error) {
|
||||
return m.Create(ctx, docID, input, createdBy)
|
||||
}
|
||||
|
||||
func (m *mockDocRepo) Delete(ctx context.Context, docID string) error {
|
||||
delete(m.documents, docID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestFindOrCreateDocument_SameReferenceTwice tests that calling FindOrCreateDocument
|
||||
// with the same reference twice does NOT create duplicate documents
|
||||
func TestFindOrCreateDocument_SameReferenceTwice(t *testing.T) {
|
||||
|
||||
@@ -378,6 +378,26 @@ func (m *mockDocumentRepository) FindByReference(ctx context.Context, ref string
|
||||
return nil, nil // Not found by default
|
||||
}
|
||||
|
||||
func (m *mockDocumentRepository) List(ctx context.Context, limit, offset int) ([]*models.Document, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockDocumentRepository) Search(ctx context.Context, query string, limit, offset int) ([]*models.Document, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockDocumentRepository) Count(ctx context.Context, searchQuery string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockDocumentRepository) CreateOrUpdate(ctx context.Context, docID string, input models.DocumentInput, createdBy string) (*models.Document, error) {
|
||||
return m.Create(ctx, docID, input, createdBy)
|
||||
}
|
||||
|
||||
func (m *mockDocumentRepository) Delete(ctx context.Context, docID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test CreateDocument with URL reference
|
||||
func TestDocumentService_CreateDocument_WithURL(t *testing.T) {
|
||||
mockRepo := &mockDocumentRepository{}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/btouchard/ackify-ce/backend/internal/domain/models"
|
||||
)
|
||||
|
||||
type expectedSignerRepo interface {
|
||||
ListByDocID(ctx context.Context, docID string) ([]*models.ExpectedSigner, error)
|
||||
ListWithStatusByDocID(ctx context.Context, docID string) ([]*models.ExpectedSignerWithStatus, error)
|
||||
AddExpected(ctx context.Context, docID string, contacts []models.ContactInfo, addedBy string) error
|
||||
Remove(ctx context.Context, docID, email string) error
|
||||
GetStats(ctx context.Context, docID string) (*models.DocCompletionStats, error)
|
||||
}
|
||||
|
||||
// ExpectedSignerService handles expected signer operations
|
||||
type ExpectedSignerService struct {
|
||||
repo expectedSignerRepo
|
||||
}
|
||||
|
||||
// NewExpectedSignerService creates a new expected signer service
|
||||
func NewExpectedSignerService(repo expectedSignerRepo) *ExpectedSignerService {
|
||||
return &ExpectedSignerService{repo: repo}
|
||||
}
|
||||
|
||||
// ListByDocID returns all expected signers for a document
|
||||
func (s *ExpectedSignerService) ListByDocID(ctx context.Context, docID string) ([]*models.ExpectedSigner, error) {
|
||||
return s.repo.ListByDocID(ctx, docID)
|
||||
}
|
||||
|
||||
// ListWithStatusByDocID returns all expected signers with their signature status
|
||||
func (s *ExpectedSignerService) ListWithStatusByDocID(ctx context.Context, docID string) ([]*models.ExpectedSignerWithStatus, error) {
|
||||
return s.repo.ListWithStatusByDocID(ctx, docID)
|
||||
}
|
||||
|
||||
// AddExpected adds expected signers to a document
|
||||
func (s *ExpectedSignerService) AddExpected(ctx context.Context, docID string, contacts []models.ContactInfo, addedBy string) error {
|
||||
return s.repo.AddExpected(ctx, docID, contacts, addedBy)
|
||||
}
|
||||
|
||||
// Remove removes an expected signer from a document
|
||||
func (s *ExpectedSignerService) Remove(ctx context.Context, docID, email string) error {
|
||||
return s.repo.Remove(ctx, docID, email)
|
||||
}
|
||||
|
||||
// GetStats returns completion statistics for a document
|
||||
func (s *ExpectedSignerService) GetStats(ctx context.Context, docID string) (*models.DocCompletionStats, error) {
|
||||
return s.repo.GetStats(ctx, docID)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
package authorization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/btouchard/ackify-ce/backend/internal/domain/models"
|
||||
)
|
||||
|
||||
type CEDocumentAuthorizer struct {
|
||||
adminEmails []string
|
||||
onlyAdminCanCreate bool
|
||||
}
|
||||
|
||||
func NewCEDocumentAuthorizer(adminEmails []string, onlyAdminCanCreate bool) *CEDocumentAuthorizer {
|
||||
return &CEDocumentAuthorizer{
|
||||
adminEmails: adminEmails,
|
||||
onlyAdminCanCreate: onlyAdminCanCreate,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *CEDocumentAuthorizer) CanCreateDocument(ctx context.Context, user *models.User) bool {
|
||||
if !a.onlyAdminCanCreate {
|
||||
return true
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, adminEmail := range a.adminEmails {
|
||||
if strings.EqualFold(user.Email, adminEmail) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -42,26 +42,34 @@ type webhookPublisher interface {
|
||||
Publish(ctx context.Context, eventType string, payload map[string]interface{}) error
|
||||
}
|
||||
|
||||
// signatureService defines the interface for signature operations (used for counts)
|
||||
type signatureService interface {
|
||||
GetDocumentSignatures(ctx context.Context, docID string) ([]*models.Signature, error)
|
||||
}
|
||||
|
||||
// documentAuthorizer defines the interface for document creation authorization
|
||||
type documentAuthorizer interface {
|
||||
CanCreateDocument(ctx context.Context, user *models.User) bool
|
||||
}
|
||||
|
||||
// Handler handles document API requests
|
||||
type Handler struct {
|
||||
signatureService *services.SignatureService
|
||||
signatureService signatureService
|
||||
documentService documentService
|
||||
documentRepo documentRepository
|
||||
expectedSignerRepo expectedSignerRepository
|
||||
webhookPublisher webhookPublisher
|
||||
adminEmails []string
|
||||
onlyAdminCanCreate bool
|
||||
authorizer documentAuthorizer
|
||||
}
|
||||
|
||||
// NewHandler creates a handler with all dependencies for full functionality
|
||||
func NewHandler(
|
||||
signatureService *services.SignatureService,
|
||||
signatureService signatureService,
|
||||
documentService documentService,
|
||||
documentRepo documentRepository,
|
||||
expectedSignerRepo expectedSignerRepository,
|
||||
publisher webhookPublisher,
|
||||
adminEmails []string,
|
||||
onlyAdminCanCreate bool,
|
||||
authorizer documentAuthorizer,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
signatureService: signatureService,
|
||||
@@ -69,8 +77,7 @@ func NewHandler(
|
||||
documentRepo: documentRepo,
|
||||
expectedSignerRepo: expectedSignerRepo,
|
||||
webhookPublisher: publisher,
|
||||
adminEmails: adminEmails,
|
||||
onlyAdminCanCreate: onlyAdminCanCreate,
|
||||
authorizer: authorizer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,32 +124,14 @@ type CreateDocumentResponse struct {
|
||||
func (h *Handler) HandleCreateDocument(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Check if only admins can create documents
|
||||
if h.onlyAdminCanCreate {
|
||||
user, authenticated := shared.GetUserFromContext(ctx)
|
||||
if !authenticated {
|
||||
logger.Logger.Warn("Unauthenticated user attempted to create document",
|
||||
"remote_addr", r.RemoteAddr)
|
||||
user, _ := shared.GetUserFromContext(ctx)
|
||||
if !h.authorizer.CanCreateDocument(ctx, user) {
|
||||
if user == nil {
|
||||
shared.WriteError(w, http.StatusUnauthorized, shared.ErrCodeUnauthorized, "Authentication required to create document", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
isAdmin := false
|
||||
for _, adminEmail := range h.adminEmails {
|
||||
if strings.ToLower(user.Email) == strings.ToLower(adminEmail) {
|
||||
isAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
logger.Logger.Warn("Non-admin user attempted to create document",
|
||||
"user_email", user.Email,
|
||||
"remote_addr", r.RemoteAddr)
|
||||
shared.WriteError(w, http.StatusForbidden, shared.ErrCodeForbidden, "Only administrators can create documents", nil)
|
||||
return
|
||||
} else {
|
||||
shared.WriteError(w, http.StatusForbidden, shared.ErrCodeForbidden, "Not authorized to create documents", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
@@ -470,8 +459,7 @@ func (h *Handler) HandleFindOrCreateDocument(w http.ResponseWriter, r *http.Requ
|
||||
"reference", ref,
|
||||
"remote_addr", r.RemoteAddr)
|
||||
|
||||
// Check if user is authenticated
|
||||
user, isAuthenticated := shared.GetUserFromContext(ctx)
|
||||
user, _ := shared.GetUserFromContext(ctx)
|
||||
|
||||
// First, try to find the document (without creating)
|
||||
refType := detectReferenceType(ref)
|
||||
@@ -505,35 +493,16 @@ func (h *Handler) HandleFindOrCreateDocument(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
// Document doesn't exist - check authentication before creating
|
||||
if !isAuthenticated {
|
||||
logger.Logger.Warn("Unauthenticated user attempted to create document",
|
||||
"reference", ref,
|
||||
"remote_addr", r.RemoteAddr)
|
||||
shared.WriteError(w, http.StatusUnauthorized, shared.ErrCodeUnauthorized, "Authentication required to create document", nil)
|
||||
// Document doesn't exist - check authorization before creating
|
||||
if !h.authorizer.CanCreateDocument(ctx, user) {
|
||||
if user == nil {
|
||||
shared.WriteError(w, http.StatusUnauthorized, shared.ErrCodeUnauthorized, "Authentication required to create document", nil)
|
||||
} else {
|
||||
shared.WriteError(w, http.StatusForbidden, shared.ErrCodeForbidden, "Not authorized to create documents", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if only admins can create documents
|
||||
if h.onlyAdminCanCreate {
|
||||
isAdmin := false
|
||||
for _, adminEmail := range h.adminEmails {
|
||||
if strings.ToLower(user.Email) == strings.ToLower(adminEmail) {
|
||||
isAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
logger.Logger.Warn("Non-admin user attempted to create document via find-or-create",
|
||||
"user_email", user.Email,
|
||||
"reference", ref,
|
||||
"remote_addr", r.RemoteAddr)
|
||||
shared.WriteError(w, http.StatusForbidden, shared.ErrCodeForbidden, "Only administrators can create documents", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// User is authenticated, create the document
|
||||
doc, isNew, err := h.documentService.FindOrCreateDocument(ctx, ref)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/shared"
|
||||
)
|
||||
|
||||
// Note: services import is still needed for CreateDocumentRequest
|
||||
|
||||
// ============================================================================
|
||||
// TEST FIXTURES & MOCKS
|
||||
// ============================================================================
|
||||
@@ -103,10 +105,23 @@ func (m *mockSignatureService) GetDocumentSignatures(ctx context.Context, docID
|
||||
return []*models.Signature{testSignature}, nil
|
||||
}
|
||||
|
||||
// Mock document authorizer
|
||||
type mockDocumentAuthorizer struct {
|
||||
canCreateFunc func(ctx context.Context, user *models.User) bool
|
||||
}
|
||||
|
||||
func (m *mockDocumentAuthorizer) CanCreateDocument(ctx context.Context, user *models.User) bool {
|
||||
if m.canCreateFunc != nil {
|
||||
return m.canCreateFunc(ctx, user)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func createTestHandler() *Handler {
|
||||
return &Handler{
|
||||
signatureService: &services.SignatureService{}, // Not used in these tests
|
||||
signatureService: &mockSignatureService{},
|
||||
documentService: &mockDocumentService{},
|
||||
authorizer: &mockDocumentAuthorizer{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,14 +136,16 @@ func addUserToContext(ctx context.Context, user *models.User) context.Context {
|
||||
func TestNewHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sigService := &services.SignatureService{}
|
||||
sigService := &mockSignatureService{}
|
||||
docService := &mockDocumentService{}
|
||||
authorizer := &mockDocumentAuthorizer{}
|
||||
|
||||
handler := NewHandler(sigService, docService, nil, nil, nil, nil, false)
|
||||
handler := NewHandler(sigService, docService, nil, nil, nil, authorizer)
|
||||
|
||||
assert.NotNil(t, handler)
|
||||
assert.Equal(t, sigService, handler.signatureService)
|
||||
assert.Equal(t, docService, handler.documentService)
|
||||
assert.Equal(t, authorizer, handler.authorizer)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -174,6 +191,7 @@ func TestHandler_HandleCreateDocument_Success(t *testing.T) {
|
||||
|
||||
handler := &Handler{
|
||||
documentService: mockDocService,
|
||||
authorizer: &mockDocumentAuthorizer{},
|
||||
}
|
||||
|
||||
reqBody := CreateDocumentRequest{
|
||||
@@ -272,6 +290,7 @@ func TestHandler_HandleCreateDocument_ServiceError(t *testing.T) {
|
||||
|
||||
handler := &Handler{
|
||||
documentService: mockDocService,
|
||||
authorizer: &mockDocumentAuthorizer{},
|
||||
}
|
||||
|
||||
reqBody := CreateDocumentRequest{
|
||||
@@ -422,6 +441,7 @@ func TestHandler_HandleFindOrCreateDocument_FindExisting(t *testing.T) {
|
||||
|
||||
handler := &Handler{
|
||||
documentService: mockDocService,
|
||||
authorizer: &mockDocumentAuthorizer{},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/find-or-create?ref=https://example.com/doc.pdf", nil)
|
||||
@@ -446,7 +466,6 @@ func TestHandler_HandleFindOrCreateDocument_CreateNew(t *testing.T) {
|
||||
|
||||
mockDocService := &mockDocumentService{
|
||||
findByReferenceFunc: func(ctx context.Context, ref string, refType string) (*models.Document, error) {
|
||||
// Document not found - return nil, nil (not an error)
|
||||
return nil, nil
|
||||
},
|
||||
findOrCreateDocFunc: func(ctx context.Context, ref string) (*models.Document, bool, error) {
|
||||
@@ -457,11 +476,11 @@ func TestHandler_HandleFindOrCreateDocument_CreateNew(t *testing.T) {
|
||||
|
||||
handler := &Handler{
|
||||
documentService: mockDocService,
|
||||
authorizer: &mockDocumentAuthorizer{},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/find-or-create?ref=https://example.com/new-doc.pdf", nil)
|
||||
|
||||
// Add authenticated user to context
|
||||
ctx := addUserToContext(req.Context(), testUser)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
@@ -486,17 +505,16 @@ func TestHandler_HandleFindOrCreateDocument_UnauthenticatedCreate(t *testing.T)
|
||||
|
||||
mockDocService := &mockDocumentService{
|
||||
findByReferenceFunc: func(ctx context.Context, ref string, refType string) (*models.Document, error) {
|
||||
// Document not found - return nil, nil (not an error)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := &Handler{
|
||||
documentService: mockDocService,
|
||||
authorizer: &mockDocumentAuthorizer{canCreateFunc: func(ctx context.Context, user *models.User) bool { return user != nil }},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/find-or-create?ref=https://example.com/new-doc.pdf", nil)
|
||||
// No user in context
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.HandleFindOrCreateDocument(rec, req)
|
||||
@@ -698,14 +716,13 @@ func TestHandler_HandleCreateDocument_Concurrent(t *testing.T) {
|
||||
// ADMIN-ONLY DOCUMENT CREATION TESTS
|
||||
// ============================================================================
|
||||
|
||||
func TestHandler_HandleCreateDocument_AdminOnlyEnabled_AdminUser(t *testing.T) {
|
||||
func TestHandler_HandleCreateDocument_Authorized(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := &Handler{
|
||||
signatureService: &services.SignatureService{},
|
||||
documentService: &mockDocumentService{},
|
||||
adminEmails: []string{"admin@example.com"},
|
||||
onlyAdminCanCreate: true,
|
||||
signatureService: &mockSignatureService{},
|
||||
documentService: &mockDocumentService{},
|
||||
authorizer: &mockDocumentAuthorizer{canCreateFunc: func(ctx context.Context, user *models.User) bool { return true }},
|
||||
}
|
||||
|
||||
reqBody := CreateDocumentRequest{
|
||||
@@ -718,7 +735,6 @@ func TestHandler_HandleCreateDocument_AdminOnlyEnabled_AdminUser(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add admin user to context
|
||||
adminUser := &models.User{
|
||||
Sub: "oauth2|admin",
|
||||
Email: "admin@example.com",
|
||||
@@ -734,14 +750,13 @@ func TestHandler_HandleCreateDocument_AdminOnlyEnabled_AdminUser(t *testing.T) {
|
||||
assert.Contains(t, rec.Body.String(), "test-doc-123")
|
||||
}
|
||||
|
||||
func TestHandler_HandleCreateDocument_AdminOnlyEnabled_NonAdminUser(t *testing.T) {
|
||||
func TestHandler_HandleCreateDocument_NotAuthorized(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := &Handler{
|
||||
signatureService: &services.SignatureService{},
|
||||
documentService: &mockDocumentService{},
|
||||
adminEmails: []string{"admin@example.com"},
|
||||
onlyAdminCanCreate: true,
|
||||
signatureService: &mockSignatureService{},
|
||||
documentService: &mockDocumentService{},
|
||||
authorizer: &mockDocumentAuthorizer{canCreateFunc: func(ctx context.Context, user *models.User) bool { return false }},
|
||||
}
|
||||
|
||||
reqBody := CreateDocumentRequest{
|
||||
@@ -754,7 +769,6 @@ func TestHandler_HandleCreateDocument_AdminOnlyEnabled_NonAdminUser(t *testing.T
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add non-admin user to context
|
||||
regularUser := &models.User{
|
||||
Sub: "oauth2|user",
|
||||
Email: "user@example.com",
|
||||
@@ -767,17 +781,16 @@ func TestHandler_HandleCreateDocument_AdminOnlyEnabled_NonAdminUser(t *testing.T
|
||||
handler.HandleCreateDocument(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, rec.Code)
|
||||
assert.Contains(t, rec.Body.String(), "Only administrators can create documents")
|
||||
assert.Contains(t, rec.Body.String(), "Not authorized")
|
||||
}
|
||||
|
||||
func TestHandler_HandleCreateDocument_AdminOnlyEnabled_Unauthenticated(t *testing.T) {
|
||||
func TestHandler_HandleCreateDocument_Unauthenticated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := &Handler{
|
||||
signatureService: &services.SignatureService{},
|
||||
documentService: &mockDocumentService{},
|
||||
adminEmails: []string{"admin@example.com"},
|
||||
onlyAdminCanCreate: true,
|
||||
signatureService: &mockSignatureService{},
|
||||
documentService: &mockDocumentService{},
|
||||
authorizer: &mockDocumentAuthorizer{canCreateFunc: func(ctx context.Context, user *models.User) bool { return user != nil }},
|
||||
}
|
||||
|
||||
reqBody := CreateDocumentRequest{
|
||||
@@ -789,7 +802,6 @@ func TestHandler_HandleCreateDocument_AdminOnlyEnabled_Unauthenticated(t *testin
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/documents", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
// No user in context (unauthenticated)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.HandleCreateDocument(rec, req)
|
||||
@@ -798,14 +810,13 @@ func TestHandler_HandleCreateDocument_AdminOnlyEnabled_Unauthenticated(t *testin
|
||||
assert.Contains(t, rec.Body.String(), "Authentication required")
|
||||
}
|
||||
|
||||
func TestHandler_HandleCreateDocument_AdminOnlyDisabled_AnyUser(t *testing.T) {
|
||||
func TestHandler_HandleCreateDocument_PublicCreation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := &Handler{
|
||||
signatureService: &services.SignatureService{},
|
||||
documentService: &mockDocumentService{},
|
||||
adminEmails: []string{"admin@example.com"},
|
||||
onlyAdminCanCreate: false, // Disabled
|
||||
signatureService: &mockSignatureService{},
|
||||
documentService: &mockDocumentService{},
|
||||
authorizer: &mockDocumentAuthorizer{canCreateFunc: func(ctx context.Context, user *models.User) bool { return true }},
|
||||
}
|
||||
|
||||
reqBody := CreateDocumentRequest{
|
||||
|
||||
@@ -16,35 +16,27 @@ import (
|
||||
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/database"
|
||||
apiAdmin "github.com/btouchard/ackify-ce/backend/internal/presentation/api/admin"
|
||||
apiAuth "github.com/btouchard/ackify-ce/backend/internal/presentation/api/auth"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/documents"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/health"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/shared"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/signatures"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/users"
|
||||
"github.com/btouchard/ackify-ce/backend/pkg/coreapp"
|
||||
)
|
||||
|
||||
// RouterConfig holds configuration for the API router
|
||||
type RouterConfig struct {
|
||||
AuthService *auth.OauthService
|
||||
MagicLinkService *services.MagicLinkService
|
||||
SignatureService *services.SignatureService
|
||||
DocumentService *services.DocumentService
|
||||
DocumentRepository *database.DocumentRepository
|
||||
ExpectedSignerRepository *database.ExpectedSignerRepository
|
||||
ReminderService *services.ReminderAsyncService // Now using async service
|
||||
WebhookRepository *database.WebhookRepository
|
||||
WebhookDeliveryRepository *database.WebhookDeliveryRepository
|
||||
WebhookPublisher *services.WebhookPublisher
|
||||
CoreDeps coreapp.CoreDeps
|
||||
BaseURL string
|
||||
AdminEmails []string
|
||||
AutoLogin bool
|
||||
OAuthEnabled bool
|
||||
MagicLinkEnabled bool
|
||||
OnlyAdminCanCreate bool
|
||||
AuthRateLimit int // Global auth rate limit (requests per minute), default: 5
|
||||
DocumentRateLimit int // Document creation rate limit (requests per minute), default: 10
|
||||
GeneralRateLimit int // General API rate limit (requests per minute), default: 100
|
||||
ImportMaxSigners int // Maximum signers per CSV import, default: 500
|
||||
AuthRateLimit int
|
||||
DocumentRateLimit int
|
||||
GeneralRateLimit int
|
||||
}
|
||||
|
||||
// NewRouter creates and configures the API v1 router
|
||||
@@ -82,20 +74,14 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||
r.Use(apiMiddleware.CORS)
|
||||
r.Use(generalRateLimit.Middleware)
|
||||
|
||||
// Initialize handlers
|
||||
// Initialize coreapp handler groups (documents/signatures)
|
||||
coreGroups := coreapp.NewHandlerGroups(cfg.CoreDeps)
|
||||
|
||||
// Initialize handlers for non-coreapp routes
|
||||
healthHandler := health.NewHandler()
|
||||
authHandler := apiAuth.NewHandler(cfg.AuthService, cfg.MagicLinkService, apiMiddleware, cfg.BaseURL, cfg.OAuthEnabled, cfg.MagicLinkEnabled)
|
||||
usersHandler := users.NewHandler(cfg.AdminEmails)
|
||||
documentsHandler := documents.NewHandler(
|
||||
cfg.SignatureService,
|
||||
cfg.DocumentService,
|
||||
cfg.DocumentRepository,
|
||||
cfg.ExpectedSignerRepository,
|
||||
cfg.WebhookPublisher,
|
||||
cfg.AdminEmails,
|
||||
cfg.OnlyAdminCanCreate,
|
||||
)
|
||||
signaturesHandler := signatures.NewHandler(cfg.SignatureService, cfg.ExpectedSignerRepository, cfg.WebhookPublisher)
|
||||
webhooksHandler := apiAdmin.NewWebhooksHandler(cfg.WebhookRepository, cfg.WebhookDeliveryRepository)
|
||||
|
||||
// Public routes
|
||||
r.Group(func(r chi.Router) {
|
||||
@@ -139,27 +125,18 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||
})
|
||||
})
|
||||
|
||||
// Public document endpoints
|
||||
r.Route("/documents", func(r chi.Router) {
|
||||
// Document creation (with CSRF and stricter rate limiting)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(apiMiddleware.CSRFProtect)
|
||||
r.Use(documentRateLimit.Middleware)
|
||||
r.Post("/", documentsHandler.HandleCreateDocument)
|
||||
})
|
||||
// Public document endpoints (from coreapp)
|
||||
coreGroups.RegisterPublic(r)
|
||||
})
|
||||
|
||||
// Read-only document endpoints
|
||||
r.Get("/", documentsHandler.HandleListDocuments)
|
||||
r.Get("/{docId}", documentsHandler.HandleGetDocument)
|
||||
r.Get("/{docId}/signatures", documentsHandler.HandleGetDocumentSignatures)
|
||||
r.Get("/{docId}/expected-signers", documentsHandler.HandleGetExpectedSigners)
|
||||
|
||||
// Find or create document by reference (public for embed support, but with optional auth)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(apiMiddleware.OptionalAuth)
|
||||
r.Get("/find-or-create", documentsHandler.HandleFindOrCreateDocument)
|
||||
})
|
||||
})
|
||||
// User document routes with special handling
|
||||
// - Document creation requires CSRF + rate limiting
|
||||
// - find-or-create requires optional auth
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(apiMiddleware.CSRFProtect)
|
||||
r.Use(documentRateLimit.Middleware)
|
||||
r.Use(apiMiddleware.OptionalAuth)
|
||||
coreGroups.RegisterUser(r)
|
||||
})
|
||||
|
||||
// Authenticated routes
|
||||
@@ -167,19 +144,10 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||
r.Use(apiMiddleware.RequireAuth)
|
||||
r.Use(apiMiddleware.CSRFProtect)
|
||||
|
||||
// User endpoints
|
||||
// User endpoints (non-coreapp)
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Get("/me", usersHandler.HandleGetCurrentUser)
|
||||
})
|
||||
|
||||
// Signature endpoints
|
||||
r.Route("/signatures", func(r chi.Router) {
|
||||
r.Get("/", signaturesHandler.HandleGetUserSignatures)
|
||||
r.Post("/", signaturesHandler.HandleCreateSignature)
|
||||
})
|
||||
|
||||
// Document signature status (authenticated)
|
||||
r.Get("/documents/{docId}/signatures/status", signaturesHandler.HandleGetSignatureStatus)
|
||||
})
|
||||
|
||||
// Admin routes
|
||||
@@ -187,53 +155,18 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||
r.Use(apiMiddleware.RequireAdmin)
|
||||
r.Use(apiMiddleware.CSRFProtect)
|
||||
|
||||
// Configure import max signers with default
|
||||
importMaxSigners := cfg.ImportMaxSigners
|
||||
if importMaxSigners == 0 {
|
||||
importMaxSigners = 500 // Default: 500 signers per import
|
||||
}
|
||||
// Admin document routes (from coreapp)
|
||||
coreGroups.RegisterAdmin(r)
|
||||
|
||||
// Initialize admin handler
|
||||
adminHandler := apiAdmin.NewHandler(cfg.DocumentRepository, cfg.ExpectedSignerRepository, cfg.ReminderService, cfg.SignatureService, cfg.BaseURL, importMaxSigners)
|
||||
webhooksHandler := apiAdmin.NewWebhooksHandler(cfg.WebhookRepository, cfg.WebhookDeliveryRepository)
|
||||
|
||||
r.Route("/admin", func(r chi.Router) {
|
||||
// Document management
|
||||
r.Route("/documents", func(r chi.Router) {
|
||||
r.Get("/", adminHandler.HandleListDocuments)
|
||||
r.Get("/{docId}", adminHandler.HandleGetDocument)
|
||||
r.Get("/{docId}/signers", adminHandler.HandleGetDocumentWithSigners)
|
||||
r.Get("/{docId}/status", adminHandler.HandleGetDocumentStatus)
|
||||
|
||||
// Document metadata
|
||||
r.Put("/{docId}/metadata", adminHandler.HandleUpdateDocumentMetadata)
|
||||
|
||||
// Document deletion
|
||||
r.Delete("/{docId}", adminHandler.HandleDeleteDocument)
|
||||
|
||||
// Expected signers management
|
||||
r.Post("/{docId}/signers", adminHandler.HandleAddExpectedSigner)
|
||||
r.Delete("/{docId}/signers/{email}", adminHandler.HandleRemoveExpectedSigner)
|
||||
|
||||
// CSV import for expected signers
|
||||
r.Post("/{docId}/signers/preview-csv", adminHandler.HandlePreviewCSV)
|
||||
r.Post("/{docId}/signers/import", adminHandler.HandleImportSigners)
|
||||
|
||||
// Reminder management
|
||||
r.Post("/{docId}/reminders", adminHandler.HandleSendReminders)
|
||||
r.Get("/{docId}/reminders", adminHandler.HandleGetReminderHistory)
|
||||
})
|
||||
|
||||
// Webhooks management
|
||||
r.Route("/webhooks", func(r chi.Router) {
|
||||
r.Get("/", webhooksHandler.HandleListWebhooks)
|
||||
r.Post("/", webhooksHandler.HandleCreateWebhook)
|
||||
r.Get("/{id}", webhooksHandler.HandleGetWebhook)
|
||||
r.Put("/{id}", webhooksHandler.HandleUpdateWebhook)
|
||||
r.Patch("/{id}/{action}", webhooksHandler.HandleToggleWebhook) // action: enable|disable
|
||||
r.Delete("/{id}", webhooksHandler.HandleDeleteWebhook)
|
||||
r.Get("/{id}/deliveries", webhooksHandler.HandleListDeliveries)
|
||||
})
|
||||
// Webhooks management (non-coreapp)
|
||||
r.Route("/admin/webhooks", func(r chi.Router) {
|
||||
r.Get("/", webhooksHandler.HandleListWebhooks)
|
||||
r.Post("/", webhooksHandler.HandleCreateWebhook)
|
||||
r.Get("/{id}", webhooksHandler.HandleGetWebhook)
|
||||
r.Put("/{id}", webhooksHandler.HandleUpdateWebhook)
|
||||
r.Patch("/{id}/{action}", webhooksHandler.HandleToggleWebhook) // action: enable|disable
|
||||
r.Delete("/{id}", webhooksHandler.HandleDeleteWebhook)
|
||||
r.Get("/{id}/deliveries", webhooksHandler.HandleListDeliveries)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
package coreapp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/btouchard/ackify-ce/backend/internal/application/services"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/domain/models"
|
||||
)
|
||||
|
||||
type DocumentService interface {
|
||||
// Creation
|
||||
CreateDocument(ctx context.Context, req services.CreateDocumentRequest) (*models.Document, error)
|
||||
FindOrCreateDocument(ctx context.Context, ref string) (*models.Document, bool, error)
|
||||
FindByReference(ctx context.Context, ref string, refType string) (*models.Document, error)
|
||||
// Read
|
||||
List(ctx context.Context, limit, offset int) ([]*models.Document, error)
|
||||
Search(ctx context.Context, query string, limit, offset int) ([]*models.Document, error)
|
||||
Count(ctx context.Context, searchQuery string) (int, error)
|
||||
GetByDocID(ctx context.Context, docID string) (*models.Document, error)
|
||||
// Write
|
||||
CreateOrUpdate(ctx context.Context, docID string, input models.DocumentInput, createdBy string) (*models.Document, error)
|
||||
Delete(ctx context.Context, docID string) error
|
||||
}
|
||||
|
||||
type SignatureService interface {
|
||||
CreateSignature(ctx context.Context, request *models.SignatureRequest) error
|
||||
GetSignatureStatus(ctx context.Context, docID string, user *models.User) (*models.SignatureStatus, error)
|
||||
GetSignatureByDocAndUser(ctx context.Context, docID string, user *models.User) (*models.Signature, error)
|
||||
GetDocumentSignatures(ctx context.Context, docID string) ([]*models.Signature, error)
|
||||
GetUserSignatures(ctx context.Context, user *models.User) ([]*models.Signature, error)
|
||||
}
|
||||
|
||||
type ExpectedSignerService interface {
|
||||
ListByDocID(ctx context.Context, docID string) ([]*models.ExpectedSigner, error)
|
||||
ListWithStatusByDocID(ctx context.Context, docID string) ([]*models.ExpectedSignerWithStatus, error)
|
||||
AddExpected(ctx context.Context, docID string, contacts []models.ContactInfo, addedBy string) error
|
||||
Remove(ctx context.Context, docID, email string) error
|
||||
GetStats(ctx context.Context, docID string) (*models.DocCompletionStats, error)
|
||||
}
|
||||
|
||||
type ReminderService interface {
|
||||
SendReminders(ctx context.Context, docID, sentBy string, specificEmails []string, docURL string, locale string) (*models.ReminderSendResult, error)
|
||||
GetReminderHistory(ctx context.Context, docID string) ([]*models.ReminderLog, error)
|
||||
GetReminderStats(ctx context.Context, docID string) (*models.ReminderStats, error)
|
||||
}
|
||||
|
||||
type WebhookPublisher interface {
|
||||
Publish(ctx context.Context, eventType string, payload map[string]interface{}) error
|
||||
}
|
||||
|
||||
type DocumentAuthorizer interface {
|
||||
CanCreateDocument(ctx context.Context, user *models.User) bool
|
||||
}
|
||||
|
||||
type CoreDeps struct {
|
||||
Documents DocumentService
|
||||
DocumentAuthorizer DocumentAuthorizer
|
||||
Signatures SignatureService
|
||||
ExpectedSigners ExpectedSignerService
|
||||
Reminders ReminderService
|
||||
WebhookPublisher WebhookPublisher
|
||||
BaseURL string
|
||||
ImportMaxSigners int
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
package coreapp
|
||||
|
||||
import (
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/admin"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/documents"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api/signatures"
|
||||
)
|
||||
|
||||
type CoreHandlers struct {
|
||||
Documents *documents.Handler
|
||||
Signatures *signatures.Handler
|
||||
Admin *admin.Handler
|
||||
}
|
||||
|
||||
func NewCoreHandlers(deps CoreDeps) *CoreHandlers {
|
||||
return &CoreHandlers{
|
||||
Documents: documents.NewHandler(
|
||||
deps.Signatures,
|
||||
deps.Documents,
|
||||
deps.Documents, // DocumentService implements documentRepository interface
|
||||
deps.ExpectedSigners, // ExpectedSignerService implements expectedSignerRepository interface
|
||||
deps.WebhookPublisher,
|
||||
deps.DocumentAuthorizer,
|
||||
),
|
||||
Signatures: signatures.NewHandler(
|
||||
deps.Signatures,
|
||||
deps.ExpectedSigners, // ExpectedSignerService implements expectedSignerStatsRepo interface
|
||||
deps.WebhookPublisher,
|
||||
),
|
||||
Admin: admin.NewHandler(
|
||||
deps.Documents, // DocumentService implements documentRepository interface
|
||||
deps.ExpectedSigners, // ExpectedSignerService implements expectedSignerRepository interface
|
||||
deps.Reminders,
|
||||
deps.Signatures,
|
||||
deps.BaseURL,
|
||||
deps.ImportMaxSigners,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
package coreapp
|
||||
|
||||
import "github.com/go-chi/chi/v5"
|
||||
|
||||
type RouteRegistrar func(r chi.Router)
|
||||
|
||||
type HandlerGroups struct {
|
||||
RegisterPublic RouteRegistrar
|
||||
RegisterUser RouteRegistrar
|
||||
RegisterAdmin RouteRegistrar
|
||||
}
|
||||
|
||||
func NewHandlerGroups(deps CoreDeps) HandlerGroups {
|
||||
h := NewCoreHandlers(deps)
|
||||
|
||||
return HandlerGroups{
|
||||
RegisterPublic: func(r chi.Router) {
|
||||
r.Route("/documents", func(r chi.Router) {
|
||||
r.Get("/", h.Documents.HandleListDocuments)
|
||||
r.Get("/{docId}", h.Documents.HandleGetDocument)
|
||||
r.Get("/{docId}/signatures", h.Documents.HandleGetDocumentSignatures)
|
||||
r.Get("/{docId}/expected-signers", h.Documents.HandleGetExpectedSigners)
|
||||
})
|
||||
},
|
||||
|
||||
RegisterUser: func(r chi.Router) {
|
||||
r.Post("/documents", h.Documents.HandleCreateDocument)
|
||||
r.Get("/documents/find-or-create", h.Documents.HandleFindOrCreateDocument)
|
||||
r.Get("/signatures", h.Signatures.HandleGetUserSignatures)
|
||||
r.Post("/signatures", h.Signatures.HandleCreateSignature)
|
||||
r.Get("/documents/{docId}/signatures/status", h.Signatures.HandleGetSignatureStatus)
|
||||
},
|
||||
|
||||
RegisterAdmin: func(r chi.Router) {
|
||||
r.Route("/admin/documents", func(r chi.Router) {
|
||||
r.Get("/", h.Admin.HandleListDocuments)
|
||||
r.Get("/{docId}", h.Admin.HandleGetDocument)
|
||||
r.Get("/{docId}/signers", h.Admin.HandleGetDocumentWithSigners)
|
||||
r.Get("/{docId}/status", h.Admin.HandleGetDocumentStatus)
|
||||
r.Put("/{docId}/metadata", h.Admin.HandleUpdateDocumentMetadata)
|
||||
r.Delete("/{docId}", h.Admin.HandleDeleteDocument)
|
||||
r.Post("/{docId}/signers", h.Admin.HandleAddExpectedSigner)
|
||||
r.Delete("/{docId}/signers/{email}", h.Admin.HandleRemoveExpectedSigner)
|
||||
r.Post("/{docId}/signers/preview-csv", h.Admin.HandlePreviewCSV)
|
||||
r.Post("/{docId}/signers/import", h.Admin.HandleImportSigners)
|
||||
r.Post("/{docId}/reminders", h.Admin.HandleSendReminders)
|
||||
r.Get("/{docId}/reminders", h.Admin.HandleGetReminderHistory)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/btouchard/ackify-ce/backend/internal/application/services"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/auth"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/authorization"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/config"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/database"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/email"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/workers"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/api"
|
||||
"github.com/btouchard/ackify-ce/backend/internal/presentation/handlers"
|
||||
"github.com/btouchard/ackify-ce/backend/pkg/coreapp"
|
||||
"github.com/btouchard/ackify-ce/backend/pkg/crypto"
|
||||
"github.com/btouchard/ackify-ce/backend/pkg/logger"
|
||||
)
|
||||
@@ -104,6 +106,7 @@ func NewServer(ctx context.Context, cfg *config.Config, frontend embed.FS, versi
|
||||
signatureService := services.NewSignatureService(signatureRepo, documentRepo, signer)
|
||||
signatureService.SetChecksumConfig(&cfg.Checksum)
|
||||
documentService := services.NewDocumentService(documentRepo, &cfg.Checksum)
|
||||
expectedSignerService := services.NewExpectedSignerService(expectedSignerRepo)
|
||||
|
||||
// Initialize email worker for async processing
|
||||
var emailWorker *email.Worker
|
||||
@@ -177,27 +180,39 @@ func NewServer(ctx context.Context, cfg *config.Config, frontend embed.FS, versi
|
||||
webhookPublisher,
|
||||
))
|
||||
|
||||
// Build CoreDeps for coreapp handlers (documents/signatures)
|
||||
importMaxSigners := cfg.App.ImportMaxSigners
|
||||
if importMaxSigners == 0 {
|
||||
importMaxSigners = 500 // Default: 500 signers per import
|
||||
}
|
||||
|
||||
documentAuthorizer := authorization.NewCEDocumentAuthorizer(cfg.App.AdminEmails, cfg.App.OnlyAdminCanCreate)
|
||||
|
||||
coreDeps := coreapp.CoreDeps{
|
||||
Documents: documentService,
|
||||
Signatures: signatureService,
|
||||
ExpectedSigners: expectedSignerService,
|
||||
Reminders: reminderService,
|
||||
WebhookPublisher: webhookPublisher,
|
||||
DocumentAuthorizer: documentAuthorizer,
|
||||
BaseURL: cfg.App.BaseURL,
|
||||
ImportMaxSigners: importMaxSigners,
|
||||
}
|
||||
|
||||
apiConfig := api.RouterConfig{
|
||||
AuthService: authService,
|
||||
MagicLinkService: magicLinkService,
|
||||
SignatureService: signatureService,
|
||||
DocumentService: documentService,
|
||||
DocumentRepository: documentRepo,
|
||||
ExpectedSignerRepository: expectedSignerRepo,
|
||||
ReminderService: reminderService,
|
||||
WebhookRepository: webhookRepo,
|
||||
WebhookDeliveryRepository: webhookDeliveryRepo,
|
||||
WebhookPublisher: webhookPublisher,
|
||||
CoreDeps: coreDeps,
|
||||
BaseURL: cfg.App.BaseURL,
|
||||
AdminEmails: cfg.App.AdminEmails,
|
||||
AutoLogin: cfg.OAuth.AutoLogin,
|
||||
OAuthEnabled: cfg.Auth.OAuthEnabled,
|
||||
MagicLinkEnabled: cfg.Auth.MagicLinkEnabled,
|
||||
OnlyAdminCanCreate: cfg.App.OnlyAdminCanCreate,
|
||||
AuthRateLimit: cfg.App.AuthRateLimit,
|
||||
DocumentRateLimit: cfg.App.DocumentRateLimit,
|
||||
GeneralRateLimit: cfg.App.GeneralRateLimit,
|
||||
ImportMaxSigners: cfg.App.ImportMaxSigners,
|
||||
}
|
||||
apiRouter := api.NewRouter(apiConfig)
|
||||
router.Mount("/api/v1", apiRouter)
|
||||
|
||||
Reference in New Issue
Block a user