709 lines
22 KiB
Go
709 lines
22 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"eslogad-be/config"
|
|
"eslogad-be/internal/appcontext"
|
|
"eslogad-be/internal/contract"
|
|
"eslogad-be/internal/entities"
|
|
"eslogad-be/internal/processor"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type OnlyOfficeService interface {
|
|
ProcessCallback(ctx context.Context, documentKey string, req *contract.OnlyOfficeCallbackRequest) (*contract.OnlyOfficeCallbackResponse, error)
|
|
GetEditorConfig(ctx context.Context, req *contract.GetEditorConfigRequest) (*contract.GetEditorConfigResponse, error)
|
|
LockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error
|
|
UnlockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error
|
|
GetDocumentSession(ctx context.Context, documentKey string) (*contract.DocumentSession, error)
|
|
GetOnlyOfficeConfig(ctx context.Context) (*contract.OnlyOfficeConfigInfo, error)
|
|
}
|
|
|
|
type OnlyOfficeServiceImpl struct {
|
|
processor processor.OnlyOfficeProcessor
|
|
documentBaseURL string
|
|
callbackBaseURL string
|
|
serverURL string
|
|
jwtSecret string
|
|
config *config.OnlyOffice
|
|
db *gorm.DB
|
|
fileStorage FileStorage
|
|
docBucket string
|
|
}
|
|
|
|
func NewOnlyOfficeService(processor processor.OnlyOfficeProcessor, cfg *config.OnlyOffice, db *gorm.DB, fileStorage FileStorage) *OnlyOfficeServiceImpl {
|
|
return &OnlyOfficeServiceImpl{
|
|
processor: processor,
|
|
documentBaseURL: getEnvOrDefault("DOCUMENT_BASE_URL", "https://68878e421f6d.ngrok-free.app/api/v1/files"),
|
|
callbackBaseURL: getEnvOrDefault("CALLBACK_BASE_URL", "https://b4ed0a70d9d6.ngrok-free.app/api/v1/onlyoffice/callback"),
|
|
serverURL: cfg.URL,
|
|
jwtSecret: cfg.Token,
|
|
config: cfg,
|
|
db: db,
|
|
fileStorage: fileStorage,
|
|
docBucket: "documents", // Use the same bucket as document uploads
|
|
}
|
|
}
|
|
|
|
func getEnvOrDefault(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (s *OnlyOfficeServiceImpl) ProcessCallback(ctx context.Context, documentKey string, req *contract.OnlyOfficeCallbackRequest) (*contract.OnlyOfficeCallbackResponse, error) {
|
|
// Verify JWT token if provided and secret is configured
|
|
if req.Token != "" && s.jwtSecret != "" {
|
|
claims, err := s.verifyJWT(req.Token)
|
|
if err != nil {
|
|
// Log the error but continue processing
|
|
// OnlyOffice may not always send valid tokens
|
|
fmt.Printf("JWT verification failed: %v\n", err)
|
|
} else if claims != nil {
|
|
// Extract data from JWT claims if needed
|
|
if key, ok := claims["key"].(string); ok && key != documentKey {
|
|
return &contract.OnlyOfficeCallbackResponse{Error: 1}, fmt.Errorf("document key mismatch in JWT")
|
|
}
|
|
}
|
|
}
|
|
|
|
session, err := s.processor.GetDocumentSessionByKey(ctx, documentKey)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return &contract.OnlyOfficeCallbackResponse{Error: 1}, nil // Document key not found
|
|
}
|
|
return &contract.OnlyOfficeCallbackResponse{Error: 3}, err // Internal server error
|
|
}
|
|
|
|
// Process based on status
|
|
switch req.Status {
|
|
case contract.OnlyOfficeStatusEditing:
|
|
// Document is being edited
|
|
err = s.handleEditingStatus(ctx, session, req)
|
|
|
|
case contract.OnlyOfficeStatusReady:
|
|
// Document is ready for saving
|
|
err = s.handleReadyStatus(ctx, session, req)
|
|
|
|
case contract.OnlyOfficeStatusSaveError:
|
|
// Document saving error
|
|
err = s.handleSaveError(ctx, session, req)
|
|
|
|
case contract.OnlyOfficeStatusClosed:
|
|
// Document closed with no changes
|
|
err = s.handleClosedStatus(ctx, session, req)
|
|
|
|
case contract.OnlyOfficeStatusForceSave:
|
|
// Force save during editing
|
|
err = s.handleForceSave(ctx, session, req)
|
|
|
|
case contract.OnlyOfficeStatusForceSaveError:
|
|
// Force save error
|
|
err = s.handleForceSaveError(ctx, session, req)
|
|
|
|
default:
|
|
return &contract.OnlyOfficeCallbackResponse{Error: 3}, fmt.Errorf("unknown status: %d", req.Status)
|
|
}
|
|
|
|
if err != nil {
|
|
return &contract.OnlyOfficeCallbackResponse{Error: 3}, err
|
|
}
|
|
|
|
return &contract.OnlyOfficeCallbackResponse{Error: 0}, nil
|
|
}
|
|
|
|
// handleEditingStatus handles when document is being edited
|
|
func (s *OnlyOfficeServiceImpl) handleEditingStatus(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
|
// Update session status
|
|
session.Status = req.Status
|
|
|
|
// Lock document if not already locked
|
|
if !session.IsLocked && len(req.Users) > 0 {
|
|
userID := getOnlyOfficeUserIDFromContext(ctx)
|
|
session.IsLocked = true
|
|
session.LockedBy = &userID
|
|
now := time.Now()
|
|
session.LockedAt = &now
|
|
}
|
|
|
|
return s.processor.UpdateDocumentSession(ctx, session)
|
|
}
|
|
|
|
// handleReadyStatus handles when document is ready for saving
|
|
func (s *OnlyOfficeServiceImpl) handleReadyStatus(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
|
if req.URL == "" {
|
|
return errors.New("document URL is required for saving")
|
|
}
|
|
|
|
// Download the document
|
|
documentData, err := s.downloadDocument(req.URL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to download document: %w", err)
|
|
}
|
|
|
|
// Use session UserID as SavedBy since callbacks don't have user context
|
|
savedBy := session.UserID
|
|
if savedBy == uuid.Nil {
|
|
// Fallback to getting from context if available
|
|
if userID := getOnlyOfficeUserIDFromContext(ctx); userID != uuid.Nil {
|
|
savedBy = userID
|
|
}
|
|
}
|
|
|
|
// Save new version
|
|
version := &entities.DocumentVersion{
|
|
DocumentID: session.DocumentID,
|
|
Version: session.Version + 1,
|
|
FileSize: int64(len(documentData)),
|
|
SavedBy: savedBy,
|
|
SavedAt: time.Now(),
|
|
IsActive: true,
|
|
}
|
|
|
|
// For now, default to outgoing_attachment
|
|
// In production, this should be stored in the session or document metadata
|
|
documentType := "outgoing_attachment"
|
|
|
|
// Generate new file path and save
|
|
fileName := fmt.Sprintf("v%d_%s_%s.docx", version.Version, time.Now().Format("20060102150405"), session.DocumentKey)
|
|
filePath, err := s.saveDocumentFile(ctx, documentData, session.DocumentID, fileName, documentType)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save document file: %w", err)
|
|
}
|
|
version.FileURL = filePath
|
|
|
|
// Save changes URL if provided
|
|
if req.ChangesURL != "" {
|
|
version.ChangesURL = &req.ChangesURL
|
|
}
|
|
|
|
// Create new version
|
|
err = s.processor.CreateDocumentVersion(ctx, version)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create document version: %w", err)
|
|
}
|
|
|
|
// Update session
|
|
session.Status = req.Status
|
|
session.Version = version.Version
|
|
now := time.Now()
|
|
session.LastSavedAt = &now
|
|
session.IsLocked = false
|
|
session.LockedBy = nil
|
|
session.LockedAt = nil
|
|
|
|
// Update the original document reference with new URL
|
|
err = s.processor.UpdateDocumentURL(ctx, session.DocumentID, version.FileURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update document URL: %w", err)
|
|
}
|
|
|
|
return s.processor.UpdateDocumentSession(ctx, session)
|
|
}
|
|
|
|
// handleSaveError handles document save errors
|
|
func (s *OnlyOfficeServiceImpl) handleSaveError(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
|
// Log the error
|
|
s.processor.LogDocumentError(ctx, session.DocumentID, "Save error occurred", req)
|
|
|
|
// Update session status
|
|
session.Status = req.Status
|
|
return s.processor.UpdateDocumentSession(ctx, session)
|
|
}
|
|
|
|
// handleClosedStatus handles when document is closed without changes
|
|
func (s *OnlyOfficeServiceImpl) handleClosedStatus(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
|
// Unlock document
|
|
session.Status = req.Status
|
|
session.IsLocked = false
|
|
session.LockedBy = nil
|
|
session.LockedAt = nil
|
|
|
|
return s.processor.UpdateDocumentSession(ctx, session)
|
|
}
|
|
|
|
func (s *OnlyOfficeServiceImpl) handleForceSave(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
|
if req.URL == "" {
|
|
return errors.New("document URL is required for force save")
|
|
}
|
|
|
|
documentData, err := s.downloadDocument(req.URL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to download document: %w", err)
|
|
}
|
|
|
|
savedBy := session.UserID
|
|
if savedBy == uuid.Nil {
|
|
if userID := getOnlyOfficeUserIDFromContext(ctx); userID != uuid.Nil {
|
|
savedBy = userID
|
|
}
|
|
}
|
|
|
|
version := &entities.DocumentVersion{
|
|
DocumentID: session.DocumentID,
|
|
Version: session.Version + 1,
|
|
FileSize: int64(len(documentData)),
|
|
SavedBy: savedBy,
|
|
SavedAt: time.Now(),
|
|
IsActive: false,
|
|
Comments: stringPtr("Auto-save during editing"),
|
|
}
|
|
|
|
// For now, default to outgoing_attachment
|
|
// In production, this should be stored in the session or document metadata
|
|
documentType := "outgoing_attachment"
|
|
|
|
fileName := fmt.Sprintf("autosave_v%d_%s_%s.docx", version.Version, time.Now().Format("20060102150405"), session.DocumentKey)
|
|
filePath, err := s.saveDocumentFile(ctx, documentData, session.DocumentID, fileName, documentType)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save document file: %w", err)
|
|
}
|
|
version.FileURL = filePath
|
|
|
|
err = s.processor.CreateDocumentVersion(ctx, version)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create document version: %w", err)
|
|
}
|
|
|
|
now := time.Now()
|
|
session.LastSavedAt = &now
|
|
session.Version = version.Version
|
|
|
|
return s.processor.UpdateDocumentSession(ctx, session)
|
|
}
|
|
|
|
// handleForceSaveError handles force save errors
|
|
func (s *OnlyOfficeServiceImpl) handleForceSaveError(ctx context.Context, session *entities.DocumentSession, req *contract.OnlyOfficeCallbackRequest) error {
|
|
// Log the error
|
|
s.processor.LogDocumentError(ctx, session.DocumentID, "Force save error occurred", req)
|
|
|
|
// Update session status
|
|
session.Status = req.Status
|
|
return s.processor.UpdateDocumentSession(ctx, session)
|
|
}
|
|
|
|
// downloadDocument downloads document from OnlyOffice
|
|
func (s *OnlyOfficeServiceImpl) downloadDocument(url string) ([]byte, error) {
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("failed to download document: status %d", resp.StatusCode)
|
|
}
|
|
|
|
return io.ReadAll(resp.Body)
|
|
}
|
|
|
|
// saveDocumentFile saves document file to S3 storage
|
|
func (s *OnlyOfficeServiceImpl) saveDocumentFile(ctx context.Context, data []byte, documentID uuid.UUID, fileName string, documentType string) (string, error) {
|
|
// Ensure bucket exists
|
|
if err := s.fileStorage.EnsureBucket(ctx, s.docBucket); err != nil {
|
|
return "", fmt.Errorf("failed to ensure bucket: %w", err)
|
|
}
|
|
|
|
// Create S3 key with date structure
|
|
dateDir := time.Now().Format("2006/01/02")
|
|
key := fmt.Sprintf("onlyoffice/%s/%s/%s", documentID.String(), dateDir, fileName)
|
|
|
|
// Detect content type from file extension
|
|
contentType := "application/octet-stream"
|
|
ext := filepath.Ext(fileName)
|
|
switch strings.ToLower(ext) {
|
|
case ".docx":
|
|
contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
case ".doc":
|
|
contentType = "application/msword"
|
|
case ".xlsx":
|
|
contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
case ".xls":
|
|
contentType = "application/vnd.ms-excel"
|
|
case ".pptx":
|
|
contentType = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
case ".ppt":
|
|
contentType = "application/vnd.ms-powerpoint"
|
|
case ".pdf":
|
|
contentType = "application/pdf"
|
|
}
|
|
|
|
// Upload to S3
|
|
url, err := s.fileStorage.Upload(ctx, s.docBucket, key, data, contentType)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to upload to S3: %w", err)
|
|
}
|
|
|
|
// Now update the attachment URL in the database
|
|
if err := s.updateAttachmentURL(ctx, documentID, url, documentType); err != nil {
|
|
// Log error but don't fail - the document is already saved
|
|
fmt.Printf("Warning: Failed to update attachment URL: %v\n", err)
|
|
}
|
|
|
|
return url, nil
|
|
}
|
|
|
|
// updateAttachmentURL updates the file_url in the appropriate attachment table
|
|
func (s *OnlyOfficeServiceImpl) updateAttachmentURL(ctx context.Context, attachmentID uuid.UUID, newURL string, documentType string) error {
|
|
switch documentType {
|
|
case "letter_outgoing_attachment", "outgoing_attachment":
|
|
return s.db.WithContext(ctx).
|
|
Table("letter_outgoing_attachments").
|
|
Where("id = ?", attachmentID).
|
|
Update("file_url", newURL).Error
|
|
|
|
case "letter_incoming_attachment", "incoming_attachment":
|
|
return s.db.WithContext(ctx).
|
|
Table("letter_incoming_attachments").
|
|
Where("id = ?", attachmentID).
|
|
Update("file_url", newURL).Error
|
|
|
|
default:
|
|
return fmt.Errorf("unsupported document type for URL update: %s", documentType)
|
|
}
|
|
}
|
|
|
|
// getDocumentFromAttachment retrieves document details directly from attachment tables
|
|
func (s *OnlyOfficeServiceImpl) getDocumentFromAttachment(ctx context.Context, documentID uuid.UUID, documentType string) (*processor.DocumentDetails, error) {
|
|
var fileName, fileURL, fileType string
|
|
var fileSize int64
|
|
|
|
switch documentType {
|
|
case "letter_outgoing_attachment", "outgoing_attachment":
|
|
var attachment struct {
|
|
FileName string `gorm:"column:file_name"`
|
|
FileURL string `gorm:"column:file_url"`
|
|
FileType string `gorm:"column:file_type"`
|
|
}
|
|
|
|
err := s.db.WithContext(ctx).
|
|
Table("letter_outgoing_attachments").
|
|
Where("id = ?", documentID).
|
|
Select("file_name, file_url, file_type").
|
|
First(&attachment).Error
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get outgoing attachment: %w", err)
|
|
}
|
|
|
|
fileName = attachment.FileName
|
|
fileURL = attachment.FileURL
|
|
fileType = attachment.FileType
|
|
|
|
case "letter_incoming_attachment", "incoming_attachment":
|
|
var attachment struct {
|
|
FileName string `gorm:"column:file_name"`
|
|
FileURL string `gorm:"column:file_url"`
|
|
FileType string `gorm:"column:file_type"`
|
|
}
|
|
|
|
err := s.db.WithContext(ctx).
|
|
Table("letter_incoming_attachments").
|
|
Where("id = ?", documentID).
|
|
Select("file_name, file_url, file_type").
|
|
First(&attachment).Error
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get incoming attachment: %w", err)
|
|
}
|
|
|
|
fileName = attachment.FileName
|
|
fileURL = attachment.FileURL
|
|
fileType = attachment.FileType
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unsupported document type: %s", documentType)
|
|
}
|
|
|
|
return &processor.DocumentDetails{
|
|
DocumentID: documentID,
|
|
FileName: fileName,
|
|
FileType: fileType,
|
|
FileURL: fileURL,
|
|
FileSize: fileSize,
|
|
DocumentType: documentType,
|
|
ReferenceID: documentID,
|
|
}, nil
|
|
}
|
|
|
|
// GetEditorConfig generates OnlyOffice editor configuration
|
|
func (s *OnlyOfficeServiceImpl) GetEditorConfig(ctx context.Context, req *contract.GetEditorConfigRequest) (*contract.GetEditorConfigResponse, error) {
|
|
userCtx := appcontext.FromGinContext(ctx)
|
|
if userCtx == nil {
|
|
return nil, errors.New("user context not found")
|
|
}
|
|
|
|
session, err := s.processor.GetOrCreateDocumentSession(ctx, req.DocumentID, userCtx.UserID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get document session: %w", err)
|
|
}
|
|
|
|
// Get document details directly from attachment tables
|
|
document, err := s.getDocumentFromAttachment(ctx, req.DocumentID, req.DocumentType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get document details: %w", err)
|
|
}
|
|
|
|
documentKey := session.DocumentKey
|
|
|
|
fileExt := s.getFileExtension(document.FileName)
|
|
if fileExt == "" || fileExt == strings.ToLower(document.FileName) {
|
|
fileExt = s.getFileExtension(document.FileType)
|
|
}
|
|
|
|
ooType := "desktop"
|
|
if req.DocumentType == "incoming_attachment" {
|
|
ooType = "embedded"
|
|
}
|
|
|
|
config := &contract.OnlyOfficeConfigRequest{
|
|
Document: &contract.OnlyOfficeDocument{
|
|
FileType: fileExt,
|
|
Key: documentKey,
|
|
Title: document.FileName,
|
|
URL: document.FileURL,
|
|
Permissions: &contract.OnlyOfficePermissions{
|
|
Comment: true,
|
|
Download: true,
|
|
Edit: req.Mode == "edit",
|
|
FillForms: true,
|
|
Print: true,
|
|
Review: req.Mode == "edit",
|
|
},
|
|
Info: &contract.OnlyOfficeDocumentInfo{
|
|
Owner: fmt.Sprintf("User-%s", userCtx.UserID.String()[:8]),
|
|
Uploaded: time.Now().Format("2006-01-02 15:04:05"),
|
|
},
|
|
},
|
|
DocumentType: s.getDocumentType(fileExt), // Convert file extension to document type
|
|
EditorConfig: &contract.OnlyOfficeEditorConfig{
|
|
CallbackURL: fmt.Sprintf("%s/%s", s.callbackBaseURL, documentKey),
|
|
Lang: "en",
|
|
Mode: req.Mode,
|
|
User: &contract.OnlyOfficeUserConfig{
|
|
ID: userCtx.UserID.String(),
|
|
Name: userCtx.UserName,
|
|
},
|
|
Customization: &contract.OnlyOfficeCustomization{
|
|
Autosave: true,
|
|
Comments: true,
|
|
CompactHeader: false,
|
|
ForceSave: true,
|
|
Zoom: 100,
|
|
},
|
|
},
|
|
Type: ooType, // Can be desktop, mobile, or embedded
|
|
}
|
|
|
|
if s.jwtSecret != "" {
|
|
token, err := s.generateJWT(config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate JWT: %w", err)
|
|
}
|
|
config.Token = token
|
|
}
|
|
|
|
return &contract.GetEditorConfigResponse{
|
|
DocumentServerURL: s.serverURL,
|
|
Config: config,
|
|
}, nil
|
|
}
|
|
|
|
// LockDocument locks a document for editing
|
|
func (s *OnlyOfficeServiceImpl) LockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error {
|
|
return s.processor.LockDocument(ctx, documentID, userID)
|
|
}
|
|
|
|
// UnlockDocument unlocks a document
|
|
func (s *OnlyOfficeServiceImpl) UnlockDocument(ctx context.Context, documentID uuid.UUID, userID uuid.UUID) error {
|
|
return s.processor.UnlockDocument(ctx, documentID, userID)
|
|
}
|
|
|
|
// GetDocumentSession gets document session by key
|
|
func (s *OnlyOfficeServiceImpl) GetDocumentSession(ctx context.Context, documentKey string) (*contract.DocumentSession, error) {
|
|
session, err := s.processor.GetDocumentSessionByKey(ctx, documentKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &contract.DocumentSession{
|
|
ID: session.ID,
|
|
DocumentID: session.DocumentID,
|
|
DocumentKey: session.DocumentKey,
|
|
UserID: session.UserID,
|
|
Status: session.Status,
|
|
IsLocked: session.IsLocked,
|
|
LockedBy: session.LockedBy,
|
|
LockedAt: session.LockedAt,
|
|
LastSavedAt: session.LastSavedAt,
|
|
Version: session.Version,
|
|
CreatedAt: session.CreatedAt,
|
|
UpdatedAt: session.UpdatedAt,
|
|
}, nil
|
|
}
|
|
|
|
// generateDocumentKey generates a unique key for OnlyOffice
|
|
func (s *OnlyOfficeServiceImpl) generateDocumentKey(documentID uuid.UUID, version int) string {
|
|
// Use nanoseconds and random bytes for uniqueness
|
|
randomBytes := make([]byte, 8)
|
|
rand.Read(randomBytes)
|
|
data := fmt.Sprintf("%s_%d_%d_%s", documentID.String(), version, time.Now().UnixNano(), hex.EncodeToString(randomBytes))
|
|
hash := md5.Sum([]byte(data))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
// getFileExtension extracts the file extension from file type or filename
|
|
func (s *OnlyOfficeServiceImpl) getFileExtension(fileType string) string {
|
|
// Remove any leading dot
|
|
fileType = strings.TrimPrefix(fileType, ".")
|
|
|
|
// If fileType contains a dot, extract the extension after the last dot
|
|
if strings.Contains(fileType, ".") {
|
|
parts := strings.Split(fileType, ".")
|
|
if len(parts) > 1 {
|
|
return strings.ToLower(parts[len(parts)-1])
|
|
}
|
|
}
|
|
|
|
// Otherwise, return as is (assuming it's already an extension like "docx", "xlsx", etc.)
|
|
return strings.ToLower(fileType)
|
|
}
|
|
|
|
// getDocumentType determines OnlyOffice document type from file extension
|
|
func (s *OnlyOfficeServiceImpl) getDocumentType(fileType string) string {
|
|
fileType = strings.ToLower(fileType)
|
|
|
|
// Remove dot if present
|
|
fileType = strings.TrimPrefix(fileType, ".")
|
|
|
|
// Text documents
|
|
if fileType == "doc" || fileType == "docx" || fileType == "docm" ||
|
|
fileType == "dot" || fileType == "dotx" || fileType == "dotm" ||
|
|
fileType == "odt" || fileType == "fodt" || fileType == "ott" ||
|
|
fileType == "rtf" || fileType == "txt" || fileType == "html" ||
|
|
fileType == "htm" || fileType == "mht" || fileType == "pdf" ||
|
|
fileType == "djvu" || fileType == "fb2" || fileType == "epub" ||
|
|
fileType == "xps" {
|
|
return "word"
|
|
}
|
|
|
|
// Spreadsheets
|
|
if fileType == "xls" || fileType == "xlsx" || fileType == "xlsm" ||
|
|
fileType == "xlt" || fileType == "xltx" || fileType == "xltm" ||
|
|
fileType == "ods" || fileType == "fods" || fileType == "ots" ||
|
|
fileType == "csv" {
|
|
return "cell"
|
|
}
|
|
|
|
// Presentations
|
|
if fileType == "pps" || fileType == "ppsx" || fileType == "ppsm" ||
|
|
fileType == "ppt" || fileType == "pptx" || fileType == "pptm" ||
|
|
fileType == "pot" || fileType == "potx" || fileType == "potm" ||
|
|
fileType == "odp" || fileType == "fodp" || fileType == "otp" {
|
|
return "presentation"
|
|
}
|
|
|
|
// Default to text
|
|
return "slide"
|
|
}
|
|
|
|
// generateJWT generates JWT token for OnlyOffice
|
|
func (s *OnlyOfficeServiceImpl) generateJWT(config *contract.OnlyOfficeConfigRequest) (string, error) {
|
|
// OnlyOffice expects the entire config to be in the JWT payload
|
|
payload := make(map[string]interface{})
|
|
|
|
// Convert the config struct to a map for JWT payload
|
|
configJSON, err := json.Marshal(config)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal config: %w", err)
|
|
}
|
|
|
|
var configMap map[string]interface{}
|
|
if err := json.Unmarshal(configJSON, &configMap); err != nil {
|
|
return "", fmt.Errorf("failed to unmarshal config to map: %w", err)
|
|
}
|
|
|
|
// Add all config fields to the payload
|
|
for key, value := range configMap {
|
|
payload[key] = value
|
|
}
|
|
|
|
// Create the token with the payload
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload))
|
|
|
|
// Sign the token with the secret
|
|
tokenString, err := token.SignedString([]byte(s.jwtSecret))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to sign JWT token: %w", err)
|
|
}
|
|
|
|
return tokenString, nil
|
|
}
|
|
|
|
func getOnlyOfficeUserIDFromContext(ctx context.Context) uuid.UUID {
|
|
userCtx := appcontext.FromGinContext(ctx)
|
|
if userCtx != nil {
|
|
return userCtx.UserID
|
|
}
|
|
return uuid.Nil
|
|
}
|
|
|
|
func stringPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
// GetOnlyOfficeConfig returns the OnlyOffice configuration
|
|
func (s *OnlyOfficeServiceImpl) GetOnlyOfficeConfig(ctx context.Context) (*contract.OnlyOfficeConfigInfo, error) {
|
|
return &contract.OnlyOfficeConfigInfo{
|
|
URL: s.config.URL,
|
|
Token: s.config.Token,
|
|
}, nil
|
|
}
|
|
|
|
// verifyJWT verifies JWT token from OnlyOffice
|
|
func (s *OnlyOfficeServiceImpl) verifyJWT(tokenString string) (jwt.MapClaims, error) {
|
|
if s.jwtSecret == "" {
|
|
// If no secret is configured, skip verification
|
|
return nil, nil
|
|
}
|
|
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
// Validate the signing method
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
}
|
|
return []byte(s.jwtSecret), nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse JWT: %w", err)
|
|
}
|
|
|
|
if !token.Valid {
|
|
return nil, errors.New("invalid JWT token")
|
|
}
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return nil, errors.New("failed to parse JWT claims")
|
|
}
|
|
|
|
return claims, nil
|
|
}
|