feat: change email, export my data, delete account (with data anonymization)
This commit is contained in:
1 parent
bb191c389a
commit
10a66cd979
123 files changed
+10789
-76
No files matched your search
@@ -94,6 +94,7 @@ type Repository interface {
|
||||
Get(id uint32) (Article, error)
|
||||
GetByName(name string) (Article, error)
|
||||
GetAll() ([]Article, error)
|
||||
ListForAccount(creatorID uint32, authorID *uint32) ([]Article, error)
|
||||
Create(post Article) (Article, error)
|
||||
UpdateBody(id uint32, body string) (Article, error)
|
||||
Update(id uint32, update Update) (Article, error)
|
||||
|
||||
@@ -106,6 +106,12 @@ const (
|
||||
ActionUserChangeRole = "user.change_role"
|
||||
ActionUserTransferOwnership = "user.transfer_ownership"
|
||||
ActionUserRemove = "user.remove"
|
||||
ActionUserRequestEmail = "user.request_email_change"
|
||||
ActionUserResendEmail = "user.resend_email_change"
|
||||
ActionUserCancelEmail = "user.cancel_email_change"
|
||||
ActionUserConfirmEmail = "user.confirm_email_change"
|
||||
ActionUserExportData = "user.export_data"
|
||||
ActionUserDeleteAccount = "user.delete_account"
|
||||
ActionInviteCreate = "invite.create"
|
||||
ActionInviteCancel = "invite.cancel"
|
||||
ActionInviteRenew = "invite.renew"
|
||||
@@ -132,6 +138,7 @@ const (
|
||||
const (
|
||||
SystemActorName = "System"
|
||||
ReasonWrongLogin = "Wrong username or password."
|
||||
ReasonWrongPassword = "Wrong password."
|
||||
MaxUserAgentLength = 255
|
||||
ParamRemoved = "removed"
|
||||
ParamFrom = "from"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package audit
|
||||
|
||||
import "strconv"
|
||||
|
||||
func (e Event) ConcernsOtherPerson(userID uint32, ownAuthorID *uint32) bool {
|
||||
switch e.Target.Type {
|
||||
case TargetUser:
|
||||
return e.Target.ID != formatID(userID)
|
||||
case TargetInvite:
|
||||
return true
|
||||
case TargetAuthor:
|
||||
return ownAuthorID == nil || e.Target.ID != formatID(*ownAuthorID)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatID(id uint32) string {
|
||||
return strconv.FormatUint(uint64(id), 10)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ type Repository interface {
|
||||
DeleteOlderThan(cutoff time.Time) (int, error)
|
||||
DeleteOldest(limit int, before time.Time) (int, error)
|
||||
SizeBytes() (int64, error)
|
||||
AnonymiseUser(userID uint32, ownAuthorID *uint32, name string) error
|
||||
}
|
||||
|
||||
type SettingsRepository interface {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package author
|
||||
|
||||
import "strconv"
|
||||
|
||||
const DeletedName = "Deleted user"
|
||||
|
||||
const DeletedSlugPrefix = "deleted-user-"
|
||||
|
||||
func DeletedSlug(id uint32) string {
|
||||
return DeletedSlugPrefix + strconv.FormatUint(uint64(id), 10)
|
||||
}
|
||||
|
||||
func DeletedSlugAttempt(id uint32, attempt int) string {
|
||||
if attempt <= 1 {
|
||||
return DeletedSlug(id)
|
||||
}
|
||||
return DeletedSlug(id) + "-" + strconv.Itoa(attempt)
|
||||
}
|
||||
|
||||
func IsReservedSlug(candidate string) bool {
|
||||
return len(candidate) >= len(DeletedSlugPrefix) && candidate[:len(DeletedSlugPrefix)] == DeletedSlugPrefix
|
||||
}
|
||||
|
||||
func avoidReserved(candidate string) string {
|
||||
if IsReservedSlug(candidate) {
|
||||
return FallbackSlug + "-" + candidate
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
@@ -112,7 +112,7 @@ type Page struct {
|
||||
}
|
||||
|
||||
func IsValidSlug(candidate string) bool {
|
||||
return slug.IsValid(candidate)
|
||||
return slug.IsValid(candidate) && !IsReservedSlug(candidate)
|
||||
}
|
||||
|
||||
func IsValidName(name string) bool {
|
||||
@@ -172,9 +172,9 @@ func ValidateLinks(links []SocialLink) error {
|
||||
}
|
||||
|
||||
func GenerateSlug(name string) string {
|
||||
return slug.Generate(name, FallbackSlug)
|
||||
return avoidReserved(slug.Generate(name, FallbackSlug))
|
||||
}
|
||||
|
||||
func SlugWithSuffix(base string, attempt int) string {
|
||||
return slug.WithSuffix(base, attempt)
|
||||
return avoidReserved(slug.WithSuffix(base, attempt))
|
||||
}
|
||||
@@ -64,6 +64,7 @@ type Repository interface {
|
||||
Create(entry Entry) (Entry, error)
|
||||
Get(contentTypeID uint32, id uint32) (Entry, error)
|
||||
ListPage(filters Filters) (PageResult, error)
|
||||
ListByCreator(creatorID uint32) ([]Entry, error)
|
||||
Update(entry Entry) (Entry, error)
|
||||
Delete(contentTypeID uint32, id uint32) error
|
||||
DeleteByContentType(contentTypeID uint32) error
|
||||
|
||||
@@ -10,6 +10,7 @@ type Repository interface {
|
||||
Get(id uint32) (Image, error)
|
||||
Delete(id uint32) error
|
||||
List(query SearchQuery) ([]Image, error)
|
||||
ListByUploader(userID uint32) ([]Image, error)
|
||||
FindByHash(hash string) ([]Image, error)
|
||||
Rename(id uint32, filename string) (Image, error)
|
||||
ListDuplicates() ([]Image, error)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package mail
|
||||
|
||||
type Dispatcher interface {
|
||||
Dispatch(job func())
|
||||
}
|
||||
@@ -22,9 +22,16 @@ var ErrPasswordTooLong = errors.New("password too long")
|
||||
var ErrRoleChanged = errors.New("user role changed concurrently")
|
||||
var ErrInvalidTransfer = errors.New("invalid ownership transfer")
|
||||
var ErrConfirmationMismatch = errors.New("confirmation email does not match")
|
||||
var ErrWrongPassword = errors.New("wrong password")
|
||||
var ErrSameEmail = errors.New("new email is the current email")
|
||||
var ErrNoPendingEmailChange = errors.New("no pending email change")
|
||||
var ErrInvalidEmailChangeCode = errors.New("invalid email change code")
|
||||
var ErrEmailUnavailable = errors.New("email unavailable")
|
||||
|
||||
const MaxLookupIDs = 100
|
||||
|
||||
const EmailChangeLifetime = 2 * time.Hour
|
||||
|
||||
const MaxPasswordBytes = 72
|
||||
|
||||
func ValidatePassword(password string) error {
|
||||
@@ -47,6 +54,9 @@ type User struct {
|
||||
VerificationCode string `json:"verification_code"`
|
||||
VerificationExpiration time.Time `json:"verification_expiration"`
|
||||
PasswordResetExpiration time.Time `json:"password_reset_expiration"`
|
||||
PendingEmail string `json:"pending_email"`
|
||||
EmailChangeCode string `json:"email_change_code"`
|
||||
EmailChangeExpiration time.Time `json:"email_change_expiration"`
|
||||
TokenVersion uint32 `json:"token_version"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package user
|
||||
|
||||
import "errors"
|
||||
|
||||
const DeletedUserName = "Deleted user"
|
||||
|
||||
const (
|
||||
LastRoleOwner = "Owner"
|
||||
LastRoleAdmin = "Admin"
|
||||
)
|
||||
|
||||
var ErrLastOwner = errors.New("last owner")
|
||||
var ErrLastAdmin = errors.New("last administrator")
|
||||
var ErrAccountNotDeletable = errors.New("account not deletable")
|
||||
|
||||
type LastAdminError struct {
|
||||
Role string
|
||||
}
|
||||
|
||||
func (e LastAdminError) Error() string {
|
||||
if e.Role == LastRoleOwner {
|
||||
return ErrLastOwner.Error()
|
||||
}
|
||||
return ErrLastAdmin.Error()
|
||||
}
|
||||
|
||||
func (e LastAdminError) Is(target error) bool {
|
||||
if target == ErrLastAdmin {
|
||||
return true
|
||||
}
|
||||
return target == ErrLastOwner && e.Role == LastRoleOwner
|
||||
}
|
||||
|
||||
type Erasure struct {
|
||||
UserID uint32
|
||||
GuardRoleIDs []uint32
|
||||
AvatarImageID *uint32
|
||||
}
|
||||
|
||||
type Eraser interface {
|
||||
Erase(erasure Erasure) error
|
||||
}
|
||||
@@ -17,6 +17,10 @@ type Repository interface {
|
||||
ClearVerificationCode(userId uint32) (User, error)
|
||||
ResetPassword(userId uint32, hashedPassword string) (User, error)
|
||||
UpdateDisplayName(userId uint32, displayName string) (User, error)
|
||||
IsEmailInUse(email string, exceptUserId uint32) (bool, error)
|
||||
SetPendingEmailChange(userId uint32, pendingEmail string, code string, expiration time.Time) (User, error)
|
||||
ClearPendingEmailChange(userId uint32) (User, error)
|
||||
ConfirmEmailChange(userId uint32, pendingEmail string, code string, now time.Time) (User, error)
|
||||
UpdateRole(id uint32, from uint32, to uint32, assignedBy uint32) (User, error)
|
||||
TransferOwnership(from uint32, to uint32, ownerRoleID uint32, formerRoleID uint32) error
|
||||
}
|
||||
Reference in new issue
Block a user