初始提交: Gitea 项目代码

This commit is contained in:
root
2026-05-30 22:47:36 +08:00
commit f288f76350
6116 changed files with 776822 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"errors"
"net/http"
"time"
org_model "gitea.dev/models/organization"
packages_model "gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/auth/password"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/web"
"gitea.dev/services/auth"
"gitea.dev/services/auth/source/db"
"gitea.dev/services/auth/source/smtp"
"gitea.dev/services/context"
"gitea.dev/services/forms"
"gitea.dev/services/mailer"
"gitea.dev/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
}
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.Doer.Email
if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil {
switch {
case user_model.IsErrUserNotExist(err):
loadAccountData(ctx)
ctx.RenderWithErrDeprecated(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil)
case errors.Is(err, smtp.ErrUnsupportedLoginType):
loadAccountData(ctx)
ctx.RenderWithErrDeprecated(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil)
case errors.As(err, &db.ErrUserPasswordNotSet{}):
loadAccountData(ctx)
ctx.RenderWithErrDeprecated(ctx.Tr("form.unset_password"), tplSettingsAccount, nil)
case errors.As(err, &db.ErrUserPasswordInvalid{}):
loadAccountData(ctx)
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
default:
ctx.ServerError("UserSignIn", err)
}
return
}
// admin should not delete themself
if ctx.Doer.IsAdmin {
ctx.Flash.Error(ctx.Tr("form.admin_cannot_delete_self"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil {
switch {
case repo_model.IsErrUserOwnRepos(err):
ctx.Flash.Error(ctx.Tr("form.still_own_repo"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
case org_model.IsErrUserHasOrgs(err):
ctx.Flash.Error(ctx.Tr("form.still_has_org"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
case packages_model.IsErrUserOwnPackages(err):
ctx.Flash.Error(ctx.Tr("form.still_own_packages"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
case user_model.IsErrDeleteLastAdminUser(err):
ctx.Flash.Error(ctx.Tr("auth.last_admin"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
default:
ctx.ServerError("DeleteUser", err)
}
} else {
log.Trace("Account deleted: %s", ctx.Doer.Name)
ctx.Redirect(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())
}
}
+101
View File
@@ -0,0 +1,101 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"testing"
"gitea.dev/models/unittest"
"gitea.dev/modules/setting"
"gitea.dev/modules/web"
"gitea.dev/services/contexttest"
"gitea.dev/services/forms"
"github.com/stretchr/testify/assert"
)
func TestChangePassword(t *testing.T) {
oldPassword := "password"
setting.MinPasswordLength = 6
pcALL := []string{"lower", "upper", "digit", "spec"}
pcLUN := []string{"lower", "upper", "digit"}
pcLU := []string{"lower", "upper"}
for _, req := range []struct {
OldPassword string
NewPassword string
Retype string
Message string
PasswordComplexity []string
}{
{
OldPassword: oldPassword,
NewPassword: "Qwerty123456-",
Retype: "Qwerty123456-",
Message: "",
PasswordComplexity: pcALL,
},
{
OldPassword: oldPassword,
NewPassword: "12345",
Retype: "12345",
Message: "auth.password_too_short",
PasswordComplexity: pcALL,
},
{
OldPassword: "12334",
NewPassword: "123456",
Retype: "123456",
Message: "settings.password_incorrect",
PasswordComplexity: pcALL,
},
{
OldPassword: oldPassword,
NewPassword: "123456",
Retype: "12345",
Message: "form.password_not_match",
PasswordComplexity: pcALL,
},
{
OldPassword: oldPassword,
NewPassword: "Qwerty",
Retype: "Qwerty",
Message: "form.password_complexity",
PasswordComplexity: pcALL,
},
{
OldPassword: oldPassword,
NewPassword: "Qwerty",
Retype: "Qwerty",
Message: "form.password_complexity",
PasswordComplexity: pcLUN,
},
{
OldPassword: oldPassword,
NewPassword: "QWERTY",
Retype: "QWERTY",
Message: "form.password_complexity",
PasswordComplexity: pcLU,
},
} {
t.Run(req.OldPassword+"__"+req.NewPassword, func(t *testing.T) {
unittest.PrepareTestEnv(t)
setting.PasswordComplexity = req.PasswordComplexity
ctx, _ := contexttest.MockContext(t, "user/settings/security")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.ChangePasswordForm{
OldPassword: req.OldPassword,
Password: req.NewPassword,
Retype: req.Retype,
})
AccountPost(ctx)
assert.Contains(t, ctx.Flash.ErrorMsg, req.Message)
assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
})
}
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/setting"
"gitea.dev/services/context"
repo_service "gitea.dev/services/repository"
)
// AdoptOrDeleteRepository adopts or deletes a repository
func AdoptOrDeleteRepository(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.adopt")
ctx.Data["PageIsSettingsRepos"] = true
allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
ctx.Data["allowAdopt"] = allowAdopt
allowDelete := ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
ctx.Data["allowDelete"] = allowDelete
dir := ctx.FormString("id")
action := ctx.FormString("action")
ctxUser := ctx.Doer
// check not a repo
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, dir)
if err != nil {
ctx.ServerError("IsRepositoryExist", err)
return
}
exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, dir)))
if err != nil {
ctx.ServerError("IsDir", err)
return
}
if has || !exist {
// Fallthrough to failure mode
} else if action == "adopt" && allowAdopt {
if _, err := repo_service.AdoptRepository(ctx, ctxUser, ctxUser, repo_service.CreateRepoOptions{
Name: dir,
IsPrivate: true,
}); err != nil {
ctx.ServerError("repository.AdoptRepository", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir))
} else if action == "delete" && allowDelete {
if err := repo_service.DeleteUnadoptedRepository(ctx, ctxUser, ctxUser, dir); err != nil {
ctx.ServerError("repository.AdoptRepository", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/repos")
}
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"strings"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/forms"
)
const (
tplSettingsApplications templates.TplName = "user/settings/applications"
)
// Applications render manage access token page
func Applications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsSettingsApplications"] = true
loadApplicationsData(ctx)
ctx.HTML(http.StatusOK, tplSettingsApplications)
}
// ApplicationsPost response for add user's access token
func ApplicationsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewAccessTokenForm)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsApplications"] = true
_ = ctx.Req.ParseForm()
var scopeNames []string
const accessTokenScopePrefix = "scope-"
for k, v := range ctx.Req.Form {
if strings.HasPrefix(k, accessTokenScopePrefix) {
scopeNames = append(scopeNames, v...)
}
}
scope, err := auth_model.AccessTokenScope(strings.Join(scopeNames, ",")).Normalize()
if err != nil {
ctx.ServerError("GetScope", err)
return
}
if !scope.HasPermissionScope() {
ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true)
}
if ctx.HasError() {
loadApplicationsData(ctx)
ctx.HTML(http.StatusOK, tplSettingsApplications)
return
}
t := &auth_model.AccessToken{
UID: ctx.Doer.ID,
Name: form.Name,
Scope: scope,
}
exist, err := auth_model.AccessTokenByNameExists(ctx, t)
if err != nil {
ctx.ServerError("AccessTokenByNameExists", err)
return
}
if exist {
ctx.Flash.Error(ctx.Tr("settings.generate_token_name_duplicate", t.Name))
ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
return
}
if err := auth_model.NewAccessToken(ctx, t); err != nil {
ctx.ServerError("NewAccessToken", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
ctx.Flash.Info(t.Token)
ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
}
// DeleteApplication response for delete user access token
func DeleteApplication(ctx *context.Context) {
if err := auth_model.DeleteAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID); err != nil {
ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications")
}
func loadApplicationsData(ctx *context.Context) {
ctx.Data["AccessTokenScopePublicOnly"] = auth_model.AccessTokenScopePublicOnly
tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID})
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
}
ctx.Data["Tokens"] = tokens
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enabled
// Handle specific ordered token categories for admin or non-admin users
tokenCategoryNames := auth_model.GetAccessTokenCategories()
if !ctx.Doer.IsAdmin {
tokenCategoryNames = util.SliceRemoveAll(tokenCategoryNames, "admin")
}
ctx.Data["TokenCategories"] = tokenCategoryNames
if setting.OAuth2.Enabled {
ctx.Data["Applications"], err = db.Find[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{
OwnerID: ctx.Doer.ID,
})
if err != nil {
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
return
}
ctx.Data["Grants"], err = auth_model.GetOAuth2GrantsByUserID(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetOAuth2GrantsByUserID", err)
return
}
}
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
shared_user "gitea.dev/routers/web/shared/user"
"gitea.dev/services/context"
)
const (
tplSettingsBlockedUsers templates.TplName = "user/settings/blocked_users"
)
func BlockedUsers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("user.block.list")
ctx.Data["PageIsSettingsBlockedUsers"] = true
shared_user.BlockedUsers(ctx, ctx.Doer)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
}
func BlockedUsersPost(ctx *context.Context) {
shared_user.BlockedUsersPost(ctx, ctx.Doer, setting.AppSubURL+"/user/settings/blocked_users")
}
+342
View File
@@ -0,0 +1,342 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"errors"
"net/http"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/web"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
"gitea.dev/services/forms"
)
const (
tplSettingsKeys templates.TplName = "user/settings/keys"
)
// Keys render user's SSH/GPG public keys page
func Keys(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) {
ctx.NotFound(errors.New("keys setting is not allowed to be changed"))
return
}
ctx.Data["Title"] = ctx.Tr("settings.ssh_gpg_keys")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
loadKeysData(ctx)
ctx.HTML(http.StatusOK, tplSettingsKeys)
}
// KeysPost response for change user's SSH/GPG keys
func KeysPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AddKeyForm)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
if ctx.HasError() {
loadKeysData(ctx)
ctx.HTML(http.StatusOK, tplSettingsKeys)
return
}
switch form.Type {
case "principal":
content, err := asymkey_model.CheckPrincipalKeyString(ctx, ctx.Doer, form.Content)
if err != nil {
if db.IsErrSSHDisabled(err) {
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
} else {
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_principal", err.Error()))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
return
}
if _, err = asymkey_service.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil {
ctx.Data["HasPrincipalError"] = true
switch {
case asymkey_model.IsErrKeyAlreadyExist(err), asymkey_model.IsErrKeyNameAlreadyUsed(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form)
default:
ctx.ServerError("AddPrincipalKey", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "gpg":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
ctx.NotFound(errors.New("gpg keys setting is not allowed to be visited"))
return
}
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
keys, err := asymkey_model.AddGPGKey(ctx, ctx.Doer.ID, form.Content, token, form.Signature)
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
keys, err = asymkey_model.AddGPGKey(ctx, ctx.Doer.ID, form.Content, lastToken, form.Signature)
}
if err != nil {
ctx.Data["HasGPGError"] = true
switch {
case asymkey_model.IsErrGPGKeyParsing(err):
ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
case asymkey_model.IsErrGPGInvalidTokenSignature(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.Data["Err_Signature"] = true
keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
ctx.Data["KeyID"] = keyID
ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID)
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
case asymkey_model.IsErrGPGNoEmailFound(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.Data["Err_Signature"] = true
keyID := err.(asymkey_model.ErrGPGNoEmailFound).ID
ctx.Data["KeyID"] = keyID
ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID)
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
default:
ctx.ServerError("AddPublicKey", err)
}
return
}
keyIDs := ""
for _, key := range keys {
keyIDs += key.KeyID
keyIDs += ", "
}
if len(keyIDs) > 0 {
keyIDs = keyIDs[:len(keyIDs)-2]
}
ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "verify_gpg":
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
keyID, err := asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, token, form.Signature)
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
keyID, err = asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, lastToken, form.Signature)
}
if err != nil {
ctx.Data["HasGPGVerifyError"] = true
switch {
case asymkey_model.IsErrGPGInvalidTokenSignature(err):
loadKeysData(ctx)
ctx.Data["VerifyingID"] = form.KeyID
ctx.Data["Err_Signature"] = true
keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
ctx.Data["KeyID"] = keyID
ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID)
ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
default:
ctx.ServerError("VerifyGPG", err)
}
}
ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
return
}
content, err := asymkey_model.CheckPublicKeyString(form.Content)
if err != nil {
if db.IsErrSSHDisabled(err) {
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
} else if asymkey_model.IsErrKeyUnableVerify(err) {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
} else if err == asymkey_model.ErrKeyIsPrivate {
ctx.Flash.Error(ctx.Tr("form.must_use_public_key"))
} else {
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
return
}
if _, err = asymkey_model.AddPublicKey(ctx, ctx.Doer.ID, form.Title, content, 0, false); err != nil {
ctx.Data["HasSSHError"] = true
switch {
case asymkey_model.IsErrKeyAlreadyExist(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
loadKeysData(ctx)
ctx.Data["Err_Title"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
case asymkey_model.IsErrKeyUnableVerify(err):
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
default:
ctx.ServerError("AddPublicKey", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "verify_ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
return
}
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
fingerprint, err := asymkey_model.VerifySSHKey(ctx, ctx.Doer.ID, form.Fingerprint, token, form.Signature)
if err != nil && asymkey_model.IsErrSSHInvalidTokenSignature(err) {
fingerprint, err = asymkey_model.VerifySSHKey(ctx, ctx.Doer.ID, form.Fingerprint, lastToken, form.Signature)
}
if err != nil {
ctx.Data["HasSSHVerifyError"] = true
switch {
case asymkey_model.IsErrSSHInvalidTokenSignature(err):
loadKeysData(ctx)
ctx.Data["Err_Signature"] = true
ctx.Data["Fingerprint"] = err.(asymkey_model.ErrSSHInvalidTokenSignature).Fingerprint
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_invalid_token_signature"), tplSettingsKeys, &form)
default:
ctx.ServerError("VerifySSH", err)
}
}
ctx.Flash.Success(ctx.Tr("settings.verify_ssh_key_success", fingerprint))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
default:
ctx.Flash.Warning("Function not implemented")
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
}
}
// DeleteKey response for delete user's SSH/GPG key
func DeleteKey(ctx *context.Context) {
switch ctx.FormString("type") {
case "gpg":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
ctx.NotFound(errors.New("gpg keys setting is not allowed to be visited"))
return
}
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteGPGKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
}
case "ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
return
}
keyID := ctx.FormInt64("id")
external, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, keyID)
if err != nil {
ctx.ServerError("sshKeysExternalManaged", err)
return
}
if external {
ctx.Flash.Error(ctx.Tr("settings.ssh_externally_managed"))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
return
}
if err := asymkey_service.DeletePublicKey(ctx, ctx.Doer, keyID); err != nil {
ctx.Flash.Error("DeletePublicKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
}
case "principal":
if err := asymkey_service.DeletePublicKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeletePublicKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.ssh_principal_deletion_success"))
}
default:
ctx.Flash.Warning("Function not implemented")
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/keys")
}
func loadKeysData(ctx *context.Context) {
keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
OwnerID: ctx.Doer.ID,
NotKeytype: asymkey_model.KeyTypePrincipal,
})
if err != nil {
ctx.ServerError("ListPublicKeys", err)
return
}
ctx.Data["Keys"] = keys
externalKeys, err := asymkey_model.PublicKeysAreExternallyManaged(ctx, keys)
if err != nil {
ctx.ServerError("ListPublicKeys", err)
return
}
ctx.Data["ExternalKeys"] = externalKeys
gpgkeys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
ListOptions: db.ListOptionsAll,
OwnerID: ctx.Doer.ID,
})
if err != nil {
ctx.ServerError("ListGPGKeys", err)
return
}
if err := asymkey_model.GPGKeyList(gpgkeys).LoadSubKeys(ctx); err != nil {
ctx.ServerError("LoadSubKeys", err)
return
}
ctx.Data["GPGKeys"] = gpgkeys
tokenToSign := asymkey_model.VerificationToken(ctx.Doer, 1)
// generate a new aes cipher using the token
ctx.Data["TokenToSign"] = tokenToSign
principals, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
ListOptions: db.ListOptionsAll,
OwnerID: ctx.Doer.ID,
KeyTypes: []asymkey_model.KeyType{asymkey_model.KeyTypePrincipal},
})
if err != nil {
ctx.ServerError("ListPrincipalKeys", err)
return
}
ctx.Data["Principals"] = principals
ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"testing"
"gitea.dev/models/unittest"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/services/context"
"gitea.dev/services/user"
)
const tplSettingsNotifications templates.TplName = "user/settings/notifications"
// Notifications render user's notifications settings
func Notifications(ctx *context.Context) {
if !setting.Service.EnableNotifyMail {
ctx.NotFound(nil)
return
}
ctx.Data["Title"] = ctx.Tr("notifications")
ctx.Data["PageIsSettingsNotifications"] = true
ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
actionsEmailPref, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyEmailNotificationGiteaActions, user_model.SettingEmailNotificationGiteaActionsFailureOnly)
if err != nil {
ctx.ServerError("GetUserSetting", err)
return
}
ctx.Data["ActionsEmailNotificationsPreference"] = actionsEmailPref
ctx.HTML(http.StatusOK, tplSettingsNotifications)
}
// NotificationsEmailPost set user's email notification preference
func NotificationsEmailPost(ctx *context.Context) {
if !setting.Service.EnableNotifyMail {
ctx.NotFound(nil)
return
}
preference := ctx.FormString("preference")
if !(preference == user_model.EmailNotificationsEnabled ||
preference == user_model.EmailNotificationsOnMention ||
preference == user_model.EmailNotificationsDisabled ||
preference == user_model.EmailNotificationsAndYourOwn) {
ctx.Flash.Error(ctx.Tr("invalid_data", preference))
ctx.Redirect(setting.AppSubURL + "/user/settings/notifications")
return
}
opts := &user.UpdateOptions{
EmailNotificationsPreference: optional.Some(preference),
}
if err := user.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/notifications")
}
// NotificationsActionsEmailPost set user's email notification preference on Gitea Actions
func NotificationsActionsEmailPost(ctx *context.Context) {
if !setting.Actions.Enabled || unit.TypeActions.UnitGlobalDisabled() {
ctx.NotFound(nil)
return
}
preference := ctx.FormString("preference")
if !(preference == user_model.SettingEmailNotificationGiteaActionsAll ||
preference == user_model.SettingEmailNotificationGiteaActionsDisabled ||
preference == user_model.SettingEmailNotificationGiteaActionsFailureOnly) {
ctx.Flash.Error(ctx.Tr("invalid_data", preference))
ctx.Redirect(setting.AppSubURL + "/user/settings/notifications")
return
}
if err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyEmailNotificationGiteaActions, preference); err != nil {
ctx.ServerError("SetUserSetting", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/notifications")
}
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/services/context"
)
const (
tplSettingsOAuthApplicationEdit templates.TplName = "user/settings/applications_oauth2_edit"
)
func newOAuth2CommonHandlers(userID int64) *OAuth2CommonHandlers {
return &OAuth2CommonHandlers{
OwnerID: userID,
BasePathList: setting.AppSubURL + "/user/settings/applications",
BasePathEditPrefix: setting.AppSubURL + "/user/settings/applications/oauth2",
TplAppEdit: tplSettingsOAuthApplicationEdit,
}
}
// OAuthApplicationsPost response for adding a oauth2 application
func OAuthApplicationsPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsApplications"] = true
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
oa.AddApp(ctx)
}
// OAuthApplicationsEdit response for editing oauth2 application
func OAuthApplicationsEdit(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsApplications"] = true
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
oa.EditSave(ctx)
}
// OAuthApplicationsRegenerateSecret handles the post request for regenerating the secret
func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsApplications"] = true
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
oa.RegenerateSecret(ctx)
}
// OAuth2ApplicationShow displays the given application
func OAuth2ApplicationShow(ctx *context.Context) {
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
oa.EditShow(ctx)
}
// DeleteOAuth2Application deletes the given oauth2 application
func DeleteOAuth2Application(ctx *context.Context) {
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
oa.DeleteApp(ctx)
}
// RevokeOAuth2Grant revokes the grant with the given id
func RevokeOAuth2Grant(ctx *context.Context) {
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
oa.RevokeGrant(ctx)
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"fmt"
"net/http"
"gitea.dev/models/auth"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
shared_user "gitea.dev/routers/web/shared/user"
"gitea.dev/services/context"
"gitea.dev/services/forms"
)
type OAuth2CommonHandlers struct {
OwnerID int64 // 0 for instance-wide, otherwise OrgID or UserID
BasePathList string // the base URL for the application list page, eg: "/user/setting/applications"
BasePathEditPrefix string // the base URL for the application edit page, will be appended with app id, eg: "/user/setting/applications/oauth2"
TplAppEdit templates.TplName // the template for the application edit page
}
func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) {
app := ctx.Data["App"].(*auth.OAuth2Application)
ctx.Data["FormActionPath"] = fmt.Sprintf("%s/%d", oa.BasePathEditPrefix, app.ID)
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() {
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
return
}
}
ctx.HTML(http.StatusOK, oa.TplAppEdit)
}
// AddApp adds an oauth2 application
func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
if ctx.HasError() {
ctx.Flash.Error(ctx.GetErrMsg())
// go to the application list page
ctx.Redirect(oa.BasePathList)
return
}
app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{
Name: form.Name,
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
UserID: oa.OwnerID,
ConfidentialClient: form.ConfidentialClient,
SkipSecondaryAuthorization: form.SkipSecondaryAuthorization,
})
if err != nil {
ctx.ServerError("CreateOAuth2Application", err)
return
}
// render the edit page with secret
ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"), true)
ctx.Data["App"] = app
ctx.Data["ClientSecret"], err = app.GenerateClientSecret(ctx)
if err != nil {
ctx.ServerError("GenerateClientSecret", err)
return
}
oa.renderEditPage(ctx)
}
// EditShow displays the given application
func (oa *OAuth2CommonHandlers) EditShow(ctx *context.Context) {
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if auth.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound(err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != oa.OwnerID {
ctx.NotFound(nil)
return
}
ctx.Data["App"] = app
oa.renderEditPage(ctx)
}
// EditSave saves the oauth2 application
func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
if ctx.HasError() {
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if auth.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound(err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != oa.OwnerID {
ctx.NotFound(nil)
return
}
ctx.Data["App"] = app
oa.renderEditPage(ctx)
return
}
var err error
if ctx.Data["App"], err = auth.UpdateOAuth2Application(ctx, auth.UpdateOAuth2ApplicationOptions{
ID: ctx.PathParamInt64("id"),
Name: form.Name,
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
UserID: oa.OwnerID,
ConfidentialClient: form.ConfidentialClient,
SkipSecondaryAuthorization: form.SkipSecondaryAuthorization,
}); err != nil {
ctx.ServerError("UpdateOAuth2Application", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
ctx.Redirect(oa.BasePathList)
}
// RegenerateSecret regenerates the secret
func (oa *OAuth2CommonHandlers) RegenerateSecret(ctx *context.Context) {
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if auth.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound(err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != oa.OwnerID {
ctx.NotFound(nil)
return
}
ctx.Data["App"] = app
ctx.Data["ClientSecret"], err = app.GenerateClientSecret(ctx)
if err != nil {
ctx.ServerError("GenerateClientSecret", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"), true)
oa.renderEditPage(ctx)
}
// DeleteApp deletes the given oauth2 application
func (oa *OAuth2CommonHandlers) DeleteApp(ctx *context.Context) {
if err := auth.DeleteOAuth2Application(ctx, ctx.PathParamInt64("id"), oa.OwnerID); err != nil {
ctx.ServerError("DeleteOAuth2Application", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
ctx.JSONRedirect(oa.BasePathList)
}
// RevokeGrant revokes the grant
func (oa *OAuth2CommonHandlers) RevokeGrant(ctx *context.Context) {
if err := auth.RevokeOAuth2Grant(ctx, ctx.PathParamInt64("grantId"), oa.OwnerID); err != nil {
ctx.ServerError("RevokeOAuth2Grant", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.revoke_oauth2_grant_success"))
ctx.JSONRedirect(oa.BasePathList)
}
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"strings"
user_model "gitea.dev/models/user"
chef_module "gitea.dev/modules/packages/chef"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
shared "gitea.dev/routers/web/shared/packages"
"gitea.dev/services/context"
)
const (
tplSettingsPackages templates.TplName = "user/settings/packages"
tplSettingsPackagesRuleEdit templates.TplName = "user/settings/packages_cleanup_rules_edit"
tplSettingsPackagesRulePreview templates.TplName = "user/settings/packages_cleanup_rules_preview"
)
func Packages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.SetPackagesContext(ctx, ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsPackages)
}
func PackagesRuleAdd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.SetRuleAddContext(ctx)
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
}
func PackagesRuleEdit(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.SetRuleEditContext(ctx, ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
}
func PackagesRuleAddPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsPackages"] = true
shared.PerformRuleAddPost(
ctx,
ctx.Doer,
setting.AppSubURL+"/user/settings/packages",
tplSettingsPackagesRuleEdit,
)
}
func PackagesRuleEditPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.PerformRuleEditPost(
ctx,
ctx.Doer,
setting.AppSubURL+"/user/settings/packages",
tplSettingsPackagesRuleEdit,
)
}
func PackagesRulePreview(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.SetRulePreviewContext(ctx, ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview)
}
func InitializeCargoIndex(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.InitializeCargoIndex(ctx, ctx.Doer)
ctx.Redirect(setting.AppSubURL + "/user/settings/packages")
}
func RebuildCargoIndex(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.RebuildCargoIndex(ctx, ctx.Doer)
ctx.Redirect(setting.AppSubURL + "/user/settings/packages")
}
func RegenerateChefKeyPair(ctx *context.Context) {
priv, pub, err := util.GenerateKeyPair(chef_module.KeyBits)
if err != nil {
ctx.ServerError("GenerateKeyPair", err)
return
}
if err := user_model.SetUserSetting(ctx, ctx.Doer.ID, chef_module.SettingPublicPem, pub); err != nil {
ctx.ServerError("SetUserSetting", err)
return
}
ctx.ServeContent(strings.NewReader(priv), context.ServeHeaderOptions{
ContentType: "application/x-pem-file",
Filename: ctx.Doer.Name + ".priv",
})
}
+425
View File
@@ -0,0 +1,425 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"errors"
"fmt"
"io"
"math/big"
"net/http"
"os"
"path/filepath"
"strings"
"gitea.dev/models/avatars"
"gitea.dev/models/db"
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/templates"
"gitea.dev/modules/translation"
"gitea.dev/modules/typesniffer"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/modules/web/middleware"
"gitea.dev/services/context"
"gitea.dev/services/forms"
user_service "gitea.dev/services/user"
"gitea.dev/services/webtheme"
)
const (
tplSettingsProfile templates.TplName = "user/settings/profile"
tplSettingsAppearance templates.TplName = "user/settings/appearance"
tplSettingsOrganization templates.TplName = "user/settings/organization"
tplSettingsRepositories templates.TplName = "user/settings/repos"
)
// Profile render user's profile page
func Profile(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.profile")
ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
ctx.HTML(http.StatusOK, tplSettingsProfile)
}
// ProfilePost response for change user's profile
func ProfilePost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsProfile"] = true
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsProfile)
return
}
form := web.GetForm(ctx).(*forms.UpdateProfileForm)
if form.Name != "" {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeUsername) {
ctx.Flash.Error(ctx.Tr("user.form.change_username_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
if err := user_service.RenameUser(ctx, ctx.Doer, form.Name, ctx.Doer); 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
}
}
opts := &user_service.UpdateOptions{
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 form.FullName != "" {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeFullName) {
ctx.Flash.Error(ctx.Tr("user.form.change_full_name_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
opts.FullName = optional.Some(form.FullName)
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
log.Trace("User settings updated: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
// UpdateAvatarSetting update user's avatar
// FIXME: limit size.
func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *user_model.User) error {
ctxUser.UseCustomAvatar = form.Source == forms.AvatarLocal
if len(form.Gravatar) > 0 {
if form.Avatar != nil {
ctxUser.Avatar = avatars.HashEmail(form.Gravatar)
} else {
ctxUser.Avatar = ""
}
ctxUser.AvatarEmail = form.Gravatar
}
if form.Avatar != nil && form.Avatar.Filename != "" {
fr, err := form.Avatar.Open()
if err != nil {
return fmt.Errorf("Avatar.Open: %w", err)
}
defer fr.Close()
if form.Avatar.Size > setting.Avatar.MaxFileSize {
return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
}
data, err := io.ReadAll(fr)
if err != nil {
return fmt.Errorf("io.ReadAll: %w", err)
}
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
}
if err = user_service.UploadAvatar(ctx, ctxUser, data); err != nil {
return fmt.Errorf("UploadAvatar: %w", err)
}
} else if ctxUser.UseCustomAvatar && ctxUser.Avatar == "" {
// No avatar is uploaded but setting has been changed to enable,
// generate a random one when needed.
if err := user_model.GenerateRandomAvatar(ctx, ctxUser); err != nil {
log.Error("GenerateRandomAvatar[%d]: %v", ctxUser.ID, err)
}
}
if err := user_model.UpdateUserCols(ctx, ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
return fmt.Errorf("UpdateUserCols: %w", err)
}
return nil
}
// AvatarPost response for change user's avatar request
func AvatarPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AvatarForm)
if err := UpdateAvatarSetting(ctx, form, ctx.Doer); err != nil {
ctx.Flash.Error(err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.update_avatar_success"))
}
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
// DeleteAvatar render delete avatar page
func DeleteAvatar(ctx *context.Context) {
if err := user_service.DeleteAvatar(ctx, ctx.Doer); err != nil {
ctx.Flash.Error(err.Error())
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings")
}
// Organization render all the organization of the user
func Organization(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.organization")
ctx.Data["PageIsSettingsOrganization"] = true
opts := organization.FindOrgOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.Admin.UserPagingNum,
Page: ctx.FormInt("page"),
},
UserID: ctx.Doer.ID,
IncludeVisibility: structs.VisibleTypePrivate,
}
if opts.Page <= 0 {
opts.Page = 1
}
orgs, total, err := db.FindAndCount[organization.Organization](ctx, opts)
if err != nil {
ctx.ServerError("FindOrgs", err)
return
}
ctx.Data["Orgs"] = orgs
pager := context.NewPagination(total, opts.PageSize, opts.Page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSettingsOrganization)
}
// Repos display a list of all repositories of the user
func Repos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.repos")
ctx.Data["PageIsSettingsRepos"] = true
ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
opts := db.ListOptions{
PageSize: setting.UI.Admin.UserPagingNum,
Page: ctx.FormInt("page"),
}
if opts.Page <= 0 {
opts.Page = 1
}
start := int64((opts.Page - 1) * opts.PageSize)
end := start + int64(opts.PageSize)
adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories)
ctxUser := ctx.Doer
var count int64
if adoptOrDelete {
repoNames := make([]string, 0, setting.UI.Admin.UserPagingNum)
repos := map[string]*repo_model.Repository{}
// We're going to iterate by pagesize.
root := user_model.UserPath(ctxUser.Name)
if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if !d.IsDir() || path == root {
return nil
}
name := d.Name()
if !strings.HasSuffix(name, ".git") {
return filepath.SkipDir
}
name = name[:len(name)-4]
if repo_model.IsUsableRepoName(name) != nil || strings.ToLower(name) != name {
return filepath.SkipDir
}
if count >= start && count < end {
repoNames = append(repoNames, name)
}
count++
return filepath.SkipDir
}); err != nil {
ctx.ServerError("filepath.WalkDir", err)
return
}
userRepos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
Actor: ctxUser,
Private: true,
ListOptions: db.ListOptions{
Page: 1,
PageSize: setting.UI.Admin.UserPagingNum,
},
LowerNames: repoNames,
})
if err != nil {
ctx.ServerError("GetUserRepositories", err)
return
}
for _, repo := range userRepos {
if repo.IsFork {
if err := repo.GetBaseRepo(ctx); err != nil {
ctx.ServerError("GetBaseRepo", err)
return
}
}
repos[repo.LowerName] = repo
}
ctx.Data["Dirs"] = repoNames
ctx.Data["ReposMap"] = repos
} else {
repos, reposCount, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
if err != nil {
ctx.ServerError("GetUserRepositories", err)
return
}
count = reposCount
for i := range repos {
if repos[i].IsFork {
if err := repos[i].GetBaseRepo(ctx); err != nil {
ctx.ServerError("GetBaseRepo", err)
return
}
}
}
ctx.Data["Repos"] = repos
}
ctx.Data["ContextUser"] = ctxUser
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSettingsRepositories)
}
// Appearance render user's appearance settings
func Appearance(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.appearance")
ctx.Data["PageIsSettingsAppearance"] = true
ctx.Data["AllThemes"] = webtheme.GetAvailableThemes()
var hiddenCommentTypes *big.Int
val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
if err != nil {
ctx.ServerError("GetUserSetting", err)
return
}
hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here
ctx.Data["IsCommentTypeGroupChecked"] = func(commentTypeGroup string) bool {
return forms.IsUserHiddenCommentTypeGroupChecked(commentTypeGroup, hiddenCommentTypes)
}
ctx.HTML(http.StatusOK, tplSettingsAppearance)
}
// UpdateUIThemePost is used to update users' specific theme
func UpdateUIThemePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateThemeForm)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsAppearance"] = true
if ctx.HasError() {
ctx.Flash.Error(ctx.GetErrMsg())
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
return
}
if webtheme.GetThemeMetaInfo(form.Theme) == nil {
ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
return
}
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"))
} else {
ctx.Flash.Success(ctx.Tr("settings.theme_update_success"))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}
// UpdateUserLang update a user's language
func UpdateUserLang(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateLanguageForm)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsAppearance"] = true
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
}
}
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
}
// Update the language to the one we just set
middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0)
log.Trace("User settings updated: %s", ctx.Doer.Name)
ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).TrString("settings.update_language_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}
// UpdateUserHiddenComments update a user's shown comment types
func UpdateUserHiddenComments(ctx *context.Context) {
err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes, forms.UserHiddenCommentTypesFromRequest(ctx).String())
if err != nil {
ctx.ServerError("SetUserSetting", err)
return
}
log.Trace("User settings updated: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.saved_successfully"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}
+280
View File
@@ -0,0 +1,280 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"bytes"
"encoding/base64"
"html/template"
"image/png"
"net/http"
"strings"
"gitea.dev/models/auth"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/session"
"gitea.dev/modules/setting"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/forms"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
)
// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
func RegenerateScratchTwoFactor(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
ctx.Flash.Error(ctx.Tr("settings.twofa_not_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
} else {
ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
}
return
}
token, err := t.GenerateScratchToken()
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to GenerateScratchToken", err)
return
}
if err = auth.UpdateTwoFactor(ctx, t); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to UpdateTwoFactor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
// DisableTwoFactor deletes the user's 2FA settings.
func DisableTwoFactor(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
ctx.Flash.Error(ctx.Tr("settings.twofa_not_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
} else {
ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
}
return
}
if err = auth.DeleteTwoFactorByID(ctx, t.ID, ctx.Doer.ID); err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
// There is a potential DB race here - we must have been disabled by another request in the intervening period
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
} else {
ctx.ServerError("SettingsTwoFactor: Failed to DeleteTwoFactorByID", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
func twofaGenerateSecretAndQr(ctx *context.Context) bool {
var otpKey *otp.Key
var err error
uri := ctx.Session.Get("twofaUri")
if uri != nil {
otpKey, err = otp.NewKeyFromURL(uri.(string))
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed NewKeyFromURL: ", err)
return false
}
}
// Filter unsafe character ':' in issuer
issuer := strings.ReplaceAll(setting.AppName+" ("+setting.Domain+")", ":", "")
if otpKey == nil {
otpKey, err = totp.Generate(totp.GenerateOpts{
SecretSize: 40,
Issuer: issuer,
AccountName: ctx.Doer.Name,
})
if err != nil {
ctx.ServerError("SettingsTwoFactor: totpGenerate Failed", err)
return false
}
}
ctx.Data["TwofaSecret"] = otpKey.Secret()
img, err := otpKey.Image(320, 240)
if err != nil {
ctx.ServerError("SettingsTwoFactor: otpKey image generation failed", err)
return false
}
var imgBytes bytes.Buffer
if err = png.Encode(&imgBytes, img); err != nil {
ctx.ServerError("SettingsTwoFactor: otpKey png encoding failed", err)
return false
}
ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
if err := ctx.Session.Set("twofaSecret", otpKey.Secret()); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaSecret", err)
return false
}
if err := ctx.Session.Set("twofaUri", otpKey.String()); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaUri", err)
return false
}
// Here we're just going to try to release the session early
if err := ctx.Session.Release(); err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log.Error("Unable to save changes to the session: %v", err)
}
return true
}
// EnrollTwoFactor shows the page where the user can enroll into 2FA.
func EnrollTwoFactor(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsSecurity"] = true
ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil {
// already enrolled - we should redirect back!
log.Warn("Trying to re-enroll %-v in twofa when already enrolled", ctx.Doer)
ctx.Flash.Error(ctx.Tr("settings.twofa_is_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
}
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor: GetTwoFactorByUID", err)
return
}
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
}
// EnrollTwoFactorPost handles enrolling the user into 2FA.
func EnrollTwoFactorPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsSecurity"] = true
ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil {
// already enrolled
ctx.Flash.Error(ctx.Tr("settings.twofa_is_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
}
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor: Failed to check if already enrolled with GetTwoFactorByUID", err)
return
}
if ctx.HasError() {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
return
}
secretRaw := ctx.Session.Get("twofaSecret")
if secretRaw == nil {
ctx.Flash.Error(ctx.Tr("settings.twofa_failed_get_secret"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
return
}
secret := secretRaw.(string)
if !totp.Validate(form.Passcode, secret) {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
return
}
t = &auth.TwoFactor{
UID: ctx.Doer.ID,
}
err = t.SetSecret(secret)
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to set secret", err)
return
}
token, err := t.GenerateScratchToken()
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to generate scratch token", err)
return
}
newTwoFactorErr := auth.NewTwoFactor(ctx, t)
if newTwoFactorErr == nil {
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
}
// Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
// If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
if err := ctx.Session.Delete("twofaSecret"); err != nil {
// tolerate this failure - it's more important to continue
log.Error("Unable to delete twofaSecret from the session: Error: %v", err)
}
if err := ctx.Session.Delete("twofaUri"); err != nil {
// tolerate this failure - it's more important to continue
log.Error("Unable to delete twofaUri from the session: Error: %v", err)
}
if err := ctx.Session.Release(); err != nil {
// tolerate this failure - it's more important to continue
log.Error("Unable to save changes to the session: %v", err)
}
if newTwoFactorErr != nil {
// FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
// If there is a unique constraint fail we should just tolerate the error
ctx.ServerError("SettingsTwoFactor: Failed to save two factor", newTwoFactorErr)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
@@ -0,0 +1,14 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"testing"
"gitea.dev/models/unittest"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"errors"
"net/http"
user_model "gitea.dev/models/user"
"gitea.dev/modules/auth/openid"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/forms"
)
// OpenIDPost response for change user's openid
func OpenIDPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.AddOpenIDForm)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsSecurity"] = true
if ctx.HasError() {
loadSecurityData(ctx)
ctx.HTML(http.StatusOK, tplSettingsSecurity)
return
}
// WARNING: specifying a wrong OpenID here could lock
// a user out of her account, would be better to
// verify/confirm the new OpenID before storing it
// Also, consider allowing for multiple OpenID URIs
id, err := openid.Normalize(form.Openid)
if err != nil {
loadSecurityData(ctx)
ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &form)
return
}
form.Openid = id
log.Trace("Normalized id: " + id)
oids, err := user_model.GetUserOpenIDs(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = oids
// Check that the OpenID is not already used
for _, obj := range oids {
if obj.URI == id {
loadSecurityData(ctx)
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form)
return
}
}
redirectTo := setting.AppURL + "user/settings/security"
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
if err != nil {
loadSecurityData(ctx)
ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &form)
return
}
ctx.Redirect(url)
}
func settingsOpenIDVerify(ctx *context.Context) {
log.Trace("Incoming call to: " + ctx.Req.URL.String())
fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
log.Trace("Full URL: " + fullURL)
id, err := openid.Verify(fullURL)
if err != nil {
ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{
Openid: id,
})
return
}
log.Trace("Verified ID: " + id)
oid := &user_model.UserOpenID{UID: ctx.Doer.ID, URI: id}
if err = user_model.AddUserOpenID(ctx, oid); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id})
return
}
ctx.ServerError("AddUserOpenID", err)
return
}
log.Trace("Associated OpenID %s to user %s", id, ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
// DeleteOpenID response for delete user's openid
func DeleteOpenID(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("DeleteUserOpenID", err)
}
return
}
log.Trace("OpenID address deleted: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success"))
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}
// ToggleOpenIDVisibility response for toggle visibility of user's openid
func ToggleOpenIDVisibility(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id"), ctx.Doer); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("ToggleUserOpenIDVisibility", err)
}
return
}
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
@@ -0,0 +1,36 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"net/http"
"testing"
"gitea.dev/models/unittest"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestDeleteOpenIDReturnsNotFoundForOtherUsersAddress(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
contexttest.LoadUser(t, ctx, 2)
ctx.SetFormString("id", "1")
DeleteOpenID(ctx)
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
}
func TestToggleOpenIDVisibilityReturnsNotFoundForOtherUsersAddress(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
contexttest.LoadUser(t, ctx, 2)
ctx.SetFormString("id", "1")
ToggleOpenIDVisibility(ctx)
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
}
@@ -0,0 +1,159 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"net/http"
"sort"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/services/auth/source/oauth2"
"gitea.dev/services/context"
)
const (
tplSettingsSecurity templates.TplName = "user/settings/security/security"
tplSettingsTwofaEnroll templates.TplName = "user/settings/security/twofa_enroll"
)
// Security render change user's password page and 2FA
func Security(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer,
setting.UserFeatureManageMFA, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
ctx.Data["Title"] = ctx.Tr("settings.security")
ctx.Data["PageIsSettingsSecurity"] = true
if ctx.FormString("openid.return_to") != "" {
settingsOpenIDVerify(ctx)
return
}
loadSecurityData(ctx)
ctx.HTML(http.StatusOK, tplSettingsSecurity)
}
// DeleteAccountLink delete a single account link
func DeleteAccountLink(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
id := ctx.FormInt64("id")
if id <= 0 {
ctx.Flash.Error("Account link id is not given")
} else {
if _, err := user_model.RemoveAccountLink(ctx, ctx.Doer, id); err != nil {
ctx.Flash.Error("RemoveAccountLink: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
}
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}
func loadSecurityData(ctx *context.Context) {
enrolled, err := auth_model.HasTwoFactorByUID(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
ctx.Data["TOTPEnrolled"] = enrolled
credentials, err := auth_model.GetWebAuthnCredentialsByUID(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return
}
ctx.Data["WebAuthnCredentials"] = credentials
tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID})
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
}
ctx.Data["Tokens"] = tokens
accountLinks, err := db.Find[user_model.ExternalLoginUser](ctx, user_model.FindExternalUserOptions{
UserID: ctx.Doer.ID,
OrderBy: "login_source_id DESC",
})
if err != nil {
ctx.ServerError("ListAccountLinks", err)
return
}
// map the provider display name with the AuthSource
sources := make(map[*auth_model.Source]string)
for _, externalAccount := range accountLinks {
if authSource, err := auth_model.GetSourceByID(ctx, externalAccount.LoginSourceID); err == nil {
var providerDisplayName string
type DisplayNamed interface {
DisplayName() string
}
type Named interface {
Name() string
}
if displayNamed, ok := authSource.Cfg.(DisplayNamed); ok {
providerDisplayName = displayNamed.DisplayName()
} else if named, ok := authSource.Cfg.(Named); ok {
providerDisplayName = named.Name()
} else {
providerDisplayName = authSource.Name
}
sources[authSource] = providerDisplayName
}
}
ctx.Data["AccountLinks"] = sources
authSources, err := db.Find[auth_model.Source](ctx, auth_model.FindSourcesOptions{
IsActive: optional.None[bool](),
LoginType: auth_model.OAuth2,
})
if err != nil {
ctx.ServerError("FindSources", err)
return
}
var orderedOAuth2Names []string
oauth2Providers := make(map[string]oauth2.Provider)
for _, source := range authSources {
provider, err := oauth2.CreateProviderFromSource(source)
if err != nil {
ctx.ServerError("CreateProviderFromSource", err)
return
}
oauth2Providers[source.Name] = provider
if source.IsActive {
orderedOAuth2Names = append(orderedOAuth2Names, source.Name)
}
}
sort.Strings(orderedOAuth2Names)
ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
ctx.Data["OAuth2Providers"] = oauth2Providers
openid, err := user_model.GetUserOpenIDs(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = openid
}
@@ -0,0 +1,141 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"errors"
"net/http"
"strconv"
"time"
"gitea.dev/models/auth"
user_model "gitea.dev/models/user"
wa "gitea.dev/modules/auth/webauthn"
"gitea.dev/modules/log"
"gitea.dev/modules/session"
"gitea.dev/modules/setting"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/forms"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
)
// WebAuthnRegister initializes the webauthn registration procedure
func WebAuthnRegister(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.WebauthnRegistrationForm)
if form.Name == "" {
// Set name to the hexadecimal of the current time
form.Name = strconv.FormatInt(time.Now().UnixNano(), 16)
}
cred, err := auth.GetWebAuthnCredentialByName(ctx, ctx.Doer.ID, form.Name)
if err != nil && !auth.IsErrWebAuthnCredentialNotExist(err) {
ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return
}
if cred != nil {
ctx.HTTPError(http.StatusConflict, "Name already taken")
return
}
_ = ctx.Session.Delete("webauthnRegistration")
if err := ctx.Session.Set("webauthnName", form.Name); err != nil {
ctx.ServerError("Unable to set session key for webauthnName", err)
return
}
webAuthnUser := wa.NewWebAuthnUser(ctx, ctx.Doer)
credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration(webAuthnUser, webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
ResidentKey: protocol.ResidentKeyRequirementRequired,
}))
if err != nil {
ctx.ServerError("Unable to BeginRegistration", err)
return
}
// Save the session data as marshaled JSON
if err = ctx.Session.Set("webauthnRegistration", sessionData); err != nil {
ctx.ServerError("Unable to set session", err)
return
}
ctx.JSON(http.StatusOK, credentialOptions)
}
// WebauthnRegisterPost receives the response of the security key
func WebauthnRegisterPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
name, ok := ctx.Session.Get("webauthnName").(string)
if !ok || name == "" {
ctx.ServerError("Get webauthnName", errors.New("no webauthnName"))
return
}
// Load the session data
sessionData, ok := ctx.Session.Get("webauthnRegistration").(*webauthn.SessionData)
if !ok || sessionData == nil {
ctx.ServerError("Get registration", errors.New("no registration"))
return
}
defer func() {
_ = ctx.Session.Delete("webauthnRegistration")
}()
// Verify that the challenge succeeded
webAuthnUser := wa.NewWebAuthnUser(ctx, ctx.Doer)
cred, err := wa.WebAuthn.FinishRegistration(webAuthnUser, *sessionData, ctx.Req)
if err != nil {
if pErr, ok := err.(*protocol.Error); ok {
log.Error("Unable to finish registration due to error: %v\nDevInfo: %s", pErr, pErr.DevInfo)
}
ctx.ServerError("CreateCredential", err)
return
}
dbCred, err := auth.GetWebAuthnCredentialByName(ctx, ctx.Doer.ID, name)
if err != nil && !auth.IsErrWebAuthnCredentialNotExist(err) {
ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return
}
if dbCred != nil {
ctx.HTTPError(http.StatusConflict, "Name already taken")
return
}
// Create the credential
_, err = auth.CreateCredential(ctx, ctx.Doer.ID, name, cred)
if err != nil {
ctx.ServerError("CreateCredential", err)
return
}
_ = ctx.Session.Delete("webauthnName")
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
ctx.JSON(http.StatusCreated, cred)
}
// WebauthnDelete deletes an security key by id
func WebauthnDelete(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.WebauthnDeleteForm)
if _, err := auth.DeleteCredential(ctx, form.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("GetWebAuthnCredentialByID", err)
return
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"strconv"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
"gitea.dev/modules/setting"
"gitea.dev/services/context"
)
func SettingsCtxData(ctx *context.Context) {
ctx.Data["PageIsUserSettings"] = true
ctx.Data["EnablePackages"] = setting.Packages.Enabled
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
}
func UpdatePreferences(ctx *context.Context) {
type preferencesForm struct {
CodeViewShowFileTree bool `json:"codeViewShowFileTree"`
}
form := &preferencesForm{}
if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
ctx.HTTPError(http.StatusBadRequest, "json decode failed")
return
}
_ = user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyCodeViewShowFileTree, strconv.FormatBool(form.CodeViewShowFileTree))
ctx.JSONOK()
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"gitea.dev/models/db"
"gitea.dev/models/webhook"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/services/context"
)
const (
tplSettingsHooks templates.TplName = "user/settings/hooks"
)
// Webhooks render webhook list page
func Webhooks(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsHooks"] = true
ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/hooks"
ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks"
ctx.Data["Description"] = ctx.Tr("settings.hooks.desc")
ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID})
if err != nil {
ctx.ServerError("ListWebhooksByOpts", err)
return
}
ctx.Data["Webhooks"] = ws
ctx.HTML(http.StatusOK, tplSettingsHooks)
}
// DeleteWebhook response for delete webhook
func DeleteWebhook(ctx *context.Context) {
if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Doer.ID, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/hooks")
}