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
@@ -0,0 +1,151 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/claims"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type EmailChangeHandler struct {
|
||||
RequestUseCase *useCases.RequestEmailChangeUseCase
|
||||
ResendUseCase *useCases.ResendEmailChangeUseCase
|
||||
CancelUseCase *useCases.CancelEmailChangeUseCase
|
||||
ConfirmUseCase *useCases.ConfirmEmailChangeUseCase
|
||||
}
|
||||
|
||||
type RequestEmailChangeCredentials struct {
|
||||
NewEmail string `json:"new_email" validate:"required,max=255,inviteemail"`
|
||||
CurrentPassword string `json:"current_password" validate:"required"`
|
||||
}
|
||||
|
||||
type ConfirmEmailChangeCredentials struct {
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
|
||||
func NewEmailChangeHandler(request *useCases.RequestEmailChangeUseCase, resend *useCases.ResendEmailChangeUseCase, cancel *useCases.CancelEmailChangeUseCase, confirm *useCases.ConfirmEmailChangeUseCase) *EmailChangeHandler {
|
||||
return &EmailChangeHandler{
|
||||
RequestUseCase: request,
|
||||
ResendUseCase: resend,
|
||||
CancelUseCase: cancel,
|
||||
ConfirmUseCase: confirm,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *EmailChangeHandler) Request(w http.ResponseWriter, r *http.Request) {
|
||||
var credentials RequestEmailChangeCredentials
|
||||
if err := json.NewDecoder(r.Body).Decode(&credentials); err != nil {
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validator.Validate.Struct(credentials); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
userId, ok := claims.UserId(r)
|
||||
if !ok {
|
||||
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
pending, err := h.RequestUseCase.RequestEmailChange(r.Context(), useCases.RequestEmailChangeCommand{
|
||||
UserID: userId,
|
||||
NewEmail: credentials.NewEmail,
|
||||
CurrentPassword: credentials.CurrentPassword,
|
||||
})
|
||||
if err != nil {
|
||||
writeEmailChangeError(w, err)
|
||||
return
|
||||
}
|
||||
writePendingEmail(w, pending)
|
||||
}
|
||||
|
||||
func (h *EmailChangeHandler) Resend(w http.ResponseWriter, r *http.Request) {
|
||||
userId, ok := claims.UserId(r)
|
||||
if !ok {
|
||||
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
pending, err := h.ResendUseCase.ResendEmailChange(r.Context(), userId)
|
||||
if err != nil {
|
||||
writeEmailChangeError(w, err)
|
||||
return
|
||||
}
|
||||
writePendingEmail(w, pending)
|
||||
}
|
||||
|
||||
func (h *EmailChangeHandler) Cancel(w http.ResponseWriter, r *http.Request) {
|
||||
userId, ok := claims.UserId(r)
|
||||
if !ok {
|
||||
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.CancelUseCase.CancelEmailChange(r.Context(), userId); err != nil {
|
||||
writeEmailChangeError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *EmailChangeHandler) Confirm(w http.ResponseWriter, r *http.Request) {
|
||||
var credentials ConfirmEmailChangeCredentials
|
||||
if err := json.NewDecoder(r.Body).Decode(&credentials); err != nil {
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validator.Validate.Struct(credentials); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
userId, ok := claims.UserId(r)
|
||||
if !ok {
|
||||
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
email, err := h.ConfirmUseCase.ConfirmEmailChange(r.Context(), userId, credentials.Code)
|
||||
if err != nil {
|
||||
writeEmailChangeError(w, err)
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(map[string]string{"email": email})
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
func writePendingEmail(w http.ResponseWriter, pending string) {
|
||||
body, err := json.Marshal(map[string]string{"pending_email": pending})
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
func writeEmailChangeError(w http.ResponseWriter, err error) {
|
||||
switch {
|
||||
case errors.Is(err, user.ErrUserNotFound):
|
||||
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
||||
case errors.Is(err, user.ErrWrongPassword):
|
||||
http.Error(w, apperr.WrongPasswordErrorCode, http.StatusForbidden)
|
||||
case errors.Is(err, user.ErrSameEmail):
|
||||
http.Error(w, apperr.SameEmailErrorCode, http.StatusBadRequest)
|
||||
case errors.Is(err, user.ErrNoPendingEmailChange):
|
||||
http.Error(w, apperr.NoPendingEmailChangeErrorCode, http.StatusNotFound)
|
||||
case errors.Is(err, user.ErrInvalidEmailChangeCode):
|
||||
http.Error(w, apperr.InvalidEmailChangeCodeErrorCode, http.StatusForbidden)
|
||||
case errors.Is(err, user.ErrEmailUnavailable):
|
||||
http.Error(w, apperr.EmailUnavailableErrorCode, http.StatusConflict)
|
||||
default:
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -40,16 +40,7 @@ func NewLoginHandler(token *authtoken.Auth, create *useCases.CreateUserUseCase,
|
||||
}
|
||||
|
||||
func removeJwtCookie(w http.ResponseWriter, isProduction bool) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: authtoken.CookieName,
|
||||
Value: "",
|
||||
Expires: time.Now(),
|
||||
MaxAge: -1,
|
||||
Secure: isProduction,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Path: "/",
|
||||
})
|
||||
http.SetCookie(w, authtoken.ExpiredCookie(isProduction))
|
||||
}
|
||||
|
||||
func setJwtCookie(w http.ResponseWriter, token *authtoken.Auth, isProduction bool, u user.User) error {
|
||||
|
||||
@@ -41,8 +41,9 @@ type meAccess struct {
|
||||
|
||||
type meResponse struct {
|
||||
publicuser.PublicUser
|
||||
Author *meAuthor `json:"author"`
|
||||
Access meAccess `json:"access"`
|
||||
PendingEmail *string `json:"pending_email"`
|
||||
Author *meAuthor `json:"author"`
|
||||
Access meAccess `json:"access"`
|
||||
}
|
||||
|
||||
func toMeAccess(access useCases.Access) meAccess {
|
||||
@@ -60,6 +61,13 @@ func toMeAuthor(found *author.Author) *meAuthor {
|
||||
return &meAuthor{Byline: found.Byline(), ArticleCount: found.ArticleCount}
|
||||
}
|
||||
|
||||
func pendingEmailOf(account user.User) *string {
|
||||
if account.PendingEmail == "" {
|
||||
return nil
|
||||
}
|
||||
return &account.PendingEmail
|
||||
}
|
||||
|
||||
func (h *MeHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||||
userId, ok := claims.UserId(r)
|
||||
if !ok {
|
||||
@@ -89,7 +97,7 @@ func (h *MeHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
body, err := json.Marshal(meResponse{PublicUser: publicuser.New(loadedUser), Author: toMeAuthor(linked), Access: toMeAccess(access)})
|
||||
body, err := json.Marshal(meResponse{PublicUser: publicuser.New(loadedUser), PendingEmail: pendingEmailOf(loadedUser), Author: toMeAuthor(linked), Access: toMeAccess(access)})
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user