You've already forked openaccounting-server
forked from cybercinch/openaccounting-server
feat: implement secure file upload system with JWT authentication
- Add JWT-based secure file access for local storage with 1-hour expiry - Implement GORM repository methods for attachment CRUD operations - Add secure file serving endpoint with token validation - Update storage interface to support user context in URL generation - Add comprehensive security features including path traversal protection - Update documentation with security model and configuration examples - Add utility functions for hex/byte conversion and UUID validation - Configure secure file permissions (0600) for uploaded files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -372,4 +372,167 @@ func (r *GormRepository) Escape(sql string) string {
|
||||
// GORM handles SQL injection protection automatically
|
||||
// This method is kept for interface compatibility
|
||||
return sql
|
||||
}
|
||||
|
||||
// Attachment repository methods
|
||||
func (r *GormRepository) InsertAttachment(attachment *types.Attachment) error {
|
||||
// Convert UUID strings to bytes (remove dashes if present)
|
||||
idBytes, err := stringToIDBytes(attachment.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transactionIdBytes, err := stringToIDBytes(attachment.TransactionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
orgIdBytes, err := stringToIDBytes(attachment.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIdBytes, err := stringToIDBytes(attachment.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert types.Attachment to models.Attachment
|
||||
gormAttachment := &models.Attachment{
|
||||
ID: idBytes,
|
||||
TransactionID: transactionIdBytes,
|
||||
OrgID: orgIdBytes,
|
||||
UserID: userIdBytes,
|
||||
FileName: attachment.FileName,
|
||||
OriginalName: attachment.OriginalName,
|
||||
ContentType: attachment.ContentType,
|
||||
FileSize: attachment.FileSize,
|
||||
FilePath: attachment.FilePath,
|
||||
Description: attachment.Description,
|
||||
Uploaded: attachment.Uploaded,
|
||||
Deleted: attachment.Deleted,
|
||||
}
|
||||
|
||||
result := r.db.Create(gormAttachment)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
func (r *GormRepository) GetAttachmentsByTransaction(transactionId, orgId, userId string) ([]*types.Attachment, error) {
|
||||
transactionIdBytes, err := stringToIDBytes(transactionId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgIdBytes, err := stringToIDBytes(orgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var gormAttachments []models.Attachment
|
||||
result := r.db.Where("transactionId = ? AND orgId = ? AND deleted = ?",
|
||||
transactionIdBytes, orgIdBytes, false).Find(&gormAttachments)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
attachments := make([]*types.Attachment, len(gormAttachments))
|
||||
for i, gormAttachment := range gormAttachments {
|
||||
attachments[i] = convertGormToTypesAttachment(&gormAttachment)
|
||||
}
|
||||
|
||||
return attachments, nil
|
||||
}
|
||||
|
||||
func (r *GormRepository) GetAttachment(attachmentId, transactionId, orgId, userId string) (*types.Attachment, error) {
|
||||
attachmentIdBytes, err := stringToIDBytes(attachmentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var gormAttachment models.Attachment
|
||||
query := r.db.Where("id = ? AND deleted = ?", attachmentIdBytes, false)
|
||||
|
||||
// Add additional filters if provided
|
||||
if transactionId != "" {
|
||||
transactionIdBytes, err := stringToIDBytes(transactionId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = query.Where("transactionId = ?", transactionIdBytes)
|
||||
}
|
||||
|
||||
if orgId != "" {
|
||||
orgIdBytes, err := stringToIDBytes(orgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = query.Where("orgId = ?", orgIdBytes)
|
||||
}
|
||||
|
||||
result := query.First(&gormAttachment)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return convertGormToTypesAttachment(&gormAttachment), nil
|
||||
}
|
||||
|
||||
func (r *GormRepository) DeleteAttachment(attachmentId, transactionId, orgId, userId string) error {
|
||||
attachmentIdBytes, err := stringToIDBytes(attachmentId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := r.db.Model(&models.Attachment{}).Where("id = ?", attachmentIdBytes)
|
||||
|
||||
// Add additional filters if provided
|
||||
if transactionId != "" {
|
||||
transactionIdBytes, err := stringToIDBytes(transactionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query = query.Where("transactionId = ?", transactionIdBytes)
|
||||
}
|
||||
|
||||
if orgId != "" {
|
||||
orgIdBytes, err := stringToIDBytes(orgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query = query.Where("orgId = ?", orgIdBytes)
|
||||
}
|
||||
|
||||
// Soft delete by setting deleted = true
|
||||
result := query.Update("deleted", true)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// Helper function to convert UUID string (with or without dashes) to bytes
|
||||
func stringToIDBytes(id string) ([]byte, error) {
|
||||
// Remove dashes if present
|
||||
cleanId := strings.ReplaceAll(id, "-", "")
|
||||
return util.HexToBytes(cleanId)
|
||||
}
|
||||
|
||||
// Helper function to convert bytes to UUID string (without dashes, for compatibility)
|
||||
func idBytesToString(bytes []byte) string {
|
||||
return util.BytesToHex(bytes)
|
||||
}
|
||||
|
||||
// Helper function to convert GORM attachment to types attachment
|
||||
func convertGormToTypesAttachment(gormAttachment *models.Attachment) *types.Attachment {
|
||||
return &types.Attachment{
|
||||
Id: idBytesToString(gormAttachment.ID),
|
||||
TransactionId: idBytesToString(gormAttachment.TransactionID),
|
||||
OrgId: idBytesToString(gormAttachment.OrgID),
|
||||
UserId: idBytesToString(gormAttachment.UserID),
|
||||
FileName: gormAttachment.FileName,
|
||||
OriginalName: gormAttachment.OriginalName,
|
||||
ContentType: gormAttachment.ContentType,
|
||||
FileSize: gormAttachment.FileSize,
|
||||
FilePath: gormAttachment.FilePath,
|
||||
Description: gormAttachment.Description,
|
||||
Uploaded: gormAttachment.Uploaded,
|
||||
Deleted: gormAttachment.Deleted,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user