diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go new file mode 100644 index 0000000..4cc38ac --- /dev/null +++ b/modules/structs/admin_user.go @@ -0,0 +1,10 @@ +package structs + +import ( + gitea_api "code.gitea.io/gitea/modules/structs" +) + +type HatEditUserOption struct { + NewName string `json:"new_name" binding:"MaxSize(40)"` + *gitea_api.EditUserOption +} diff --git a/routers/hat/admin/user.go b/routers/hat/admin/user.go new file mode 100644 index 0000000..c5b3e0b --- /dev/null +++ b/routers/hat/admin/user.go @@ -0,0 +1,214 @@ +package admin + +import ( + "errors" + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/password" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/agit" + container_service "code.gitea.io/gitea/services/packages/container" + hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs" +) + +func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64, loginName string) { + if sourceID == 0 { + return + } + + source, err := auth.GetSourceByID(sourceID) + if err != nil { + if auth.IsErrSourceNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { + ctx.Error(http.StatusInternalServerError, "auth.GetSourceByID", err) + } + return + } + + u.LoginType = source.Type + u.LoginSource = source.ID + u.LoginName = loginName +} + +func EditUser(ctx *context.APIContext) { + form := web.GetForm(ctx).(*hat_api.HatEditUserOption) + parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName) + if ctx.Written() { + return + } + + if len(form.Password) != 0 { + if len(form.Password) < setting.MinPasswordLength { + ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength)) + return + } + if !password.IsComplexEnough(form.Password) { + err := errors.New("PasswordComplexity") + ctx.Error(http.StatusBadRequest, "PasswordComplexity", err) + return + } + pwned, err := password.IsPwned(ctx, form.Password) + if pwned { + if err != nil { + log.Error(err.Error()) + } + ctx.Data["Err_Password"] = true + ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) + return + } + if ctx.ContextUser.Salt, err = user_model.GetUserSalt(); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateUser", err) + return + } + if err = ctx.ContextUser.SetPassword(form.Password); err != nil { + ctx.InternalServerError(err) + return + } + } + + if form.MustChangePassword != nil { + ctx.ContextUser.MustChangePassword = *form.MustChangePassword + } + + if len(form.NewName) != 0 && ctx.ContextUser.Name != form.NewName { + if err := handleUsernameChange(ctx, ctx.ContextUser, form.NewName); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateUser", err) + return + } + ctx.ContextUser.Name = form.NewName + ctx.ContextUser.LowerName = strings.ToLower(form.NewName) + } + + ctx.ContextUser.LoginName = form.LoginName + + if form.FullName != nil { + ctx.ContextUser.FullName = *form.FullName + } + var emailChanged bool + if form.Email != nil { + email := strings.TrimSpace(*form.Email) + if len(email) == 0 { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("email is not allowed to be empty string")) + return + } + + if err := user_model.ValidateEmail(email); err != nil { + ctx.InternalServerError(err) + return + } + + emailChanged = !strings.EqualFold(ctx.ContextUser.Email, email) + ctx.ContextUser.Email = email + } + if form.Website != nil { + ctx.ContextUser.Website = *form.Website + } + if form.Location != nil { + ctx.ContextUser.Location = *form.Location + } + if form.Description != nil { + ctx.ContextUser.Description = *form.Description + } + if form.Active != nil { + ctx.ContextUser.IsActive = *form.Active + } + if len(form.Visibility) != 0 { + ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility] + } + if form.Admin != nil { + ctx.ContextUser.IsAdmin = *form.Admin + } + if form.AllowGitHook != nil { + ctx.ContextUser.AllowGitHook = *form.AllowGitHook + } + if form.AllowImportLocal != nil { + ctx.ContextUser.AllowImportLocal = *form.AllowImportLocal + } + if form.MaxRepoCreation != nil { + ctx.ContextUser.MaxRepoCreation = *form.MaxRepoCreation + } + if form.AllowCreateOrganization != nil { + ctx.ContextUser.AllowCreateOrganization = *form.AllowCreateOrganization + } + if form.ProhibitLogin != nil { + ctx.ContextUser.ProhibitLogin = *form.ProhibitLogin + } + if form.Restricted != nil { + ctx.ContextUser.IsRestricted = *form.Restricted + } + + if err := user_model.UpdateUser(ctx, ctx.ContextUser, emailChanged); err != nil { + if user_model.IsErrEmailAlreadyUsed(err) || + user_model.IsErrEmailCharIsNotSupported(err) || + user_model.IsErrEmailInvalid(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { + ctx.Error(http.StatusInternalServerError, "UpdateUser", err) + } + return + } + log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name) + + ctx.JSON(http.StatusOK, convert.ToUser(ctx.ContextUser, ctx.Doer)) +} + +func handleUsernameChange(ctx *context.APIContext, user *user_model.User, newName string) error { + // Non-local users are not allowed to change their username. + if !user.IsLocal() { + ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user")) + return fmt.Errorf(ctx.Tr("form.username_change_not_local_user")) + } + + // Check if user name has been changed + if user.LowerName != strings.ToLower(newName) { + if err := user_model.ChangeUserName(user, newName); err != nil { + switch { + case user_model.IsErrUserAlreadyExist(err): + ctx.Error(http.StatusInternalServerError, "ChangeUserName", ctx.Tr("form.username_been_taken")) + case user_model.IsErrEmailAlreadyUsed(err): + ctx.Error(http.StatusInternalServerError, "ChangeUserName", ctx.Tr("form.email_been_used")) + case db.IsErrNameReserved(err): + ctx.Error(http.StatusInternalServerError, "ChangeUserName", ctx.Tr("user.form.name_reserved", newName)) + case db.IsErrNamePatternNotAllowed(err): + ctx.Error(http.StatusInternalServerError, "ChangeUserName", ctx.Tr("user.form.name_pattern_not_allowed", newName)) + case db.IsErrNameCharsNotAllowed(err): + ctx.Error(http.StatusInternalServerError, "ChangeUserName", ctx.Tr("user.form.name_chars_not_allowed", newName)) + default: + ctx.Error(http.StatusInternalServerError, "ChangeUserName", err) + } + return err + } + } else { + if err := repo_model.UpdateRepositoryOwnerNames(user.ID, newName); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateRepository", err) + return err + } + } + + // update all agit flow pull request header + err := agit.UserNameChanged(user, newName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "agit.UserNameChanged", err) + return err + } + + if err := container_service.UpdateRepositoryNames(ctx, user, newName); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateRepositoryNames", err) + return err + } + + log.Trace("User name changed: %s -> %s", user.Name, newName) + return nil +} diff --git a/routers/hat/hat.go b/routers/hat/hat.go index f39b014..88f3f9f 100644 --- a/routers/hat/hat.go +++ b/routers/hat/hat.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/misc" "code.gitea.io/gitea/services/auth" context_service "code.gitea.io/gitea/services/context" + "code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/admin" "code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/org" "code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/repo" "code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/user" @@ -127,6 +128,14 @@ func Routers(ctx gocontext.Context) *web.Route { m.Group("/orgs/{org}", func() { m.Combo("").Patch(reqToken(), reqOrgOwnership(), bind(hat_api.EditOrgOption{}), org.Edit) }, orgAssignment(true)) + + m.Group("/admin", func() { + m.Group("/users", func() { + m.Group("/{username}", func() { + m.Combo("").Patch(bind(hat_api.HatEditUserOption{}), admin.EditUser) + }, context_service.UserAssignmentAPI()) + }) + }, reqToken(), reqSiteAdmin()) }) return m @@ -379,3 +388,13 @@ func reqToken() func(ctx *context.APIContext) { ctx.Error(http.StatusUnauthorized, "reqToken", "token is required") } } + +// reqSiteAdmin user should be the site admin +func reqSiteAdmin() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if !ctx.IsUserSiteAdmin() { + ctx.Error(http.StatusForbidden, "reqSiteAdmin", "user should be the site admin") + return + } + } +}