Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

309 lines
10 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"errors"
"net/http"
"time"
org_model "github.com/hanzoai/git/models/organization"
packages_model "github.com/hanzoai/git/models/packages"
repo_model "github.com/hanzoai/git/models/repo"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/auth/password"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/modules/optional"
"github.com/hanzoai/git/modules/setting"
"github.com/hanzoai/git/modules/templates"
"github.com/hanzoai/git/modules/timeutil"
"github.com/hanzoai/git/modules/web"
"github.com/hanzoai/git/services/auth"
"github.com/hanzoai/git/services/context"
"github.com/hanzoai/git/services/forms"
"github.com/hanzoai/git/services/mailer"
"github.com/hanzoai/git/services/user"
)
const (
tplSettingsAccount templates.TplName = "user/settings/account"
)
// Account renders change user's password, user's email and user suicide page
func Account(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials, setting.UserFeatureDeletion) {
ctx.NotFound(errors.New("account setting are not allowed to be changed"))
return
}
ctx.Data["Title"] = ctx.Tr("settings.account")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.Doer.Email
loadAccountData(ctx)
ctx.HTML(http.StatusOK, tplSettingsAccount)
}
// AccountPost response for change user's password
func AccountPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.NotFound(errors.New("password setting is not allowed to be changed"))
return
}
form := web.GetForm(ctx).(*forms.ChangePasswordForm)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.Doer.Email
if ctx.HasError() {
loadAccountData(ctx)
ctx.HTML(http.StatusOK, tplSettingsAccount)
return
}
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 {
opts := &user.UpdateAuthOptions{
Password: optional.Some(form.Password),
MustChangePassword: optional.Some(false),
}
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", "https://haveibeenpwned.com/Passwords"))
case password.IsErrIsPwnedRequest(err):
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"))
}
}
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// EmailPost response for change user's email
func EmailPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.NotFound(errors.New("emails are not allowed to be changed"))
return
}
form := web.GetForm(ctx).(*forms.AddEmailForm)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.Doer.Email
// Make email address primary.
if ctx.FormString("_method") == "PRIMARY" {
if err := user_model.MakeActiveEmailPrimary(ctx, ctx.Doer.ID, ctx.FormInt64("id")); err != nil {
if user_model.IsErrEmailAddressNotExist(err) {
ctx.Flash.Error(ctx.Tr("settings.email_primary_not_found"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
ctx.ServerError("MakeEmailPrimary", err)
return
}
log.Trace("Email made primary: %s", ctx.Doer.Name)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Send activation Email
if ctx.FormString("_method") == "SENDACTIVATION" {
var address string
if ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName) {
log.Error("Send activation: activation still pending")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
id := ctx.FormInt64("id")
email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, id)
if err != nil {
log.Error("GetEmailAddressByID(%d,%d) error: %v", ctx.Doer.ID, id, err)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if email == nil {
log.Warn("Send activation failed: EmailAddress[%d] not found for user: %-v", id, ctx.Doer)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if email.IsActivated {
log.Debug("Send activation failed: email %s is already activated for user: %-v", email.Email, ctx.Doer)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if email.IsPrimary {
if ctx.Doer.IsActive && !setting.Service.RegisterEmailConfirm {
log.Debug("Send activation failed: email %s is already activated for user: %-v", email.Email, ctx.Doer)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Only fired when the primary email is inactive (Wrong state)
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
} else {
mailer.SendActivateEmailMail(ctx.Doer, email.Email)
}
address = email.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", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if ctx.HasError() {
loadAccountData(ctx)
ctx.HTML(http.StatusOK, tplSettingsAccount)
return
}
if err := user.AddEmailAddresses(ctx, ctx.Doer, []string{form.Email}); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
loadAccountData(ctx)
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
} else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
loadAccountData(ctx)
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
} else {
ctx.ServerError("AddEmailAddresses", err)
}
return
}
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
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", 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", form.Email)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// DeleteEmail response for delete user's email
func DeleteEmail(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.NotFound(errors.New("emails are not allowed to be changed"))
return
}
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)
ctx.Flash.Success(ctx.Tr("settings.email_deletion_success"))
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/account")
}
// DeleteAccount render user suicide page and response for delete user himself
func DeleteAccount(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureDeletion) {
ctx.HTTPError(http.StatusNotFound)
return
}
if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil {
switch {
case user_model.IsErrUserNotExist(err):
ctx.JSONError(ctx.Tr("form.user_not_exist"))
case errors.Is(err, auth.ErrPasswordAuth):
// Identity is Hanzo IAM's, so there is no local password to confirm
// against. The gate holds shut rather than opening on a check it can
// no longer make; re-confirmation wants an IAM step-up instead.
ctx.JSONError(err.Error())
default:
ctx.ServerError("UserSignIn", err)
}
return
}
// admin should not delete themself
if ctx.Doer.IsAdmin {
ctx.JSONError(ctx.Tr("form.admin_cannot_delete_self"))
return
}
if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil {
switch {
case repo_model.IsErrUserOwnRepos(err):
ctx.JSONError(ctx.Tr("form.still_own_repo"))
case org_model.IsErrUserHasOrgs(err):
ctx.JSONError(ctx.Tr("form.still_has_org"))
case packages_model.IsErrUserOwnPackages(err):
ctx.JSONError(ctx.Tr("form.still_own_packages"))
case user_model.IsErrDeleteLastAdminUser(err):
ctx.JSONError(ctx.Tr("auth.last_admin"))
default:
ctx.ServerError("DeleteUser", err)
}
return
}
ctx.JSONRedirect(setting.AppSubURL + "/")
}
func loadAccountData(ctx *context.Context) {
emlist, err := user_model.GetEmailAddresses(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetEmailAddresses", err)
return
}
type UserEmail struct {
user_model.EmailAddress
CanBePrimary bool
}
pendingActivation := ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName)
emails := make([]*UserEmail, len(emlist))
for i, em := range emlist {
var email UserEmail
email.EmailAddress = *em
email.CanBePrimary = em.IsActivated
emails[i] = &email
}
ctx.Data["Emails"] = emails
ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()
ctx.Data["UserDeleteWithComments"] = ctx.Doer.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now())
}
}