all repos

onasty @ babda7d822d93649597a4892e0dada0724e215b7

a one-time notes service

onasty/internal/service/notesrv/notesrv.go (view raw)

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
feat: notes manipulations for the note authors (#117)..., 1 year ago
1
package notesrv
2
3
import (
4
	"context"
5
	"errors"
6
	"log/slog"
7
	"time"
8
9
	"github.com/gofrs/uuid/v5"
10
	"github.com/olexsmir/onasty/internal/dtos"
11
	"github.com/olexsmir/onasty/internal/hasher"
12
	"github.com/olexsmir/onasty/internal/models"
13
	"github.com/olexsmir/onasty/internal/store/psql/noterepo"
14
	"github.com/olexsmir/onasty/internal/store/rdb/notecache"
15
)
16
17
var ErrNotePasswordNotProvided = errors.New("note: password was not provided")
18
19
type NoteServicer interface {
20
	// Create creates note
21
	// if slug is empty it will be generated, otherwise used as is
22
	// if userID is empty it means user isn't authorized so it will be used
23
	Create(ctx context.Context, note dtos.CreateNote, userID uuid.UUID) (dtos.NoteSlug, error)
24
25
	// GetBySlugAndRemoveIfNeeded returns note by slug, and removes if if needed.
26
	// If notes is not found returns [models.ErrNoteNotFound].
27
	GetBySlugAndRemoveIfNeeded(
28
		ctx context.Context,
29
		input GetNoteBySlugInput,
30
	) (dtos.GetNote, error)
31
32
	// GetAllByAuthorID returns all notes by author id.
33
	GetAllByAuthorID(
34
		ctx context.Context,
35
		authorID uuid.UUID,
36
	) ([]dtos.NoteDetailed, error)
37
38
	// UpdateExpirationTimeSettings updates expiresAt and burnBeforeExpiration.
39
	// If notes is not found returns [models.ErrNoteNotFound].
40
	UpdateExpirationTimeSettings(
41
		ctx context.Context,
42
		patchData dtos.PatchNote,
43
		slug dtos.NoteSlug,
44
		userID uuid.UUID,
45
	) error
46
47
	// UpdatePassword sets or updates notes password.
48
	// If notes is not found returns [models.ErrNoteNotFound].
49
	UpdatePassword(ctx context.Context, slug dtos.NoteSlug, passwd string, userID uuid.UUID) error
50
51
	// DeleteBySlug deletes note by slug
52
	DeleteBySlug(ctx context.Context, slug dtos.NoteSlug, userID uuid.UUID) error
53
}
54
55
var _ NoteServicer = (*NoteSrv)(nil)
56
57
type NoteSrv struct {
58
	noterepo noterepo.NoteStorer
59
	hasher   hasher.Hasher
60
	cache    notecache.NoteCacher
61
}
62
63
func New(noterepo noterepo.NoteStorer, hasher hasher.Hasher, cache notecache.NoteCacher) *NoteSrv {
64
	return &NoteSrv{
65
		noterepo: noterepo,
66
		hasher:   hasher,
67
		cache:    cache,
68
	}
69
}
70
71
func (n *NoteSrv) Create(
72
	ctx context.Context,
73
	inp dtos.CreateNote,
74
	userID uuid.UUID,
75
) (dtos.NoteSlug, error) {
76
	slog.DebugContext(ctx, "creating", "inp", inp)
77
78
	if inp.Slug == "" {
79
		inp.Slug = uuid.Must(uuid.NewV4()).String()
80
	}
81
82
	if inp.Password != "" {
83
		hashedPassword, err := n.hasher.Hash(inp.Password)
84
		if err != nil {
85
			return "", err
86
		}
87
		inp.Password = hashedPassword
88
	}
89
90
	//nolint:exhaustruct // ID - cannot be predicted, and ReadAt will be set on read
91
	note := models.Note{
92
		Content:              inp.Content,
93
		Slug:                 inp.Slug,
94
		Password:             inp.Password,
95
		BurnBeforeExpiration: inp.BurnBeforeExpiration,
96
		CreatedAt:            inp.CreatedAt,
97
		ExpiresAt:            inp.ExpiresAt,
98
	}
99
	if err := note.Validate(); err != nil {
100
		return "", err
101
	}
102
103
	if err := n.noterepo.Create(ctx, note); err != nil {
104
		return "", err
105
	}
106
107
	if !userID.IsNil() {
108
		if err := n.noterepo.SetAuthorIDBySlug(ctx, inp.Slug, userID); err != nil {
109
			return "", err
110
		}
111
	}
112
113
	return inp.Slug, nil
114
}
115
116
func (n *NoteSrv) GetBySlugAndRemoveIfNeeded(
117
	ctx context.Context,
118
	inp GetNoteBySlugInput,
119
) (dtos.GetNote, error) {
120
	note, err := n.getNote(ctx, inp)
121
	if err != nil {
122
		return dtos.GetNote{}, err
123
	}
124
125
	if note.IsExpired() {
126
		return dtos.GetNote{}, models.ErrNoteExpired
127
	}
128
129
	respNote := dtos.GetNote{
130
		Content:   note.Content,
131
		ReadAt:    note.ReadAt,
132
		CreatedAt: note.CreatedAt,
133
		ExpiresAt: note.ExpiresAt,
134
	}
135
136
	// since not every note should be burn before expiration
137
	// we return early if it's not
138
	if note.ShouldBeBurnt() {
139
		return respNote, nil
140
	}
141
142
	return respNote, n.noterepo.RemoveBySlug(ctx, inp.Slug, time.Now())
143
}
144
145
func (n *NoteSrv) GetAllByAuthorID(
146
	ctx context.Context,
147
	authorID uuid.UUID,
148
) ([]dtos.NoteDetailed, error) {
149
	notes, err := n.noterepo.GetAllByAuthorID(ctx, authorID)
150
	if err != nil {
151
		return nil, err
152
	}
153
154
	var resNotes []dtos.NoteDetailed
155
	for _, note := range notes {
156
		resNotes = append(resNotes, dtos.NoteDetailed{
157
			Content:              note.Content,
158
			Slug:                 note.Slug,
159
			BurnBeforeExpiration: note.BurnBeforeExpiration,
160
			HasPassword:          note.Password != "",
161
			CreatedAt:            note.CreatedAt,
162
			ExpiresAt:            note.ExpiresAt,
163
			ReadAt:               note.ReadAt,
164
		})
165
	}
166
167
	return resNotes, nil
168
}
169
170
func (n *NoteSrv) UpdateExpirationTimeSettings(
171
	ctx context.Context,
172
	patchData dtos.PatchNote,
173
	slug dtos.NoteSlug,
174
	userID uuid.UUID,
175
) error {
176
	return n.noterepo.UpdateExpirationTimeSettingsBySlug(ctx, slug, patchData, userID)
177
}
178
179
func (n *NoteSrv) UpdatePassword(
180
	ctx context.Context,
181
	slug dtos.NoteSlug,
182
	passwd string,
183
	userID uuid.UUID,
184
) error {
185
	if len(passwd) == 0 {
186
		return ErrNotePasswordNotProvided
187
	}
188
189
	hashedPassword, err := n.hasher.Hash(passwd)
190
	if err != nil {
191
		return err
192
	}
193
194
	return n.noterepo.UpdatePasswordBySlug(ctx, slug, userID, hashedPassword)
195
}
196
197
func (n *NoteSrv) DeleteBySlug(
198
	ctx context.Context,
199
	slug dtos.NoteSlug,
200
	authorID uuid.UUID,
201
) error {
202
	return n.noterepo.DeleteNoteBySlug(ctx, slug, authorID)
203
}
204
205
func (n *NoteSrv) getNote(ctx context.Context, inp GetNoteBySlugInput) (models.Note, error) {
206
	if r, err := n.cache.GetNote(ctx, inp.Slug); err == nil {
207
		return r, nil
208
	}
209
210
	note, err := n.getNoteFromDBasedOnInput(ctx, inp)
211
	if err != nil {
212
		return models.Note{}, err
213
	}
214
215
	if !note.IsRead() {
216
		if err = n.cache.SetNote(ctx, inp.Slug, note); err != nil {
217
			slog.ErrorContext(ctx, "notecache", "err", err)
218
		}
219
	}
220
221
	return note, err
222
}
223
224
func (n *NoteSrv) getNoteFromDBasedOnInput(
225
	ctx context.Context,
226
	inp GetNoteBySlugInput,
227
) (models.Note, error) {
228
	if inp.HasPassword() {
229
		hashedPassword, err := n.hasher.Hash(inp.Password)
230
		if err != nil {
231
			return models.Note{}, err
232
		}
233
234
		return n.noterepo.GetBySlugAndPassword(ctx, inp.Slug, hashedPassword)
235
	}
236
	return n.noterepo.GetBySlug(ctx, inp.Slug)
237
}