feat: 新增修改密码和注销账号功能
新增修改密码和注销账号的API接口,实现用户通过验证旧密码修改密码,以及通过短信验证流程注销账号的功能,同时补充相关常量和文档说明。
This commit is contained in:
+154
-2
@@ -1704,6 +1704,155 @@ func serveIMFeedback(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMSuccess(wrt, req, feedbackSubmissionPayload(feedback))
|
||||
}
|
||||
|
||||
type imMePasswordChangeRequest struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
// serveIMMePasswordChange lets an authenticated user change own password by
|
||||
// presenting the current one. All sessions are evicted afterwards.
|
||||
func serveIMMePasswordChange(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body imMePasswordChangeRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(body.OldPassword) == "" || strings.TrimSpace(body.NewPassword) == "" {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||
return
|
||||
}
|
||||
basicAuth := store.Store.GetLogicalAuthHandler("basic")
|
||||
if basicAuth == nil || !basicAuth.IsInitialized() {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
login, authLevel, _, _, err := store.Users.GetAuthRecord(principal.TenantID, principal.Uid)
|
||||
if err != nil || login == "" {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
authCtx := auth.AuthContext{TenantID: principal.TenantID, RemoteAddr: req.RemoteAddr}
|
||||
if _, _, err = basicAuth.Authenticate(authCtx, []byte(login+":"+body.OldPassword)); err != nil {
|
||||
writeIMError(wrt, req, http.StatusUnauthorized, 30001, imReasonAuthRequired, "invalid credentials", nil)
|
||||
return
|
||||
}
|
||||
if _, err = basicAuth.UpdateRecord(authCtx, &auth.Rec{
|
||||
TenantID: principal.TenantID,
|
||||
Uid: principal.Uid,
|
||||
AuthLevel: authLevel,
|
||||
}, []byte(":"+body.NewPassword)); err != nil {
|
||||
if err == types.ErrPolicy {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30040, imReasonPasswordPolicy, "password policy violation", nil)
|
||||
return
|
||||
}
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
if store.Security != nil {
|
||||
if _, err = store.Security.BumpAuthVersion(principal.TenantID, principal.Uid, now); err != nil {
|
||||
logs.Warn.Println("im me password change: bump auth version failed", err)
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
if globals.sessionStore != nil {
|
||||
globals.sessionStore.EvictUser(principal.Uid, "")
|
||||
}
|
||||
publishRealtimeEvictUser(principal.TenantID, principal.Uid, "password_changed", "")
|
||||
writeIMSuccess(wrt, req, map[string]any{"status": "ok"})
|
||||
}
|
||||
|
||||
// serveIMMeDeletion deletes the authenticated user's account. Requires a
|
||||
// confirmed "delete_account" sensitive-action token obtained through
|
||||
// POST /v1/im/security/challenges + POST /v1/im/security/challenges/{id}.
|
||||
func serveIMMeDeletion(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if globals.permanentAccounts {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30040, imReasonForbidden, "account deletion is disabled", nil)
|
||||
return
|
||||
}
|
||||
if !consumeIMActionTokenHeader(wrt, req, principal, "delete_account") {
|
||||
return
|
||||
}
|
||||
tenantID, uid := principal.TenantID, principal.Uid
|
||||
if state, err := userGetState(tenantID, uid); err != nil || state == types.StateDeleted {
|
||||
logs.Warn.Println("im me deletion: user is missing or already deleted", err)
|
||||
writeIMError(wrt, req, http.StatusNotFound, 20004, imReasonResourceNotFound, "user not found", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Disable all authenticators.
|
||||
for _, name := range store.Store.GetAuthNames() {
|
||||
hdl := store.Store.GetLogicalAuthHandler(name)
|
||||
if !hdl.IsInitialized() {
|
||||
continue
|
||||
}
|
||||
if err := hdl.DelRecords(tenantID, uid); err != nil {
|
||||
// This could be completely benign, i.e. authenticator exists but not used.
|
||||
logs.Warn.Println("im me deletion: failed to delete auth record", uid.UserId(), name, err)
|
||||
if storeErr, ok := err.(types.StoreError); ok && storeErr == types.ErrUnsupported {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30040, imReasonForbidden, "account cannot be deleted", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Terminate all sessions.
|
||||
if globals.sessionStore != nil {
|
||||
globals.sessionStore.EvictUser(uid, "", "user_deleted")
|
||||
}
|
||||
publishRealtimeEvictUser(tenantID, uid, "user_deleted", "")
|
||||
// Remove user from cache and announce to cluster that the user is deleted.
|
||||
usersRemoveUser(tenantID, uid)
|
||||
|
||||
// Stop topics where the user is the owner and p2p topics.
|
||||
done := make(chan bool)
|
||||
globals.hub.unreg <- &topicUnreg{tenantID: tenantID, forUser: uid, del: false, done: done}
|
||||
<-done
|
||||
|
||||
// Notify users of interest that the user is gone.
|
||||
if uoi, err := store.Users.GetSubs(tenantID, uid); err == nil {
|
||||
presUsersOfInterestOffline(tenantID, uid, uoi, "gone")
|
||||
} else {
|
||||
logs.Warn.Println("im me deletion: failed to send notifications to users", err)
|
||||
}
|
||||
// Notify subscribers of the group topics where the user was the owner.
|
||||
if ownTopics, err := store.Users.GetOwnTopics(tenantID, uid); err == nil {
|
||||
for _, topicName := range ownTopics {
|
||||
if subs, err := store.Topics.GetSubs(tenantID, topicName, nil); err == nil {
|
||||
presSubsOfflineOffline(tenantID, topicName, types.TopicCatGrp, subs, "gone", &presParams{}, "")
|
||||
} else {
|
||||
logs.Warn.Println("im me deletion: failed to notify topic subscribers", err, topicName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logs.Warn.Println("im me deletion: failed to send notifications to owned topics", err)
|
||||
}
|
||||
|
||||
// Delete user's records from the database (soft delete).
|
||||
if err := store.Users.Delete(tenantID, uid, false); err != nil {
|
||||
logs.Warn.Println("im me deletion: failed to delete user", err)
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"status": "ok"})
|
||||
}
|
||||
|
||||
func serveIMSecurityChallenges(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -6842,6 +6991,8 @@ func defaultIMActionTarget(action, fallback string) string {
|
||||
return "youth_mode"
|
||||
case "group_owner_transfer", "group_dissolve":
|
||||
return action
|
||||
case "delete_account":
|
||||
return "account"
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
@@ -8516,7 +8667,7 @@ func currentUserAccountPayload(tenantID types.TenantID, uid types.Uid) (map[stri
|
||||
func validIMSecurityChallengeScene(scene string) bool {
|
||||
switch strings.TrimSpace(scene) {
|
||||
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password",
|
||||
"group_owner_transfer", "group_dissolve":
|
||||
"group_owner_transfer", "group_dissolve", "delete_account":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -10599,6 +10750,7 @@ func cleanStringList(values []string, limit int) []string {
|
||||
func isAllowedPublicUploadPurpose(purpose string) bool {
|
||||
switch strings.TrimSpace(purpose) {
|
||||
case types.PublicUploadPurposeEnterpriseLicense, types.PublicUploadPurposeTenantFeedback,
|
||||
types.PublicUploadPurposeFeedback,
|
||||
types.PublicUploadPurposeUserAvatar,
|
||||
types.PublicUploadPurposeGroupAvatar:
|
||||
return true
|
||||
@@ -10615,7 +10767,7 @@ func isAllowedPublicUploadMime(purpose, mimeType string) bool {
|
||||
strings.HasPrefix(mimeType, "image/png") ||
|
||||
strings.HasPrefix(mimeType, "image/webp") ||
|
||||
mimeType == "application/pdf"
|
||||
case types.PublicUploadPurposeTenantFeedback:
|
||||
case types.PublicUploadPurposeTenantFeedback, types.PublicUploadPurposeFeedback:
|
||||
return strings.HasPrefix(mimeType, "image/jpeg") ||
|
||||
strings.HasPrefix(mimeType, "image/png") ||
|
||||
strings.HasPrefix(mimeType, "image/webp")
|
||||
|
||||
Reference in New Issue
Block a user