first commit
This commit is contained in:
427
backend/internal/application/services/note_service.go
Normal file
427
backend/internal/application/services/note_service.go
Normal file
@@ -0,0 +1,427 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/noteapp/backend/internal/application/dto"
|
||||
"github.com/noteapp/backend/internal/domain/entities"
|
||||
"github.com/noteapp/backend/internal/domain/repositories"
|
||||
"github.com/noteapp/backend/internal/infrastructure/security"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// NoteService handles note operations
|
||||
type NoteService struct {
|
||||
noteRepo repositories.NoteRepository
|
||||
categoryRepo repositories.CategoryRepository
|
||||
membershipRepo repositories.MembershipRepository
|
||||
revisionRepo repositories.NoteRevisionRepository
|
||||
spaceRepo repositories.SpaceRepository
|
||||
permissionService *PermissionService
|
||||
passwordHasher *security.PasswordHasher
|
||||
}
|
||||
|
||||
// NewNoteService creates a new note service
|
||||
func NewNoteService(
|
||||
noteRepo repositories.NoteRepository,
|
||||
categoryRepo repositories.CategoryRepository,
|
||||
membershipRepo repositories.MembershipRepository,
|
||||
revisionRepo repositories.NoteRevisionRepository,
|
||||
spaceRepo repositories.SpaceRepository,
|
||||
permissionService *PermissionService,
|
||||
passwordHasher *security.PasswordHasher,
|
||||
) *NoteService {
|
||||
return &NoteService{
|
||||
noteRepo: noteRepo,
|
||||
categoryRepo: categoryRepo,
|
||||
membershipRepo: membershipRepo,
|
||||
revisionRepo: revisionRepo,
|
||||
spaceRepo: spaceRepo,
|
||||
permissionService: permissionService,
|
||||
passwordHasher: passwordHasher,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NoteService) toDisplayNoteDTO(note *entities.Note) *dto.NoteDTO {
|
||||
noteDTO := dto.NewNoteDTO(note)
|
||||
if note.IsPasswordProtected {
|
||||
noteDTO.Content = ""
|
||||
}
|
||||
return noteDTO
|
||||
}
|
||||
|
||||
// CreateNote creates a new note
|
||||
func (s *NoteService) CreateNote(ctx context.Context, spaceID, userID bson.ObjectID, req *dto.CreateNoteRequest) (*dto.NoteDTO, error) {
|
||||
// Verify user has access to space
|
||||
membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID)
|
||||
if err != nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
_ = membership
|
||||
hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "note.create")
|
||||
if permErr != nil {
|
||||
return nil, permErr
|
||||
}
|
||||
if !hasPermission {
|
||||
return nil, errors.New("insufficient permissions")
|
||||
}
|
||||
|
||||
var categoryID *bson.ObjectID
|
||||
if req.CategoryID != nil {
|
||||
id, _ := bson.ObjectIDFromHex(*req.CategoryID)
|
||||
categoryID = &id
|
||||
}
|
||||
|
||||
note := &entities.Note{
|
||||
SpaceID: spaceID,
|
||||
CategoryID: categoryID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Content: req.Content,
|
||||
Tags: req.Tags,
|
||||
IsPinned: req.IsPinned,
|
||||
IsFavorite: req.IsFavorite,
|
||||
IsPublic: req.IsPublic,
|
||||
CreatedBy: userID,
|
||||
UpdatedBy: userID,
|
||||
}
|
||||
|
||||
notePassword := strings.TrimSpace(req.NotePassword)
|
||||
if notePassword != "" {
|
||||
if len(notePassword) < 4 {
|
||||
return nil, errors.New("note password must be at least 4 characters")
|
||||
}
|
||||
if s.passwordHasher == nil {
|
||||
return nil, errors.New("password hasher unavailable")
|
||||
}
|
||||
hash, err := s.passwordHasher.HashPassword(notePassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
note.PasswordHash = hash
|
||||
note.IsPasswordProtected = true
|
||||
}
|
||||
|
||||
if err := s.noteRepo.CreateNote(ctx, note); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewNoteDTO(note), nil
|
||||
}
|
||||
|
||||
// GetNote retrieves a note (with space authorization check)
|
||||
func (s *NoteService) GetNote(ctx context.Context, noteID, spaceID, userID bson.ObjectID) (*dto.NoteDTO, error) {
|
||||
// Verify user has access to space
|
||||
if _, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID); err != nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
note, err := s.noteRepo.GetNoteByID(ctx, noteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify note belongs to this space
|
||||
if note.SpaceID != spaceID {
|
||||
return nil, errors.New("note not found in this space")
|
||||
}
|
||||
|
||||
// Update viewed time
|
||||
now := time.Now()
|
||||
note.ViewedAt = &now
|
||||
_ = s.noteRepo.UpdateNote(ctx, note)
|
||||
|
||||
return s.toDisplayNoteDTO(note), nil
|
||||
}
|
||||
|
||||
// GetNotesBySpace retrieves notes in a space
|
||||
func (s *NoteService) GetNotesBySpace(ctx context.Context, spaceID, userID bson.ObjectID, skip, limit int) ([]*dto.NoteDTO, error) {
|
||||
// Verify user has access to space
|
||||
if _, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID); err != nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
notes, err := s.noteRepo.GetNotesBySpaceID(ctx, spaceID, skip, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var noteDTOs []*dto.NoteDTO
|
||||
for _, note := range notes {
|
||||
noteDTOs = append(noteDTOs, s.toDisplayNoteDTO(note))
|
||||
}
|
||||
|
||||
return noteDTOs, nil
|
||||
}
|
||||
|
||||
// GetPublicNotesBySpace returns notes for a public space without requiring auth
|
||||
func (s *NoteService) GetPublicNotesBySpace(ctx context.Context, spaceID bson.ObjectID, skip, limit int) ([]*dto.NoteDTO, error) {
|
||||
space, err := s.spaceRepo.GetSpaceByID(ctx, spaceID)
|
||||
if err != nil {
|
||||
return nil, errors.New("space not found")
|
||||
}
|
||||
if !space.IsPublic {
|
||||
return nil, errors.New("space is not public")
|
||||
}
|
||||
|
||||
notes, err := s.noteRepo.GetPublicNotesBySpaceID(ctx, spaceID, skip, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var noteDTOs []*dto.NoteDTO
|
||||
for _, note := range notes {
|
||||
noteDTOs = append(noteDTOs, s.toDisplayNoteDTO(note))
|
||||
}
|
||||
return noteDTOs, nil
|
||||
}
|
||||
|
||||
// GetPublicNoteBySpaceAndID returns a single public note in a public space without requiring auth
|
||||
func (s *NoteService) GetPublicNoteBySpaceAndID(ctx context.Context, spaceID, noteID bson.ObjectID) (*dto.NoteDTO, error) {
|
||||
space, err := s.spaceRepo.GetSpaceByID(ctx, spaceID)
|
||||
if err != nil {
|
||||
return nil, errors.New("space not found")
|
||||
}
|
||||
if !space.IsPublic {
|
||||
return nil, errors.New("space is not public")
|
||||
}
|
||||
|
||||
note, err := s.noteRepo.GetNoteByID(ctx, noteID)
|
||||
if err != nil {
|
||||
return nil, errors.New("note not found")
|
||||
}
|
||||
if note.SpaceID != spaceID {
|
||||
return nil, errors.New("note not found")
|
||||
}
|
||||
if !note.IsPublic {
|
||||
return nil, errors.New("note is not public")
|
||||
}
|
||||
|
||||
return s.toDisplayNoteDTO(note), nil
|
||||
}
|
||||
|
||||
// SearchNotes performs full-text search on notes in a space
|
||||
func (s *NoteService) SearchNotes(ctx context.Context, spaceID, userID bson.ObjectID, query string) ([]*dto.NoteDTO, error) {
|
||||
// Verify user has access to space
|
||||
if _, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID); err != nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
notes, err := s.noteRepo.SearchNotes(ctx, spaceID, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var noteDTOs []*dto.NoteDTO
|
||||
for _, note := range notes {
|
||||
noteDTOs = append(noteDTOs, s.toDisplayNoteDTO(note))
|
||||
}
|
||||
|
||||
return noteDTOs, nil
|
||||
}
|
||||
|
||||
// UpdateNote updates a note
|
||||
func (s *NoteService) UpdateNote(ctx context.Context, noteID, spaceID, userID bson.ObjectID, req *dto.UpdateNoteRequest) (*dto.NoteDTO, error) {
|
||||
// Verify user has access to space
|
||||
membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID)
|
||||
if err != nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
_ = membership
|
||||
hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "note.edit")
|
||||
if permErr != nil {
|
||||
return nil, permErr
|
||||
}
|
||||
if !hasPermission {
|
||||
return nil, errors.New("insufficient permissions")
|
||||
}
|
||||
|
||||
note, err := s.noteRepo.GetNoteByID(ctx, noteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify note belongs to this space
|
||||
if note.SpaceID != spaceID {
|
||||
return nil, errors.New("note not found in this space")
|
||||
}
|
||||
|
||||
// Create revision before updating
|
||||
if s.revisionRepo != nil {
|
||||
revision := &entities.NoteRevision{
|
||||
NoteID: note.ID,
|
||||
SpaceID: spaceID,
|
||||
Title: note.Title,
|
||||
Content: note.Content,
|
||||
ChangedBy: userID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
_ = s.revisionRepo.CreateRevision(ctx, revision)
|
||||
}
|
||||
|
||||
// Update fields
|
||||
if req.Title != "" {
|
||||
note.Title = req.Title
|
||||
}
|
||||
if req.Content != "" {
|
||||
note.Content = req.Content
|
||||
}
|
||||
if req.Description != nil {
|
||||
note.Description = *req.Description
|
||||
}
|
||||
if req.Tags != nil {
|
||||
note.Tags = req.Tags
|
||||
}
|
||||
if req.CategoryID != nil {
|
||||
id, _ := bson.ObjectIDFromHex(*req.CategoryID)
|
||||
note.CategoryID = &id
|
||||
}
|
||||
if req.IsPinned != nil {
|
||||
note.IsPinned = *req.IsPinned
|
||||
}
|
||||
if req.IsFavorite != nil {
|
||||
note.IsFavorite = *req.IsFavorite
|
||||
}
|
||||
if req.IsPublic != nil {
|
||||
note.IsPublic = *req.IsPublic
|
||||
}
|
||||
if req.NotePassword != nil {
|
||||
if s.passwordHasher == nil {
|
||||
return nil, errors.New("password hasher unavailable")
|
||||
}
|
||||
notePassword := strings.TrimSpace(*req.NotePassword)
|
||||
if notePassword == "" {
|
||||
note.PasswordHash = ""
|
||||
note.IsPasswordProtected = false
|
||||
} else {
|
||||
if len(notePassword) < 4 {
|
||||
return nil, errors.New("note password must be at least 4 characters")
|
||||
}
|
||||
hash, hashErr := s.passwordHasher.HashPassword(notePassword)
|
||||
if hashErr != nil {
|
||||
return nil, hashErr
|
||||
}
|
||||
note.PasswordHash = hash
|
||||
note.IsPasswordProtected = true
|
||||
}
|
||||
}
|
||||
|
||||
note.UpdatedBy = userID
|
||||
|
||||
if err := s.noteRepo.UpdateNote(ctx, note); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dto.NewNoteDTO(note), nil
|
||||
}
|
||||
|
||||
// UnlockNote verifies a protected note password and returns full note content for authenticated users.
|
||||
func (s *NoteService) UnlockNote(ctx context.Context, noteID, spaceID, userID bson.ObjectID, password string) (*dto.NoteDTO, error) {
|
||||
if _, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID); err != nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
note, err := s.noteRepo.GetNoteByID(ctx, noteID)
|
||||
if err != nil {
|
||||
return nil, errors.New("note not found")
|
||||
}
|
||||
if note.SpaceID != spaceID {
|
||||
return nil, errors.New("note not found in this space")
|
||||
}
|
||||
if !note.IsPasswordProtected {
|
||||
return dto.NewNoteDTO(note), nil
|
||||
}
|
||||
if strings.TrimSpace(password) == "" {
|
||||
return nil, errors.New("password is required")
|
||||
}
|
||||
if s.passwordHasher == nil {
|
||||
return nil, errors.New("password hasher unavailable")
|
||||
}
|
||||
matched, verifyErr := s.passwordHasher.VerifyPassword(password, note.PasswordHash)
|
||||
if verifyErr != nil || !matched {
|
||||
return nil, errors.New("invalid note password")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
note.ViewedAt = &now
|
||||
_ = s.noteRepo.UpdateNote(ctx, note)
|
||||
|
||||
return dto.NewNoteDTO(note), nil
|
||||
}
|
||||
|
||||
// UnlockPublicNote verifies a protected public note password and returns full note content.
|
||||
func (s *NoteService) UnlockPublicNote(ctx context.Context, spaceID, noteID bson.ObjectID, password string) (*dto.NoteDTO, error) {
|
||||
space, err := s.spaceRepo.GetSpaceByID(ctx, spaceID)
|
||||
if err != nil {
|
||||
return nil, errors.New("space not found")
|
||||
}
|
||||
if !space.IsPublic {
|
||||
return nil, errors.New("space is not public")
|
||||
}
|
||||
|
||||
note, err := s.noteRepo.GetNoteByID(ctx, noteID)
|
||||
if err != nil {
|
||||
return nil, errors.New("note not found")
|
||||
}
|
||||
if note.SpaceID != spaceID || !note.IsPublic {
|
||||
return nil, errors.New("note not found")
|
||||
}
|
||||
if !note.IsPasswordProtected {
|
||||
return dto.NewNoteDTO(note), nil
|
||||
}
|
||||
if strings.TrimSpace(password) == "" {
|
||||
return nil, errors.New("password is required")
|
||||
}
|
||||
if s.passwordHasher == nil {
|
||||
return nil, errors.New("password hasher unavailable")
|
||||
}
|
||||
matched, verifyErr := s.passwordHasher.VerifyPassword(password, note.PasswordHash)
|
||||
if verifyErr != nil || !matched {
|
||||
return nil, errors.New("invalid note password")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
note.ViewedAt = &now
|
||||
_ = s.noteRepo.UpdateNote(ctx, note)
|
||||
|
||||
return dto.NewNoteDTO(note), nil
|
||||
}
|
||||
|
||||
// DeleteNote deletes a note
|
||||
func (s *NoteService) DeleteNote(ctx context.Context, noteID, spaceID, userID bson.ObjectID) error {
|
||||
// Verify user has access to space
|
||||
membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID)
|
||||
if err != nil {
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
_ = membership
|
||||
hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "note.delete")
|
||||
if permErr != nil {
|
||||
return permErr
|
||||
}
|
||||
if !hasPermission {
|
||||
return errors.New("insufficient permissions")
|
||||
}
|
||||
|
||||
note, err := s.noteRepo.GetNoteByID(ctx, noteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify note belongs to this space
|
||||
if note.SpaceID != spaceID {
|
||||
return errors.New("note not found in this space")
|
||||
}
|
||||
|
||||
return s.noteRepo.DeleteNote(ctx, noteID)
|
||||
}
|
||||
|
||||
func (s *NoteService) hasSpacePermission(ctx context.Context, userID, spaceID bson.ObjectID, action string) (bool, error) {
|
||||
if s.permissionService == nil {
|
||||
return false, errors.New("permission service unavailable")
|
||||
}
|
||||
return s.permissionService.HasSpacePermission(ctx, userID, spaceID, action)
|
||||
}
|
||||
Reference in New Issue
Block a user