all repos

onasty @ 36f59cd12d0992b16f20057c23cfe0e3c338e637

a one-time notes service

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

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