Add document saved
This commit is contained in:
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"eslogad-be/internal/appcontext"
|
||||
"eslogad-be/internal/contract"
|
||||
@@ -36,6 +37,11 @@ type LetterOutgoingService interface {
|
||||
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error)
|
||||
UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error
|
||||
DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error
|
||||
|
||||
GetLetterApprovalInfo(ctx context.Context, letterID uuid.UUID) (*contract.LetterApprovalInfoResponse, error)
|
||||
|
||||
// GetApprovalDiscussions returns both approvals and discussions for an outgoing letter
|
||||
GetApprovalDiscussions(ctx context.Context, letterID uuid.UUID) (*contract.OutgoingLetterApprovalDiscussionsResponse, error)
|
||||
}
|
||||
|
||||
type LetterOutgoingServiceImpl struct {
|
||||
@@ -336,12 +342,13 @@ func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID
|
||||
recipients := make([]entities.LetterOutgoingRecipient, len(req.Recipients))
|
||||
for i, r := range req.Recipients {
|
||||
recipients[i] = entities.LetterOutgoingRecipient{
|
||||
LetterID: letterID,
|
||||
RecipientName: r.Name,
|
||||
RecipientEmail: r.Email,
|
||||
RecipientPosition: r.Position,
|
||||
RecipientInstitution: r.Institution,
|
||||
IsPrimary: r.IsPrimary,
|
||||
LetterID: letterID,
|
||||
UserID: r.UserID,
|
||||
DepartmentID: r.DepartmentID,
|
||||
IsPrimary: r.IsPrimary,
|
||||
Status: r.Status,
|
||||
Flag: r.Flag,
|
||||
IsArchived: r.IsArchived,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,12 +366,24 @@ func (s *LetterOutgoingServiceImpl) UpdateRecipient(ctx context.Context, letterI
|
||||
}
|
||||
|
||||
recipient := &entities.LetterOutgoingRecipient{
|
||||
ID: recipientID,
|
||||
RecipientName: req.Name,
|
||||
RecipientEmail: req.Email,
|
||||
RecipientPosition: req.Position,
|
||||
RecipientInstitution: req.Institution,
|
||||
IsPrimary: req.IsPrimary,
|
||||
ID: recipientID,
|
||||
IsPrimary: req.IsPrimary,
|
||||
}
|
||||
|
||||
if req.UserID != nil {
|
||||
recipient.UserID = req.UserID
|
||||
}
|
||||
if req.DepartmentID != nil {
|
||||
recipient.DepartmentID = req.DepartmentID
|
||||
}
|
||||
if req.Status != nil {
|
||||
recipient.Status = *req.Status
|
||||
}
|
||||
if req.Flag != nil {
|
||||
recipient.Flag = req.Flag
|
||||
}
|
||||
if req.IsArchived != nil {
|
||||
recipient.IsArchived = *req.IsArchived
|
||||
}
|
||||
|
||||
return s.processor.UpdateRecipient(ctx, recipient)
|
||||
@@ -501,6 +520,83 @@ func (s *LetterOutgoingServiceImpl) DeleteDiscussion(ctx context.Context, discus
|
||||
return s.processor.DeleteDiscussion(ctx, discussionID)
|
||||
}
|
||||
|
||||
func (s *LetterOutgoingServiceImpl) GetLetterApprovalInfo(ctx context.Context, letterID uuid.UUID) (*contract.LetterApprovalInfoResponse, error) {
|
||||
userID := getUserIDFromContext(ctx)
|
||||
|
||||
_, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var currentApproval *entities.LetterOutgoingApproval
|
||||
var isApproverOnActiveStep bool
|
||||
var canApprove bool
|
||||
|
||||
for _, approval := range approvals {
|
||||
if approval.Status == entities.ApprovalStatusPending {
|
||||
currentApproval = &approval
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check if current user is the approver for the active step
|
||||
if currentApproval != nil && currentApproval.Step != nil {
|
||||
step := currentApproval.Step
|
||||
|
||||
// Check if user is the specific approver
|
||||
if step.ApproverUserID != nil && *step.ApproverUserID == userID {
|
||||
isApproverOnActiveStep = true
|
||||
canApprove = true
|
||||
}
|
||||
// Note: Role-based approval check would require additional implementation
|
||||
// For now, we only support user-specific approvers
|
||||
}
|
||||
|
||||
// Build actions based on current status
|
||||
var actions []contract.ApprovalAction
|
||||
if canApprove && currentApproval != nil {
|
||||
actions = []contract.ApprovalAction{
|
||||
{
|
||||
Type: "APPROVE",
|
||||
Href: fmt.Sprintf("/v1/letters/%s/approvals/%s/decision", letterID, currentApproval.ID),
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Type: "REJECT",
|
||||
Href: fmt.Sprintf("/v1/letters/%s/approvals/%s/decision", letterID, currentApproval.ID),
|
||||
Method: "POST",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Determine decision status
|
||||
decisionStatus := "PENDING"
|
||||
if currentApproval == nil {
|
||||
decisionStatus = "COMPLETED"
|
||||
}
|
||||
|
||||
// Determine notes visibility
|
||||
notesVisibility := "FULL"
|
||||
if !isApproverOnActiveStep {
|
||||
notesVisibility = "READONLY"
|
||||
}
|
||||
|
||||
info := &contract.LetterApprovalInfoResponse{
|
||||
IsApproverOnActiveStep: isApproverOnActiveStep,
|
||||
DecisionStatus: decisionStatus,
|
||||
CanApprove: canApprove,
|
||||
Actions: actions,
|
||||
NotesVisibility: notesVisibility,
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func getUserIDFromContext(ctx context.Context) uuid.UUID {
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
if appCtx != nil {
|
||||
@@ -521,6 +617,211 @@ func userHasRole(ctx context.Context, roleID uuid.UUID) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context, letterID uuid.UUID) (*contract.OutgoingLetterApprovalDiscussionsResponse, error) {
|
||||
// Get the letter with all related data
|
||||
letter, err := s.processor.GetOutgoingLetterWithDetails(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Transform approvals
|
||||
approvals := make([]contract.EnhancedOutgoingLetterApprovalResponse, 0, len(letter.Approvals))
|
||||
for _, approval := range letter.Approvals {
|
||||
approvalResp := contract.EnhancedOutgoingLetterApprovalResponse{
|
||||
ID: approval.ID,
|
||||
LetterID: approval.LetterID,
|
||||
StepID: approval.StepID,
|
||||
ApproverID: approval.ApproverID,
|
||||
Status: string(approval.Status),
|
||||
Remarks: approval.Remarks,
|
||||
ActedAt: approval.ActedAt,
|
||||
CreatedAt: approval.CreatedAt,
|
||||
}
|
||||
|
||||
// Add step details if available
|
||||
if approval.Step != nil {
|
||||
approvalResp.Step = &contract.ApprovalFlowStepResponse{
|
||||
ID: approval.Step.ID,
|
||||
StepOrder: approval.Step.StepOrder,
|
||||
ParallelGroup: approval.Step.ParallelGroup,
|
||||
Required: approval.Step.Required,
|
||||
CreatedAt: approval.Step.CreatedAt,
|
||||
UpdatedAt: approval.Step.UpdatedAt,
|
||||
}
|
||||
|
||||
if approval.Step.ApproverRoleID != nil {
|
||||
approvalResp.Step.ApproverRoleID = approval.Step.ApproverRoleID
|
||||
}
|
||||
if approval.Step.ApproverUserID != nil {
|
||||
approvalResp.Step.ApproverUserID = approval.Step.ApproverUserID
|
||||
}
|
||||
|
||||
// Add role information if available
|
||||
if approval.Step.ApproverRole != nil {
|
||||
approvalResp.Step.ApproverRole = &contract.RoleResponse{
|
||||
ID: approval.Step.ApproverRole.ID,
|
||||
Name: approval.Step.ApproverRole.Name,
|
||||
Code: approval.Step.ApproverRole.Code,
|
||||
}
|
||||
}
|
||||
|
||||
// Add user information if available
|
||||
if approval.Step.ApproverUser != nil {
|
||||
approvalResp.Step.ApproverUser = &contract.UserResponse{
|
||||
ID: approval.Step.ApproverUser.ID,
|
||||
Name: approval.Step.ApproverUser.Name,
|
||||
Email: approval.Step.ApproverUser.Email,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add approver details if available
|
||||
if approval.Approver != nil {
|
||||
approvalResp.Approver = &contract.UserResponse{
|
||||
ID: approval.Approver.ID,
|
||||
Name: approval.Approver.Name,
|
||||
Email: approval.Approver.Email,
|
||||
}
|
||||
|
||||
// Add profile if available
|
||||
if approval.Approver.Profile != nil {
|
||||
approvalResp.Approver.Profile = &contract.UserProfileResponse{
|
||||
UserID: approval.Approver.Profile.UserID,
|
||||
FullName: approval.Approver.Profile.FullName,
|
||||
DisplayName: approval.Approver.Profile.DisplayName,
|
||||
Phone: approval.Approver.Profile.Phone,
|
||||
AvatarURL: approval.Approver.Profile.AvatarURL,
|
||||
JobTitle: approval.Approver.Profile.JobTitle,
|
||||
EmployeeNo: approval.Approver.Profile.EmployeeNo,
|
||||
Bio: approval.Approver.Profile.Bio,
|
||||
Timezone: approval.Approver.Profile.Timezone,
|
||||
Locale: approval.Approver.Profile.Locale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
approvals = append(approvals, approvalResp)
|
||||
}
|
||||
|
||||
// Transform discussions
|
||||
discussions := make([]contract.OutgoingLetterDiscussionResponse, 0, len(letter.Discussions))
|
||||
for _, discussion := range letter.Discussions {
|
||||
// Extract mentioned user IDs from mentions
|
||||
mentionedUserIDs := extractMentionedUserIDs(discussion.Mentions)
|
||||
|
||||
discussionResp := contract.OutgoingLetterDiscussionResponse{
|
||||
ID: discussion.ID,
|
||||
LetterID: discussion.LetterID,
|
||||
ParentID: discussion.ParentID,
|
||||
UserID: discussion.UserID,
|
||||
Message: discussion.Message,
|
||||
Mentions: discussion.Mentions,
|
||||
CreatedAt: discussion.CreatedAt,
|
||||
UpdatedAt: discussion.UpdatedAt,
|
||||
EditedAt: discussion.EditedAt,
|
||||
}
|
||||
|
||||
// Add user details if available
|
||||
if discussion.User != nil {
|
||||
discussionResp.User = &contract.UserResponse{
|
||||
ID: discussion.User.ID,
|
||||
Name: discussion.User.Name,
|
||||
Email: discussion.User.Email,
|
||||
IsActive: discussion.User.IsActive,
|
||||
CreatedAt: discussion.User.CreatedAt,
|
||||
UpdatedAt: discussion.User.UpdatedAt,
|
||||
}
|
||||
|
||||
// Add profile if available
|
||||
if discussion.User.Profile != nil {
|
||||
discussionResp.User.Profile = &contract.UserProfileResponse{
|
||||
UserID: discussion.User.Profile.UserID,
|
||||
FullName: discussion.User.Profile.FullName,
|
||||
DisplayName: discussion.User.Profile.DisplayName,
|
||||
Phone: discussion.User.Profile.Phone,
|
||||
AvatarURL: discussion.User.Profile.AvatarURL,
|
||||
JobTitle: discussion.User.Profile.JobTitle,
|
||||
EmployeeNo: discussion.User.Profile.EmployeeNo,
|
||||
Bio: discussion.User.Profile.Bio,
|
||||
Timezone: discussion.User.Profile.Timezone,
|
||||
Locale: discussion.User.Profile.Locale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get mentioned users details
|
||||
if len(mentionedUserIDs) > 0 {
|
||||
mentionedUsers, _ := s.processor.GetUsersByIDs(ctx, mentionedUserIDs)
|
||||
for _, user := range mentionedUsers {
|
||||
mentionedUserResp := contract.UserResponse{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
IsActive: user.IsActive,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
if user.Profile != nil {
|
||||
mentionedUserResp.Profile = &contract.UserProfileResponse{
|
||||
UserID: user.Profile.UserID,
|
||||
FullName: user.Profile.FullName,
|
||||
DisplayName: user.Profile.DisplayName,
|
||||
Timezone: user.Profile.Timezone,
|
||||
Locale: user.Profile.Locale,
|
||||
}
|
||||
}
|
||||
|
||||
discussionResp.MentionedUsers = append(discussionResp.MentionedUsers, mentionedUserResp)
|
||||
}
|
||||
}
|
||||
|
||||
// Add attachments if available
|
||||
for _, attachment := range discussion.Attachments {
|
||||
attachmentResp := contract.OutgoingLetterDiscussionAttachmentResponse{
|
||||
ID: attachment.ID,
|
||||
DiscussionID: attachment.DiscussionID,
|
||||
FileURL: attachment.FileURL,
|
||||
FileName: attachment.FileName,
|
||||
FileType: attachment.FileType,
|
||||
UploadedBy: attachment.UploadedBy,
|
||||
UploadedAt: attachment.UploadedAt,
|
||||
}
|
||||
discussionResp.Attachments = append(discussionResp.Attachments, attachmentResp)
|
||||
}
|
||||
|
||||
discussions = append(discussions, discussionResp)
|
||||
}
|
||||
|
||||
return &contract.OutgoingLetterApprovalDiscussionsResponse{
|
||||
Approvals: approvals,
|
||||
Discussions: discussions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper function to extract user IDs from mentions
|
||||
func extractMentionedUserIDs(mentions map[string]interface{}) []uuid.UUID {
|
||||
var userIDs []uuid.UUID
|
||||
|
||||
if mentions == nil {
|
||||
return userIDs
|
||||
}
|
||||
|
||||
if userIDsInterface, ok := mentions["user_ids"]; ok {
|
||||
if userIDsList, ok := userIDsInterface.([]interface{}); ok {
|
||||
for _, id := range userIDsList {
|
||||
if idStr, ok := id.(string); ok {
|
||||
if userID, err := uuid.Parse(idStr); err == nil {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.OutgoingLetterResponse {
|
||||
resp := &contract.OutgoingLetterResponse{
|
||||
ID: letter.ID,
|
||||
@@ -565,15 +866,36 @@ func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.Outgoi
|
||||
if len(letter.Recipients) > 0 {
|
||||
resp.Recipients = make([]contract.OutgoingLetterRecipientResponse, len(letter.Recipients))
|
||||
for i, recipient := range letter.Recipients {
|
||||
resp.Recipients[i] = contract.OutgoingLetterRecipientResponse{
|
||||
ID: recipient.ID,
|
||||
Name: recipient.RecipientName,
|
||||
Email: recipient.RecipientEmail,
|
||||
Position: recipient.RecipientPosition,
|
||||
Institution: recipient.RecipientInstitution,
|
||||
IsPrimary: recipient.IsPrimary,
|
||||
CreatedAt: recipient.CreatedAt,
|
||||
recipResp := contract.OutgoingLetterRecipientResponse{
|
||||
ID: recipient.ID,
|
||||
LetterID: recipient.LetterID,
|
||||
UserID: recipient.UserID,
|
||||
DepartmentID: recipient.DepartmentID,
|
||||
IsPrimary: recipient.IsPrimary,
|
||||
Status: recipient.Status,
|
||||
ReadAt: recipient.ReadAt,
|
||||
Flag: recipient.Flag,
|
||||
IsArchived: recipient.IsArchived,
|
||||
CreatedAt: recipient.CreatedAt,
|
||||
}
|
||||
|
||||
if recipient.User != nil {
|
||||
recipResp.User = &contract.UserResponse{
|
||||
ID: recipient.User.ID,
|
||||
Name: recipient.User.Name,
|
||||
Email: recipient.User.Email,
|
||||
}
|
||||
}
|
||||
|
||||
if recipient.Department != nil {
|
||||
recipResp.Department = &contract.DepartmentResponse{
|
||||
ID: recipient.Department.ID,
|
||||
Name: recipient.Department.Name,
|
||||
Code: recipient.Department.Code,
|
||||
}
|
||||
}
|
||||
|
||||
resp.Recipients[i] = recipResp
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,708 @@
|
||||
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: fmt.Sprintf("User-%s", userCtx.UserID.String()[:8]),
|
||||
},
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user