1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

Unify user update methods (#28733)

Fixes #28660
Fixes an admin api bug related to `user.LoginSource`
Fixed `/user/emails` response not identical to GitHub api

This PR unifies the user update methods. The goal is to keep the logic
only at one place (having audit logs in mind). For example, do the
password checks only in one method not everywhere a password is updated.

After that PR is merged, the user creation should be next.
This commit is contained in:
KN4CK3R
2024-02-04 14:29:09 +01:00
committed by GitHub
parent b4513f48ce
commit f8b471ace1
42 changed files with 1383 additions and 1068 deletions

View File

@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -18,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
@@ -107,9 +107,8 @@ func CreateUser(ctx *context.APIContext) {
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
if err != nil {
if err := password.IsPwned(ctx, form.Password); err != nil {
if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
}
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
@@ -192,115 +191,65 @@ func EditUser(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditUserOption)
parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName)
if ctx.Written() {
authOpts := &user_service.UpdateAuthOptions{
LoginSource: optional.FromNonDefault(form.SourceID),
LoginName: optional.Some(form.LoginName),
Password: optional.FromNonDefault(form.Password),
MustChangePassword: optional.FromPtr(form.MustChangePassword),
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
}
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
case errors.Is(err, password.ErrComplexity):
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
ctx.Error(http.StatusBadRequest, "PasswordIsPwned", err)
default:
ctx.Error(http.StatusInternalServerError, "UpdateAuth", err)
}
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.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
}
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"))
if err := user_service.AddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
case user_model.IsErrEmailAlreadyUsed(err):
ctx.Error(http.StatusBadRequest, "EmailUsed", err)
default:
ctx.Error(http.StatusInternalServerError, "AddOrSetPrimaryEmailAddress", err)
}
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 {
if !*form.Admin && user_model.IsLastAdminUser(ctx, ctx.ContextUser) {
ctx.Error(http.StatusBadRequest, "LastAdmin", ctx.Tr("auth.last_admin"))
return
}
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)
opts := &user_service.UpdateOptions{
FullName: optional.FromPtr(form.FullName),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
IsAdmin: optional.FromPtr(form.Admin),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
IsRestricted: optional.FromPtr(form.Restricted),
}
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
if models.IsErrDeleteLastAdminUser(err) {
ctx.Error(http.StatusBadRequest, "LastAdmin", 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, ctx.ContextUser, ctx.Doer))
@@ -527,9 +476,6 @@ func RenameUser(ctx *context.APIContext) {
// Check if user name has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
switch {
case user_model.IsErrUsernameNotChanged(err):
// Noop as username is not changed
ctx.Status(http.StatusNoContent)
case user_model.IsErrUserAlreadyExist(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
case db.IsErrNameReserved(err):
@@ -545,5 +491,5 @@ func RenameUser(ctx *context.APIContext) {
}
log.Trace("User name changed: %s -> %s", oldName, newName)
ctx.Status(http.StatusOK)
ctx.Status(http.StatusNoContent)
}

View File

@@ -13,12 +13,14 @@ import (
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/org"
user_service "code.gitea.io/gitea/services/user"
)
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
@@ -337,28 +339,30 @@ func Edit(ctx *context.APIContext) {
// "$ref": "#/responses/Organization"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditOrgOption)
org := ctx.Org.Organization
org.FullName = form.FullName
org.Email = form.Email
org.Description = form.Description
org.Website = form.Website
org.Location = form.Location
if form.Visibility != "" {
org.Visibility = api.VisibilityModes[form.Visibility]
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err)
return
}
}
if form.RepoAdminChangeTeamAccess != nil {
org.RepoAdminChangeTeamAccess = *form.RepoAdminChangeTeamAccess
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_model.UpdateUserCols(ctx, org.AsUser(),
"full_name", "description", "website", "location",
"visibility", "repo_admin_change_team_access",
); err != nil {
ctx.Error(http.StatusInternalServerError, "EditOrganization", err)
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
return
}
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, org))
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, ctx.Org.Organization))
}
// Delete an organization

View File

@@ -9,10 +9,10 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/convert"
user_service "code.gitea.io/gitea/services/user"
)
// ListEmails list all of the authenticated user's email addresses
@@ -56,22 +56,14 @@ func AddEmail(ctx *context.APIContext) {
// "$ref": "#/responses/EmailList"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateEmailOption)
if len(form.Emails) == 0 {
ctx.Error(http.StatusUnprocessableEntity, "", "Email list empty")
return
}
emails := make([]*user_model.EmailAddress, len(form.Emails))
for i := range form.Emails {
emails[i] = &user_model.EmailAddress{
UID: ctx.Doer.ID,
Email: form.Emails[i],
IsActivated: !setting.Service.RegisterEmailConfirm,
}
}
if err := user_model.AddEmailAddresses(ctx, emails); err != nil {
if err := user_service.AddEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
} else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
@@ -91,11 +83,17 @@ func AddEmail(ctx *context.APIContext) {
return
}
apiEmails := make([]*api.Email, len(emails))
for i := range emails {
apiEmails[i] = convert.ToEmail(emails[i])
emails, err := user_model.GetEmailAddresses(ctx, ctx.Doer.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetEmailAddresses", err)
return
}
ctx.JSON(http.StatusCreated, &apiEmails)
apiEmails := make([]*api.Email, 0, len(emails))
for _, email := range emails {
apiEmails = append(apiEmails, convert.ToEmail(email))
}
ctx.JSON(http.StatusCreated, apiEmails)
}
// DeleteEmail delete email
@@ -115,26 +113,19 @@ func DeleteEmail(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.DeleteEmailOption)
if len(form.Emails) == 0 {
ctx.Status(http.StatusNoContent)
return
}
emails := make([]*user_model.EmailAddress, len(form.Emails))
for i := range form.Emails {
emails[i] = &user_model.EmailAddress{
Email: form.Emails[i],
UID: ctx.Doer.ID,
}
}
if err := user_model.DeleteEmailAddresses(ctx, emails); err != nil {
if err := user_service.DeleteEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil {
if user_model.IsErrEmailAddressNotExist(err) {
ctx.Error(http.StatusNotFound, "DeleteEmailAddresses", err)
return
} else {
ctx.Error(http.StatusInternalServerError, "DeleteEmailAddresses", err)
}
ctx.Error(http.StatusInternalServerError, "DeleteEmailAddresses", err)
return
}
ctx.Status(http.StatusNoContent)

View File

@@ -6,11 +6,12 @@ package user
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/convert"
user_service "code.gitea.io/gitea/services/user"
)
// GetUserSettings returns user settings
@@ -44,36 +45,18 @@ func UpdateUserSettings(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.UserSettingsOptions)
if form.FullName != nil {
ctx.Doer.FullName = *form.FullName
opts := &user_service.UpdateOptions{
FullName: optional.FromPtr(form.FullName),
Description: optional.FromPtr(form.Description),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Language: optional.FromPtr(form.Language),
Theme: optional.FromPtr(form.Theme),
DiffViewStyle: optional.FromPtr(form.DiffViewStyle),
KeepEmailPrivate: optional.FromPtr(form.HideEmail),
KeepActivityPrivate: optional.FromPtr(form.HideActivity),
}
if form.Description != nil {
ctx.Doer.Description = *form.Description
}
if form.Website != nil {
ctx.Doer.Website = *form.Website
}
if form.Location != nil {
ctx.Doer.Location = *form.Location
}
if form.Language != nil {
ctx.Doer.Language = *form.Language
}
if form.Theme != nil {
ctx.Doer.Theme = *form.Theme
}
if form.DiffViewStyle != nil {
ctx.Doer.DiffViewStyle = *form.DiffViewStyle
}
if form.HideEmail != nil {
ctx.Doer.KeepEmailPrivate = *form.HideEmail
}
if form.HideActivity != nil {
ctx.Doer.KeepActivityPrivate = *form.HideActivity
}
if err := user_model.UpdateUser(ctx, ctx.Doer, false); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.InternalServerError(err)
return
}

View File

@@ -5,6 +5,7 @@
package admin
import (
"errors"
"net/http"
"net/url"
"strconv"
@@ -20,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -162,11 +164,10 @@ func NewUserPost(ctx *context.Context) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
if err := password.IsPwned(ctx, form.Password); err != nil {
ctx.Data["Err_Password"] = true
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
@@ -184,10 +185,7 @@ func NewUserPost(ctx *context.Context) {
case user_model.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
case user_model.IsErrEmailCharIsNotSupported(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
case user_model.IsErrEmailInvalid(err):
case user_model.IsErrEmailInvalid(err), user_model.IsErrEmailCharIsNotSupported(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
case db.IsErrNameReserved(err):
@@ -348,68 +346,111 @@ func EditUserPost(ctx *context.Context) {
return
}
if form.UserName != "" {
if err := user_service.RenameUser(ctx, u, form.UserName); err != nil {
switch {
case user_model.IsErrUserIsNotLocal(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form)
case user_model.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserEdit, &form)
case db.IsErrNameReserved(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form)
case db.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form)
case db.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form)
default:
ctx.ServerError("RenameUser", err)
}
return
}
}
authOpts := &user_service.UpdateAuthOptions{
Password: optional.FromNonDefault(form.Password),
LoginName: optional.Some(form.LoginName),
}
// skip self Prohibit Login
if ctx.Doer.ID == u.ID {
authOpts.ProhibitLogin = optional.Some(false)
} else {
authOpts.ProhibitLogin = optional.Some(form.ProhibitLogin)
}
fields := strings.Split(form.LoginType, "-")
if len(fields) == 2 {
loginType, _ := strconv.ParseInt(fields[0], 10, 0)
authSource, _ := strconv.ParseInt(fields[1], 10, 64)
if u.LoginSource != authSource {
u.LoginSource = authSource
u.LoginType = auth.Type(loginType)
}
authOpts.LoginSource = optional.Some(authSource)
}
if len(form.Password) > 0 && (u.IsLocal() || u.IsOAuth2()) {
var err error
if len(form.Password) < setting.MinPasswordLength {
if err := user_service.UpdateAuth(ctx, u, authOpts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
return
}
if !password.IsComplexEnough(form.Password) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
case errors.Is(err, password.ErrComplexity):
ctx.Data["Err_Password"] = true
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
ctx.RenderWithErr(errMsg, tplUserEdit, &form)
return
}
if err := user_model.ValidateEmail(form.Email); err != nil {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_error"), tplUserEdit, &form)
return
}
if u.Salt, err = user_model.GetUserSalt(); err != nil {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
case errors.Is(err, password.ErrIsPwned):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplUserEdit, &form)
case password.IsErrIsPwnedRequest(err):
log.Error("%s", err.Error())
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
default:
ctx.ServerError("UpdateUser", err)
return
}
if err = u.SetPassword(form.Password); err != nil {
ctx.ServerError("SetPassword", err)
return
}
if form.Email != "" {
if err := user_service.AddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
case user_model.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
default:
ctx.ServerError("AddOrSetPrimaryEmailAddress", err)
}
return
}
}
if len(form.UserName) != 0 && u.Name != form.UserName {
if err := user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil {
if ctx.Written() {
return
}
ctx.RenderWithErr(ctx.Flash.ErrorMsg, tplUserEdit, &form)
return
}
u.Name = form.UserName
u.LowerName = strings.ToLower(form.UserName)
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
IsActive: optional.Some(form.Active),
IsAdmin: optional.Some(form.Admin),
AllowGitHook: optional.Some(form.AllowGitHook),
AllowImportLocal: optional.Some(form.AllowImportLocal),
MaxRepoCreation: optional.Some(form.MaxRepoCreation),
AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
IsRestricted: optional.Some(form.Restricted),
Visibility: optional.Some(form.Visibility),
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
if models.IsErrDeleteLastAdminUser(err) {
ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
} else {
ctx.ServerError("UpdateUser", err)
}
return
}
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
if form.Reset2FA {
tf, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
@@ -433,53 +474,8 @@ func EditUserPost(ctx *context.Context) {
return
}
}
}
// Check whether user is the last admin
if !form.Admin && user_model.IsLastAdminUser(ctx, u) {
ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
return
}
u.LoginName = form.LoginName
u.FullName = form.FullName
emailChanged := !strings.EqualFold(u.Email, form.Email)
u.Email = form.Email
u.Website = form.Website
u.Location = form.Location
u.MaxRepoCreation = form.MaxRepoCreation
u.IsActive = form.Active
u.IsAdmin = form.Admin
u.IsRestricted = form.Restricted
u.AllowGitHook = form.AllowGitHook
u.AllowImportLocal = form.AllowImportLocal
u.AllowCreateOrganization = form.AllowCreateOrganization
u.Visibility = form.Visibility
// skip self Prohibit Login
if ctx.Doer.ID == u.ID {
u.ProhibitLogin = false
} else {
u.ProhibitLogin = form.ProhibitLogin
}
if err := user_model.UpdateUser(ctx, u, emailChanged); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
} else if user_model.IsErrEmailCharIsNotSupported(err) ||
user_model.IsErrEmailInvalid(err) {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
} else {
ctx.ServerError("UpdateUser", err)
}
return
}
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
}

View File

@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
@@ -30,6 +31,7 @@ import (
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
"github.com/markbates/goth"
)
@@ -104,9 +106,11 @@ func autoSignIn(ctx *context.Context) (bool, error) {
func resetLocale(ctx *context.Context, u *user_model.User) error {
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
if len(u.Language) == 0 {
u.Language = ctx.Locale.Language()
if err := user_model.UpdateUserCols(ctx, u, "language"); err != nil {
if u.Language == "" {
opts := &user_service.UpdateOptions{
Language: optional.Some(ctx.Locale.Language()),
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
return err
}
}
@@ -330,10 +334,12 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
if len(u.Language) == 0 {
u.Language = ctx.Locale.Language()
if err := user_model.UpdateUserCols(ctx, u, "language"); err != nil {
ctx.ServerError("UpdateUserCols Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
if u.Language == "" {
opts := &user_service.UpdateOptions{
Language: optional.Some(ctx.Locale.Language()),
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, ctx.Locale.Language()))
return setting.AppSubURL + "/"
}
}
@@ -348,9 +354,8 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
ctx.Csrf.DeleteCookie(ctx)
// Register last login
u.SetLastLogin()
if err := user_model.UpdateUserCols(ctx, u, "last_login_unix"); err != nil {
ctx.ServerError("UpdateUserCols", err)
if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
ctx.ServerError("UpdateUser", err)
return setting.AppSubURL + "/"
}
@@ -482,10 +487,9 @@ func SignUpPost(ctx *context.Context) {
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
if err := password.IsPwned(ctx, form.Password); err != nil {
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
@@ -589,10 +593,12 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
// Auto-set admin for the only user.
if user_model.CountUsers(ctx, nil) == 1 {
u.IsAdmin = true
u.IsActive = true
u.SetLastLogin()
if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_active", "last_login_unix"); err != nil {
opts := &user_service.UpdateOptions{
IsActive: optional.Some(true),
IsAdmin: optional.Some(true),
SetLastLogin: true,
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return false
}
@@ -752,10 +758,8 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
return
}
// Register last login
user.SetLastLogin()
if err := user_model.UpdateUserCols(ctx, user, "last_login_unix"); err != nil {
ctx.ServerError("UpdateUserCols", err)
if err := user_service.UpdateUser(ctx, user, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
ctx.ServerError("UpdateUser", err)
return
}

View File

@@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -990,7 +991,9 @@ func SignInOAuthCallback(ctx *context.Context) {
source := authSource.Cfg.(*oauth2.Source)
setUserAdminAndRestrictedFromGroupClaims(source, u, &gothUser)
isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
u.IsAdmin = isAdmin.ValueOrDefault(false)
u.IsRestricted = isRestricted.ValueOrDefault(false)
if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled
@@ -1054,19 +1057,17 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[
return claimValueToStringSet(groupClaims)
}
func setUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, u *user_model.User, gothUser *goth.User) bool {
func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
groups := getClaimedGroups(source, gothUser)
wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted
if source.AdminGroup != "" {
u.IsAdmin = groups.Contains(source.AdminGroup)
isAdmin = optional.Some(groups.Contains(source.AdminGroup))
}
if source.RestrictedGroup != "" {
u.IsRestricted = groups.Contains(source.RestrictedGroup)
isRestricted = optional.Some(groups.Contains(source.RestrictedGroup))
}
return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted
return isAdmin, isRestricted
}
func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
@@ -1133,18 +1134,12 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
// Clear whatever CSRF cookie has right now, force to generate a new one
ctx.Csrf.DeleteCookie(ctx)
// Register last login
u.SetLastLogin()
// Update GroupClaims
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
cols := []string{"last_login_unix"}
if changed {
cols = append(cols, "is_admin", "is_restricted")
opts := &user_service.UpdateOptions{
SetLastLogin: true,
}
if err := user_model.UpdateUserCols(ctx, u, cols...); err != nil {
ctx.ServerError("UpdateUserCols", err)
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
@@ -1177,10 +1172,11 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return
}
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
if changed {
if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil {
ctx.ServerError("UpdateUserCols", err)
opts := &user_service.UpdateOptions{}
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
}

View File

@@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
@@ -21,6 +22,7 @@ import (
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
)
var (
@@ -165,30 +167,6 @@ func ResetPasswdPost(ctx *context.Context) {
return
}
// Validate password length.
passwd := ctx.FormString("password")
if len(passwd) < setting.MinPasswordLength {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
return
} else if !password.IsComplexEnough(passwd) {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
return
} else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil {
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(errMsg, tplResetPassword, nil)
return
}
// Handle two-factor
regenerateScratchToken := false
if twofa != nil {
@@ -221,18 +199,27 @@ func ResetPasswdPost(ctx *context.Context) {
}
}
}
var err error
if u.Rands, err = user_model.GetUserSalt(); err != nil {
ctx.ServerError("UpdateUser", err)
return
opts := &user_service.UpdateAuthOptions{
Password: optional.Some(ctx.FormString("password")),
MustChangePassword: optional.Some(false),
}
if err = u.SetPassword(passwd); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
u.MustChangePassword = false
if err := user_model.UpdateUserCols(ctx, u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil {
ctx.ServerError("UpdateUser", err)
if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Password"] = true
switch {
case errors.Is(err, password.ErrMinLength):
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
case errors.Is(err, password.ErrComplexity):
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
case errors.Is(err, password.ErrIsPwned):
ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplResetPassword, nil)
case password.IsErrIsPwnedRequest(err):
log.Error("%s", err.Error())
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
default:
ctx.ServerError("UpdateAuth", err)
}
return
}
@@ -242,7 +229,7 @@ func ResetPasswdPost(ctx *context.Context) {
if regenerateScratchToken {
// Invalidate the scratch token.
_, err = twofa.GenerateScratchToken()
_, err := twofa.GenerateScratchToken()
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -282,11 +269,11 @@ func MustChangePasswordPost(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplMustChangePassword)
return
}
u := ctx.Doer
// Make sure only requests for users who are eligible to change their password via
// this method passes through
if !u.MustChangePassword {
ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page"))
if !ctx.Doer.MustChangePassword {
ctx.ServerError("MustUpdatePassword", errors.New("cannot update password. Please visit the settings page"))
return
}
@@ -296,44 +283,34 @@ func MustChangePasswordPost(ctx *context.Context) {
return
}
if len(form.Password) < setting.MinPasswordLength {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
return
opts := &user_service.UpdateAuthOptions{
Password: optional.Some(form.Password),
MustChangePassword: optional.Some(false),
}
if !password.IsComplexEnough(form.Password) {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
ctx.Data["Err_Password"] = true
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
case errors.Is(err, password.ErrComplexity):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
case errors.Is(err, password.ErrIsPwned):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplMustChangePassword, &form)
case password.IsErrIsPwnedRequest(err):
log.Error("%s", err.Error())
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
default:
ctx.ServerError("UpdateAuth", err)
}
ctx.RenderWithErr(errMsg, tplMustChangePassword, &form)
return
}
if err = u.SetPassword(form.Password); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
u.MustChangePassword = false
if err := user_model.UpdateUserCols(ctx, u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
log.Trace("User updated password: %s", u.Name)
log.Trace("User updated password: %s", ctx.Doer.Name)
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
middleware.DeleteRedirectToCookie(ctx.Resp)

View File

@@ -7,7 +7,6 @@ package org
import (
"net/http"
"net/url"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
@@ -17,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
@@ -71,53 +71,50 @@ func SettingsPost(ctx *context.Context) {
}
org := ctx.Org.Organization
nameChanged := org.Name != form.Name
// Check if organization name has been changed.
if nameChanged {
err := user_service.RenameUser(ctx, org.AsUser(), form.Name)
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Data["OrgName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
return
case db.IsErrNameReserved(err):
ctx.Data["OrgName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
return
case db.IsErrNamePatternNotAllowed(err):
ctx.Data["OrgName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
return
case err != nil:
ctx.ServerError("org_service.RenameOrganization", err)
if org.Name != form.Name {
if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil {
if user_model.IsErrUserAlreadyExist(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
} else if db.IsErrNameReserved(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
} else if db.IsErrNamePatternNotAllowed(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
} else {
ctx.ServerError("RenameUser", err)
}
return
}
// reset ctx.org.OrgLink with new name
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(form.Name)
log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
}
// In case it's just a case change.
org.Name = form.Name
org.LowerName = strings.ToLower(form.Name)
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
return
}
}
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
Visibility: optional.Some(form.Visibility),
RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess),
}
if ctx.Doer.IsAdmin {
org.MaxRepoCreation = form.MaxRepoCreation
opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
}
org.FullName = form.FullName
org.Email = form.Email
org.Description = form.Description
org.Website = form.Website
org.Location = form.Location
org.RepoAdminChangeTeamAccess = form.RepoAdminChangeTeamAccess
visibilityChanged := org.Visibility != form.Visibility
visibilityChanged := form.Visibility != org.Visibility
org.Visibility = form.Visibility
if err := user_model.UpdateUser(ctx, org.AsUser(), false); err != nil {
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}

View File

@@ -11,6 +11,8 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/optional"
user_service "code.gitea.io/gitea/services/user"
)
// SetEditorconfigIfExists set editor config as render variable
@@ -55,8 +57,12 @@ func SetDiffViewStyle(ctx *context.Context) {
}
ctx.Data["IsSplitStyle"] = style == "split"
if err := user_model.UpdateUserDiffViewStyle(ctx, ctx.Doer, style); err != nil {
ctx.ServerError("ErrUpdateDiffViewStyle", err)
opts := &user_service.UpdateOptions{
DiffViewStyle: optional.Some(style),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
}
}

View File

@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
@@ -53,33 +54,33 @@ func AccountPost(ctx *context.Context) {
return
}
if len(form.Password) < setting.MinPasswordLength {
ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
} else if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(form.OldPassword) {
if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(form.OldPassword) {
ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
} else if form.Password != form.Retype {
ctx.Flash.Error(ctx.Tr("form.password_not_match"))
} else if !password.IsComplexEnough(form.Password) {
ctx.Flash.Error(password.BuildComplexityError(ctx.Locale))
} else if pwned, err := password.IsPwned(ctx, form.Password); pwned || err != nil {
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
ctx.Flash.Error(errMsg)
} else {
var err error
if err = ctx.Doer.SetPassword(form.Password); err != nil {
ctx.ServerError("UpdateUser", err)
return
opts := &user.UpdateAuthOptions{
Password: optional.Some(form.Password),
MustChangePassword: optional.Some(false),
}
if err := user_model.UpdateUserCols(ctx, ctx.Doer, "salt", "passwd_hash_algo", "passwd"); err != nil {
ctx.ServerError("UpdateUser", err)
return
if err := user.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
case errors.Is(err, password.ErrComplexity):
ctx.Flash.Error(password.BuildComplexityError(ctx.Locale))
case errors.Is(err, password.ErrIsPwned):
ctx.Flash.Error(ctx.Tr("auth.password_pwned"))
case password.IsErrIsPwnedRequest(err):
log.Error("%s", err.Error())
ctx.Flash.Error(ctx.Tr("auth.password_pwned_err"))
default:
ctx.ServerError("UpdateAuth", err)
return
}
} else {
ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
}
log.Trace("User password updated: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
@@ -137,7 +138,7 @@ func EmailPost(ctx *context.Context) {
// Only fired when the primary email is inactive (Wrong state)
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
} else {
mailer.SendActivateEmailMail(ctx.Doer, email)
mailer.SendActivateEmailMail(ctx.Doer, email.Email)
}
address = email.Email
@@ -160,9 +161,12 @@ func EmailPost(ctx *context.Context) {
ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
return
}
if err := user_model.SetEmailNotifications(ctx, ctx.Doer, preference); err != nil {
opts := &user.UpdateOptions{
EmailNotificationsPreference: optional.Some(preference),
}
if err := user.UpdateUser(ctx, ctx.Doer, opts); err != nil {
log.Error("Set Email Notifications failed: %v", err)
ctx.ServerError("SetEmailNotifications", err)
ctx.ServerError("UpdateUser", err)
return
}
log.Trace("Email notifications preference made %s: %s", preference, ctx.Doer.Name)
@@ -178,48 +182,47 @@ func EmailPost(ctx *context.Context) {
return
}
email := &user_model.EmailAddress{
UID: ctx.Doer.ID,
Email: form.Email,
IsActivated: !setting.Service.RegisterEmailConfirm,
}
if err := user_model.AddEmailAddress(ctx, email); err != nil {
if err := user.AddEmailAddresses(ctx, ctx.Doer, []string{form.Email}); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
return
} else if user_model.IsErrEmailCharIsNotSupported(err) ||
user_model.IsErrEmailInvalid(err) {
} else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
return
} else {
ctx.ServerError("AddEmailAddresses", err)
}
ctx.ServerError("AddEmailAddress", err)
return
}
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
mailer.SendActivateEmailMail(ctx.Doer, email)
mailer.SendActivateEmailMail(ctx.Doer, form.Email)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)))
ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", form.Email, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)))
} else {
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
}
log.Trace("Email address added: %s", email.Email)
log.Trace("Email address added: %s", form.Email)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// DeleteEmail response for delete user's email
func DeleteEmail(ctx *context.Context) {
if err := user_model.DeleteEmailAddress(ctx, &user_model.EmailAddress{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
ctx.ServerError("DeleteEmail", err)
email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
if err != nil || email == nil {
ctx.ServerError("GetEmailAddressByID", err)
return
}
if err := user.DeleteEmailAddresses(ctx, ctx.Doer, []string{email.Email}); err != nil {
ctx.ServerError("DeleteEmailAddresses", err)
return
}
log.Trace("Email address deleted: %s", ctx.Doer.Name)
@@ -293,7 +296,7 @@ func loadAccountData(ctx *context.Context) {
emails[i] = &email
}
ctx.Data["Emails"] = emails
ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotifications()
ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm

View File

@@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/typesniffer"
@@ -49,40 +50,8 @@ func Profile(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSettingsProfile)
}
// HandleUsernameChange handle username changes from user settings and admin interface
func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName string) error {
oldName := user.Name
// rename user
if err := user_service.RenameUser(ctx, user, newName); err != nil {
switch {
// Noop as username is not changed
case user_model.IsErrUsernameNotChanged(err):
ctx.Flash.Error(ctx.Tr("form.username_has_not_been_changed"))
// Non-local users are not allowed to change their username.
case user_model.IsErrUserIsNotLocal(err):
ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
case user_model.IsErrUserAlreadyExist(err):
ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
case user_model.IsErrEmailAlreadyUsed(err):
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
case db.IsErrNameReserved(err):
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
case db.IsErrNamePatternNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
case db.IsErrNameCharsNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
default:
ctx.ServerError("ChangeUserName", err)
}
return err
}
log.Trace("User name changed: %s -> %s", oldName, newName)
return nil
}
// ProfilePost response for change user's profile
func ProfilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateProfileForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
@@ -93,29 +62,40 @@ func ProfilePost(ctx *context.Context) {
return
}
if len(form.Name) != 0 && ctx.Doer.Name != form.Name {
log.Debug("Changing name for %s to %s", ctx.Doer.Name, form.Name)
if err := HandleUsernameChange(ctx, ctx.Doer, form.Name); err != nil {
form := web.GetForm(ctx).(*forms.UpdateProfileForm)
if form.Name != "" {
if err := user_service.RenameUser(ctx, ctx.Doer, form.Name); err != nil {
switch {
case user_model.IsErrUserIsNotLocal(err):
ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
case user_model.IsErrUserAlreadyExist(err):
ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
case db.IsErrNameReserved(err):
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", form.Name))
case db.IsErrNamePatternNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", form.Name))
case db.IsErrNameCharsNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", form.Name))
default:
ctx.ServerError("RenameUser", err)
return
}
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
ctx.Doer.Name = form.Name
ctx.Doer.LowerName = strings.ToLower(form.Name)
}
ctx.Doer.FullName = form.FullName
ctx.Doer.KeepEmailPrivate = form.KeepEmailPrivate
ctx.Doer.Website = form.Website
ctx.Doer.Location = form.Location
ctx.Doer.Description = form.Description
ctx.Doer.KeepActivityPrivate = form.KeepActivityPrivate
ctx.Doer.Visibility = form.Visibility
if err := user_model.UpdateUserSetting(ctx, ctx.Doer); err != nil {
if _, ok := err.(user_model.ErrEmailAlreadyUsed); ok {
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
KeepEmailPrivate: optional.Some(form.KeepEmailPrivate),
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
Visibility: optional.Some(form.Visibility),
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
@@ -170,7 +150,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
}
if err := user_model.UpdateUserCols(ctx, ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
return fmt.Errorf("UpdateUser: %w", err)
return fmt.Errorf("UpdateUserCols: %w", err)
}
return nil
@@ -371,14 +351,15 @@ func UpdateUIThemePost(ctx *context.Context) {
return
}
if err := user_model.UpdateUserTheme(ctx, ctx.Doer, form.Theme); err != nil {
opts := &user_service.UpdateOptions{
Theme: optional.Some(form.Theme),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
return
} else {
ctx.Flash.Success(ctx.Tr("settings.theme_update_success"))
}
log.Trace("Update user theme: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.theme_update_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}
@@ -388,17 +369,19 @@ func UpdateUserLang(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAppearance"] = true
if len(form.Language) != 0 {
if form.Language != "" {
if !util.SliceContainsString(setting.Langs, form.Language) {
ctx.Flash.Error(ctx.Tr("settings.update_language_not_found", form.Language))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
return
}
ctx.Doer.Language = form.Language
}
if err := user_model.UpdateUserSetting(ctx, ctx.Doer); err != nil {
ctx.ServerError("UpdateUserSetting", err)
opts := &user_service.UpdateOptions{
Language: optional.Some(form.Language),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}