You've already forked openaccounting-server
forked from cybercinch/openaccounting-server
feat: implement attachment business logic layer
- Add AttachmentInterface to main model interface - Implement attachment CRUD operations with permission checking - Add GetTransaction method for secure attachment access validation - Add accountsContainReadAccess for permission verification - Ensure users can only access attachments for authorized transactions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -408,6 +408,15 @@ func (model *Model) accountsContainWriteAccess(accounts []*types.Account, accoun
|
||||
return false
|
||||
}
|
||||
|
||||
func (model *Model) accountsContainReadAccess(accounts []*types.Account, accountId string) bool {
|
||||
for _, account := range accounts {
|
||||
if account.Id == accountId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (model *Model) getAccountFromList(accounts []*types.Account, accountId string) *types.Account {
|
||||
for _, account := range accounts {
|
||||
if account.Id == accountId {
|
||||
|
||||
163
core/model/attachment.go
Normal file
163
core/model/attachment.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/openaccounting/oa-server/core/model/types"
|
||||
)
|
||||
|
||||
type AttachmentInterface interface {
|
||||
CreateAttachment(*types.Attachment) (*types.Attachment, error)
|
||||
GetAttachmentsByTransaction(string, string, string) ([]*types.Attachment, error)
|
||||
GetAttachment(string, string, string, string) (*types.Attachment, error)
|
||||
DeleteAttachment(string, string, string, string) error
|
||||
}
|
||||
|
||||
func (model *Model) CreateAttachment(attachment *types.Attachment) (*types.Attachment, error) {
|
||||
if attachment.Id == "" {
|
||||
return nil, errors.New("attachment ID required")
|
||||
}
|
||||
|
||||
if attachment.TransactionId == "" {
|
||||
return nil, errors.New("transaction ID required")
|
||||
}
|
||||
|
||||
if attachment.OrgId == "" {
|
||||
return nil, errors.New("organization ID required")
|
||||
}
|
||||
|
||||
if attachment.UserId == "" {
|
||||
return nil, errors.New("user ID required")
|
||||
}
|
||||
|
||||
if attachment.FileName == "" {
|
||||
return nil, errors.New("file name required")
|
||||
}
|
||||
|
||||
if attachment.FilePath == "" {
|
||||
return nil, errors.New("file path required")
|
||||
}
|
||||
|
||||
// Set upload timestamp
|
||||
attachment.Uploaded = time.Now()
|
||||
attachment.Deleted = false
|
||||
|
||||
// Save to database
|
||||
err := model.db.InsertAttachment(attachment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
func (model *Model) GetAttachmentsByTransaction(transactionId, orgId, userId string) ([]*types.Attachment, error) {
|
||||
if transactionId == "" {
|
||||
return nil, errors.New("transaction ID required")
|
||||
}
|
||||
|
||||
if orgId == "" {
|
||||
return nil, errors.New("organization ID required")
|
||||
}
|
||||
|
||||
if userId == "" {
|
||||
return nil, errors.New("user ID required")
|
||||
}
|
||||
|
||||
// First verify the user has access to the transaction
|
||||
tx, err := model.GetTransaction(transactionId, orgId, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tx == nil {
|
||||
return nil, errors.New("transaction not found or access denied")
|
||||
}
|
||||
|
||||
// Get attachments for the transaction
|
||||
attachments, err := model.db.GetAttachmentsByTransaction(transactionId, orgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return attachments, nil
|
||||
}
|
||||
|
||||
func (model *Model) GetAttachment(attachmentId, transactionId, orgId, userId string) (*types.Attachment, error) {
|
||||
if attachmentId == "" {
|
||||
return nil, errors.New("attachment ID required")
|
||||
}
|
||||
|
||||
if transactionId == "" {
|
||||
return nil, errors.New("transaction ID required")
|
||||
}
|
||||
|
||||
if orgId == "" {
|
||||
return nil, errors.New("organization ID required")
|
||||
}
|
||||
|
||||
if userId == "" {
|
||||
return nil, errors.New("user ID required")
|
||||
}
|
||||
|
||||
// First verify the user has access to the transaction
|
||||
tx, err := model.GetTransaction(transactionId, orgId, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tx == nil {
|
||||
return nil, errors.New("transaction not found or access denied")
|
||||
}
|
||||
|
||||
// Get the attachment
|
||||
attachment, err := model.db.GetAttachment(attachmentId, transactionId, orgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
func (model *Model) DeleteAttachment(attachmentId, transactionId, orgId, userId string) error {
|
||||
if attachmentId == "" {
|
||||
return errors.New("attachment ID required")
|
||||
}
|
||||
|
||||
if transactionId == "" {
|
||||
return errors.New("transaction ID required")
|
||||
}
|
||||
|
||||
if orgId == "" {
|
||||
return errors.New("organization ID required")
|
||||
}
|
||||
|
||||
if userId == "" {
|
||||
return errors.New("user ID required")
|
||||
}
|
||||
|
||||
// First verify the user has access to the transaction
|
||||
tx, err := model.GetTransaction(transactionId, orgId, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tx == nil {
|
||||
return errors.New("transaction not found or access denied")
|
||||
}
|
||||
|
||||
// Verify the attachment exists and belongs to the transaction
|
||||
attachment, err := model.db.GetAttachment(attachmentId, transactionId, orgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if attachment == nil {
|
||||
return errors.New("attachment not found")
|
||||
}
|
||||
|
||||
// Soft delete the attachment
|
||||
err = model.db.DeleteAttachment(attachmentId, transactionId, orgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -20,11 +20,13 @@ type Interface interface {
|
||||
OrgInterface
|
||||
AccountInterface
|
||||
TransactionInterface
|
||||
AttachmentInterface
|
||||
PriceInterface
|
||||
SessionInterface
|
||||
ApiKeyInterface
|
||||
SystemHealthInteface
|
||||
BudgetInterface
|
||||
GetTransaction(string, string, string) (*types.Transaction, error)
|
||||
}
|
||||
|
||||
func NewModel(db db.Datastore, bcrypt util.Bcrypt, config types.Config) *Model {
|
||||
|
||||
@@ -169,6 +169,31 @@ func (model *Model) getTransactionById(id string) (*types.Transaction, error) {
|
||||
return model.db.GetTransactionById(id)
|
||||
}
|
||||
|
||||
func (model *Model) GetTransaction(transactionId, orgId, userId string) (*types.Transaction, error) {
|
||||
transaction, err := model.getTransactionById(transactionId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if transaction == nil || transaction.OrgId != orgId {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check if user has access to all accounts in the transaction
|
||||
userAccounts, err := model.GetAccounts(orgId, userId, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, split := range transaction.Splits {
|
||||
if !model.accountsContainReadAccess(userAccounts, split.AccountId) {
|
||||
return nil, fmt.Errorf("user does not have permission to access account %s", split.AccountId)
|
||||
}
|
||||
}
|
||||
|
||||
return transaction, nil
|
||||
}
|
||||
|
||||
func (model *Model) checkSplits(transaction *types.Transaction) (err error) {
|
||||
if len(transaction.Splits) < 2 {
|
||||
return errors.New("at least 2 splits are required")
|
||||
|
||||
Reference in New Issue
Block a user