Skip to content

Commit 2ff1897

Browse files
committed
Add ability for users to delete their acounts with their data #322
1 parent 570b65f commit 2ff1897

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+756
-287
lines changed

api/cmd/experiments/main.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ func main() {
3434
logger := container.Logger()
3535

3636
logger.Info("Starting experiments")
37-
deleteContacts(container)
3837
}
3938

4039
func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {

api/go.mod

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ require (
4949
go.opentelemetry.io/otel/trace v1.31.0
5050
google.golang.org/api v0.200.0
5151
google.golang.org/protobuf v1.35.1
52-
gorm.io/datatypes v1.2.4
5352
gorm.io/driver/postgres v1.5.7
5453
gorm.io/gorm v1.25.12
5554
gorm.io/plugin/opentelemetry v0.1.8
@@ -66,7 +65,6 @@ require (
6665
cloud.google.com/go/monitoring v1.21.1 // indirect
6766
cloud.google.com/go/storage v1.43.0 // indirect
6867
cloud.google.com/go/trace v1.11.1 // indirect
69-
filippo.io/edwards25519 v1.1.0 // indirect
7068
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.3 // indirect
7169
github.com/KyleBanks/depth v1.2.1 // indirect
7270
github.com/Masterminds/goutils v1.1.1 // indirect
@@ -85,7 +83,6 @@ require (
8583
github.com/go-openapi/jsonreference v0.21.0 // indirect
8684
github.com/go-openapi/spec v0.21.0 // indirect
8785
github.com/go-openapi/swag v0.23.0 // indirect
88-
github.com/go-sql-driver/mysql v1.8.1 // indirect
8986
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
9087
github.com/golang/protobuf v1.5.4 // indirect
9188
github.com/google/s2a-go v0.1.8 // indirect
@@ -160,5 +157,4 @@ require (
160157
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
161158
google.golang.org/grpc v1.67.1 // indirect
162159
gopkg.in/yaml.v3 v3.0.1 // indirect
163-
gorm.io/driver/mysql v1.5.6 // indirect
164160
)

api/go.sum

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyX
2525
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
2626
cloud.google.com/go/trace v1.11.1 h1:UNqdP+HYYtnm6lb91aNA5JQ0X14GnxkABGlfz2PzPew=
2727
cloud.google.com/go/trace v1.11.1/go.mod h1:IQKNQuBzH72EGaXEodKlNJrWykGZxet2zgjtS60OtjA=
28-
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
29-
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3028
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
3129
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
3230
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -116,9 +114,6 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z
116114
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
117115
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
118116
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
119-
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
120-
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
121-
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
122117
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
123118
github.com/gofiber/contrib/otelfiber v1.0.10 h1:Bu28Pi4pfYmGfIc/9+sNaBbFwTHGY/zpSIK5jBxuRtM=
124119
github.com/gofiber/contrib/otelfiber v1.0.10/go.mod h1:jN6AvS1HolDHTQHFURsV+7jSX96FpXYeKH6nmkq8AIw=
@@ -130,10 +125,6 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
130125
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
131126
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
132127
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
133-
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
134-
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
135-
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
136-
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
137128
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
138129
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
139130
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -245,8 +236,6 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
245236
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
246237
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
247238
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
248-
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
249-
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
250239
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
251240
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
252241
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
@@ -528,17 +517,10 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
528517
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
529518
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
530519
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
531-
gorm.io/datatypes v1.2.4 h1:uZmGAcK/QZ0uyfCuVg0VQY1ZmV9h1fuG0tMwKByO1z4=
532-
gorm.io/datatypes v1.2.4/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
533-
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
534-
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
535520
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
536521
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
537522
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
538523
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
539-
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
540-
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
541-
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
542524
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
543525
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
544526
gorm.io/plugin/opentelemetry v0.1.8 h1:uX3deb3w71mufbx8iY9buiGh+4HJjhItRNisZIy1fDY=

api/pkg/di/container.go

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -696,16 +696,6 @@ func (container *Container) MessageThreadRepository() (repository repositories.M
696696
)
697697
}
698698

699-
// EventRepository creates a new instance of repositories.EventRepository
700-
func (container *Container) EventRepository() (repository repositories.EventRepository) {
701-
container.logger.Debug("creating GORM repositories.EventRepository")
702-
return repositories.NewGormEventRepository(
703-
container.Logger(),
704-
container.Tracer(),
705-
container.DB(),
706-
)
707-
}
708-
709699
// HeartbeatMonitorRepository creates a new instance of repositories.HeartbeatMonitorRepository
710700
func (container *Container) HeartbeatMonitorRepository() (repository repositories.HeartbeatMonitorRepository) {
711701
container.logger.Debug("creating GORM repositories.HeartbeatMonitorRepository")
@@ -716,16 +706,6 @@ func (container *Container) HeartbeatMonitorRepository() (repository repositorie
716706
)
717707
}
718708

719-
// EventListenerLogRepository creates a new instance of repositories.EventListenerLogRepository
720-
func (container *Container) EventListenerLogRepository() (repository repositories.EventListenerLogRepository) {
721-
container.logger.Debug("creating GORM repositories.EventListenerLogRepository")
722-
return repositories.NewGormEventListenerLogRepository(
723-
container.Logger(),
724-
container.Tracer(),
725-
container.DB(),
726-
)
727-
}
728-
729709
// HeartbeatService creates a new instance of services.HeartbeatService
730710
func (container *Container) HeartbeatService() (service *services.HeartbeatService) {
731711
container.logger.Debug(fmt.Sprintf("creating %T", service))
@@ -861,6 +841,7 @@ func (container *Container) UserService() (service *services.UserService) {
861841
container.MarketingService(),
862842
container.LemonsqueezyClient(),
863843
container.EventDispatcher(),
844+
container.FirebaseAuthClient(),
864845
)
865846
}
866847

api/pkg/entities/user.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import (
99
// UserID is the ID of a user
1010
type UserID string
1111

12+
// String returns the string representation of a UserID
13+
func (id UserID) String() string {
14+
return string(id)
15+
}
16+
1217
// SubscriptionName is the name of the subscription
1318
type SubscriptionName string
1419

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
)
8+
9+
// UserAccountDeleted is raised when a user's account is deleted.
10+
const UserAccountDeleted = "user.account.deleted"
11+
12+
// UserAccountDeletedPayload stores the data for the UserAccountDeletedPayload event
13+
type UserAccountDeletedPayload struct {
14+
UserID entities.UserID `json:"user_id"`
15+
Timestamp time.Time `json:"timestamp"`
16+
}

api/pkg/handlers/user_handler.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func NewUserHandler(
4141
func (h *UserHandler) RegisterRoutes(router fiber.Router) {
4242
router.Get("/users/me", h.Show)
4343
router.Put("/users/me", h.Update)
44+
router.Delete("/users/me", h.Delete)
4445
router.Delete("/users/:userID/api-keys", h.DeleteAPIKey)
4546
router.Put("/users/:userID/notifications", h.UpdateNotifications)
4647
router.Get("/users/subscription-update-url", h.subscriptionUpdateURL)
@@ -121,6 +122,30 @@ func (h *UserHandler) Update(c *fiber.Ctx) error {
121122
return h.responseOK(c, "user updated successfully", user)
122123
}
123124

125+
// Delete an entities.User
126+
// @Summary Delete a user
127+
// @Description Deletes the currently authenticated user together with all their data.
128+
// @Security ApiKeyAuth
129+
// @Tags Users
130+
// @Accept json
131+
// @Produce json
132+
// @Success 201 {object} responses.NoContent
133+
// @Failure 401 {object} responses.Unauthorized
134+
// @Failure 500 {object} responses.InternalServerError
135+
// @Router /users/me [delete]
136+
func (h *UserHandler) Delete(c *fiber.Ctx) error {
137+
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
138+
defer span.End()
139+
140+
if err := h.service.Delete(ctx, c.OriginalURL(), h.userIDFomContext(c)); err != nil {
141+
msg := fmt.Sprintf("cannot delete user user with ID [%s]", h.userIDFomContext(c))
142+
ctxLogger.Error(stacktrace.Propagate(err, msg))
143+
return h.responseInternalServerError(c)
144+
}
145+
146+
return h.responseNoContent(c, "user deleted successfully")
147+
}
148+
124149
// UpdateNotifications an entities.User
125150
// @Summary Update notification settings
126151
// @Description Update the email notification settings for a user

api/pkg/listeners/billing_listener.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func NewBillingListener(
3434

3535
return l, map[string]events.EventListener{
3636
events.EventTypeMessageAPISent: l.OnMessageAPISent,
37+
events.UserAccountDeleted: l.onUserAccountDeleted,
3738
events.EventTypeMessagePhoneReceived: l.OnMessagePhoneReceived,
3839
}
3940
}
@@ -75,3 +76,21 @@ func (listener *BillingListener) OnMessagePhoneReceived(ctx context.Context, eve
7576

7677
return nil
7778
}
79+
80+
func (listener *BillingListener) onUserAccountDeleted(ctx context.Context, event cloudevents.Event) error {
81+
ctx, span := listener.tracer.Start(ctx)
82+
defer span.End()
83+
84+
var payload events.UserAccountDeletedPayload
85+
if err := event.DataAs(&payload); err != nil {
86+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
87+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
88+
}
89+
90+
if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil {
91+
msg := fmt.Sprintf("cannot delete [entities.BillingUsage] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())
92+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
93+
}
94+
95+
return nil
96+
}

api/pkg/listeners/discord_listener.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func NewDiscordListener(
3232

3333
return l, map[string]events.EventListener{
3434
events.EventTypeMessagePhoneReceived: l.OnMessagePhoneReceived,
35+
events.UserAccountDeleted: l.onUserAccountDeleted,
3536
}
3637
}
3738

@@ -53,3 +54,21 @@ func (listener *DiscordListener) OnMessagePhoneReceived(ctx context.Context, eve
5354

5455
return nil
5556
}
57+
58+
func (listener *DiscordListener) onUserAccountDeleted(ctx context.Context, event cloudevents.Event) error {
59+
ctx, span := listener.tracer.Start(ctx)
60+
defer span.End()
61+
62+
var payload events.UserAccountDeletedPayload
63+
if err := event.DataAs(&payload); err != nil {
64+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
65+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
66+
}
67+
68+
if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil {
69+
msg := fmt.Sprintf("cannot delete [entities.Discord] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())
70+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
71+
}
72+
73+
return nil
74+
}

api/pkg/listeners/heartbeat_listener.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func NewHeartbeatListener(
3737
events.EventTypePhoneDeleted: l.onPhoneDeleted,
3838
events.EventTypePhoneHeartbeatCheck: l.onPhoneHeartbeatCheck,
3939
events.EventTypePhoneHeartbeatOffline: l.onPhoneHeartbeatOffline,
40+
events.UserAccountDeleted: l.onUserAccountDeleted,
4041
}
4142
}
4243

@@ -130,3 +131,21 @@ func (listener *HeartbeatListener) onPhoneHeartbeatOffline(ctx context.Context,
130131

131132
return nil
132133
}
134+
135+
func (listener *HeartbeatListener) onUserAccountDeleted(ctx context.Context, event cloudevents.Event) error {
136+
ctx, span := listener.tracer.Start(ctx)
137+
defer span.End()
138+
139+
var payload events.UserAccountDeletedPayload
140+
if err := event.DataAs(&payload); err != nil {
141+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
142+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
143+
}
144+
145+
if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil {
146+
msg := fmt.Sprintf("cannot delete [entities.Heartbeat] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())
147+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
148+
}
149+
150+
return nil
151+
}

0 commit comments

Comments
 (0)