first-commit
This commit is contained in:
336
routers/web/user/setting/account.go
Normal file
336
routers/web/user/setting/account.go
Normal file
@@ -0,0 +1,336 @@
|
||||
// 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 "code.gitea.io/gitea/models/organization"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/db"
|
||||
"code.gitea.io/gitea/services/auth/source/smtp"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
"code.gitea.io/gitea/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)
|
||||
|
||||
// 界面原型:更新微信,展示公众号带参数二维码
|
||||
wechatQrTicket, wechatQrCodeUrl, err := auth_service.GetWechatQRTicket(ctx)
|
||||
if err != nil {
|
||||
log.Warn("微信创建二维码失败,跳过")
|
||||
}
|
||||
|
||||
ctx.Data["wechatQrTicket"] = wechatQrTicket
|
||||
ctx.Data["wechatQrCodeUrl"] = wechatQrCodeUrl
|
||||
ctx.Data["wechatQrExpireSeconds"] = setting.Wechat.TempQrExpireSeconds
|
||||
|
||||
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")
|
||||
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")
|
||||
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.FormInt64("id")); err != nil {
|
||||
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.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
|
||||
} else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
|
||||
loadAccountData(ctx)
|
||||
|
||||
ctx.RenderWithErr(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")
|
||||
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.RenderWithErr(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil)
|
||||
case errors.Is(err, smtp.ErrUnsupportedLoginType):
|
||||
loadAccountData(ctx)
|
||||
|
||||
ctx.RenderWithErr(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil)
|
||||
case errors.As(err, &db.ErrUserPasswordNotSet{}):
|
||||
loadAccountData(ctx)
|
||||
|
||||
ctx.RenderWithErr(ctx.Tr("form.unset_password"), tplSettingsAccount, nil)
|
||||
case errors.As(err, &db.ErrUserPasswordInvalid{}):
|
||||
loadAccountData(ctx)
|
||||
|
||||
ctx.RenderWithErr(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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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
routers/web/user/setting/account_test.go
Normal file
101
routers/web/user/setting/account_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
"code.gitea.io/gitea/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())
|
||||
})
|
||||
}
|
||||
}
|
64
routers/web/user/setting/adopt.go
Normal file
64
routers/web/user/setting/adopt.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/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
|
||||
root := user_model.UserPath(ctxUser.LowerName)
|
||||
|
||||
// check not a repo
|
||||
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, dir)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsRepositoryExist", err)
|
||||
return
|
||||
}
|
||||
|
||||
isDir, err := util.IsDir(filepath.Join(root, dir+".git"))
|
||||
if err != nil {
|
||||
ctx.ServerError("IsDir", err)
|
||||
return
|
||||
}
|
||||
if has || !isDir {
|
||||
// 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")
|
||||
}
|
138
routers/web/user/setting/applications.go
Normal file
138
routers/web/user/setting/applications.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// 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 "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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")
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
_ = 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
|
||||
}
|
||||
}
|
||||
}
|
338
routers/web/user/setting/appstore.go
Normal file
338
routers/web/user/setting/appstore.go
Normal file
@@ -0,0 +1,338 @@
|
||||
// Copyright 2024 The Devstar Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
appstore_model "code.gitea.io/gitea/models/appstore"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/appstore"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsAppStore templates.TplName = "user/settings/appstore"
|
||||
)
|
||||
|
||||
// AppStore displays the app store page
|
||||
func AppStore(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.appstore")
|
||||
ctx.Data["PageIsSettingsAppStore"] = true
|
||||
ctx.HTML(http.StatusOK, tplSettingsAppStore)
|
||||
}
|
||||
|
||||
// AppStoreAPI handles API requests for app store data
|
||||
func AppStoreAPI(ctx *context.Context) {
|
||||
action := ctx.PathParam("action")
|
||||
|
||||
switch action {
|
||||
case "apps":
|
||||
handleGetApps(ctx)
|
||||
case "categories":
|
||||
handleGetCategories(ctx)
|
||||
case "tags":
|
||||
handleGetTags(ctx)
|
||||
default:
|
||||
ctx.JSON(http.StatusBadRequest, map[string]interface{}{
|
||||
"error": "Invalid action",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetApps returns the list of apps from database
|
||||
func handleGetApps(ctx *context.Context) {
|
||||
// Get query parameters
|
||||
category := ctx.FormString("category")
|
||||
tag := ctx.FormString("tag")
|
||||
search := ctx.FormString("search")
|
||||
deployment := ctx.FormString("deployment") // docker, kubernetes, all
|
||||
source := ctx.FormString("source") // local | devstar
|
||||
|
||||
manager := appstore.NewManager(ctx)
|
||||
|
||||
var apps []appstore.App
|
||||
var err error
|
||||
|
||||
if source == "devstar" {
|
||||
apps, err = manager.ListAppsFromDevstar()
|
||||
if err == nil {
|
||||
// 与本地一致的过滤逻辑(先拿全量,再在服务端筛选)
|
||||
if category != "" || tag != "" || search != "" || deployment != "" {
|
||||
var filtered []appstore.App
|
||||
for _, a := range apps {
|
||||
if category != "" && a.Category != category {
|
||||
continue
|
||||
}
|
||||
if tag != "" {
|
||||
matched := false
|
||||
for _, t := range a.Tags {
|
||||
if t == tag {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if deployment != "" && deployment != "all" {
|
||||
// 处理部署类型过滤,包括 'both' 类型
|
||||
appDeployment := a.DeploymentType
|
||||
if appDeployment == "" {
|
||||
// 如果没有 deployment_type 字段,尝试从其他字段获取
|
||||
if a.Deploy.Type != "" {
|
||||
appDeployment = a.Deploy.Type
|
||||
} else {
|
||||
appDeployment = "docker" // 默认值
|
||||
}
|
||||
}
|
||||
|
||||
if appDeployment != "both" && appDeployment != deployment {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if search != "" {
|
||||
low := strings.ToLower(search)
|
||||
nameOk := strings.Contains(strings.ToLower(a.Name), low)
|
||||
descOk := strings.Contains(strings.ToLower(a.Description), low)
|
||||
authorOk := strings.Contains(strings.ToLower(a.Author), low)
|
||||
if !(nameOk || descOk || authorOk) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, a)
|
||||
}
|
||||
apps = filtered
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if search != "" {
|
||||
// Convert tag parameter to tags slice
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = strings.Split(tag, ",")
|
||||
}
|
||||
apps, err = manager.SearchApps(search, category, tags)
|
||||
} else {
|
||||
apps, err = manager.ListApps()
|
||||
|
||||
// Filter by category, tag and deployment if specified
|
||||
if category != "" || tag != "" || deployment != "" {
|
||||
var filteredApps []appstore.App
|
||||
for _, app := range apps {
|
||||
matchCategory := category == "" || app.Category == category
|
||||
matchTag := tag == ""
|
||||
if tag != "" {
|
||||
for _, appTag := range app.Tags {
|
||||
if appTag == tag {
|
||||
matchTag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理部署类型过滤,包括 'both' 类型
|
||||
matchDeployment := deployment == "" || deployment == "all"
|
||||
if deployment != "" && deployment != "all" {
|
||||
appDeployment := app.DeploymentType
|
||||
if appDeployment == "" {
|
||||
// 如果没有 deployment_type 字段,尝试从其他字段获取
|
||||
if app.Deploy.Type != "" {
|
||||
appDeployment = app.Deploy.Type
|
||||
} else {
|
||||
appDeployment = "docker" // 默认值
|
||||
}
|
||||
}
|
||||
|
||||
matchDeployment = appDeployment == "both" || appDeployment == deployment
|
||||
}
|
||||
|
||||
if matchCategory && matchTag && matchDeployment {
|
||||
filteredApps = append(filteredApps, app)
|
||||
}
|
||||
}
|
||||
apps = filteredApps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"apps": apps,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetCategories returns the list of categories
|
||||
func handleGetCategories(ctx *context.Context) {
|
||||
categories, err := appstore_model.GetCategories(ctx)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"categories": categories,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetTags returns the list of tags
|
||||
func handleGetTags(ctx *context.Context) {
|
||||
tags, err := appstore_model.GetTags(ctx)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"tags": tags,
|
||||
})
|
||||
}
|
||||
|
||||
// AppStoreInstall handles app installation
|
||||
func AppStoreInstall(ctx *context.Context) {
|
||||
if ctx.Req.Method != "POST" {
|
||||
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析表单数据
|
||||
appID := ctx.FormString("app_id")
|
||||
configJSON := ctx.FormString("config")
|
||||
installTarget := ctx.FormString("install_target")
|
||||
kubeconfig := ctx.FormString("kubeconfig")
|
||||
kubeconfigContext := ctx.FormString("kubeconfig_context")
|
||||
|
||||
if appID == "" {
|
||||
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建 manager 并执行安装
|
||||
manager := appstore.NewManager(ctx)
|
||||
if err := manager.InstallApp(appID, configJSON, installTarget, kubeconfig, kubeconfigContext); err != nil {
|
||||
// 根据错误类型返回相应的状态码和消息
|
||||
if appErr, ok := err.(*appstore.AppStoreError); ok {
|
||||
switch appErr.Code {
|
||||
case "APP_NOT_FOUND", "CONFIG_MERGE_ERROR":
|
||||
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
|
||||
case "KUBERNETES_INSTALL_ERROR":
|
||||
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
|
||||
case "NOT_IMPLEMENTED":
|
||||
ctx.JSON(501, map[string]string{"error": appErr.Message})
|
||||
default:
|
||||
ctx.JSON(500, map[string]string{"error": appErr.Message})
|
||||
}
|
||||
} else {
|
||||
ctx.JSON(500, map[string]string{"error": "安装失败: " + err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 安装成功
|
||||
if installTarget == "kubeconfig" && kubeconfig != "" {
|
||||
ctx.Flash.Success("应用已成功安装到外部 Kubernetes 集群")
|
||||
} else {
|
||||
ctx.Flash.Success("应用配置已准备完成,本地安装功能开发中")
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
|
||||
}
|
||||
|
||||
// AppStoreUninstall handles app uninstallation
|
||||
func AppStoreUninstall(ctx *context.Context) {
|
||||
if ctx.Req.Method != "POST" {
|
||||
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析表单数据
|
||||
appID := ctx.FormString("app_id")
|
||||
installTarget := ctx.FormString("install_target")
|
||||
kubeconfig := ctx.FormString("kubeconfig")
|
||||
kubeconfigContext := ctx.FormString("kubeconfig_context")
|
||||
|
||||
if appID == "" {
|
||||
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建 manager 并执行卸载
|
||||
manager := appstore.NewManager(ctx)
|
||||
if err := manager.UninstallApp(appID, installTarget, kubeconfig, kubeconfigContext); err != nil {
|
||||
// 根据错误类型返回相应的状态码和消息
|
||||
if appErr, ok := err.(*appstore.AppStoreError); ok {
|
||||
switch appErr.Code {
|
||||
case "APP_NOT_FOUND":
|
||||
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
|
||||
case "KUBERNETES_UNINSTALL_ERROR":
|
||||
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
|
||||
case "NOT_IMPLEMENTED":
|
||||
ctx.JSON(501, map[string]string{"error": appErr.Message})
|
||||
default:
|
||||
ctx.JSON(500, map[string]string{"error": appErr.Message})
|
||||
}
|
||||
} else {
|
||||
ctx.JSON(500, map[string]string{"error": "卸载失败: " + err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 卸载成功
|
||||
if installTarget == "kubeconfig" && kubeconfig != "" {
|
||||
ctx.Flash.Success("应用已成功从外部 Kubernetes 集群卸载")
|
||||
} else {
|
||||
ctx.Flash.Success("本地卸载功能开发中")
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
|
||||
}
|
||||
|
||||
// AppStoreConfigure handles app configuration
|
||||
func AppStoreConfigure(ctx *context.Context) {
|
||||
// TODO: Load app configuration form
|
||||
ctx.Data["Title"] = "App Configuration"
|
||||
ctx.Data["PageIsSettingsAppStore"] = true
|
||||
ctx.HTML(http.StatusOK, tplSettingsAppStore)
|
||||
}
|
||||
|
||||
// AppStoreConfigurePost handles app configuration form submission
|
||||
func AppStoreConfigurePost(ctx *context.Context) {
|
||||
// TODO: Handle configuration form submission
|
||||
ctx.Flash.Success("App configuration feature coming soon")
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
|
||||
}
|
||||
|
||||
// 添加应用API
|
||||
func AddAppAPI(ctx *context.Context) {
|
||||
manager := appstore.NewManager(ctx)
|
||||
if ctx.Req.Method != "POST" {
|
||||
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
|
||||
return
|
||||
}
|
||||
defer ctx.Req.Body.Close()
|
||||
jsonBytes, err := io.ReadAll(ctx.Req.Body)
|
||||
if err != nil {
|
||||
ctx.JSON(400, map[string]string{"error": "读取请求体失败"})
|
||||
return
|
||||
}
|
||||
if err := manager.AddAppFromJSON(jsonBytes); err != nil {
|
||||
ctx.JSON(400, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
ctx.JSON(200, map[string]string{"msg": "ok"})
|
||||
}
|
40
routers/web/user/setting/block.go
Normal file
40
routers/web/user/setting/block.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
|
||||
}
|
345
routers/web/user/setting/keys.go
Normal file
345
routers/web/user/setting/keys.go
Normal file
@@ -0,0 +1,345 @@
|
||||
// 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 "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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")
|
||||
ctx.Data["PageIsSettingsKeys"] = true
|
||||
ctx.Data["DisableSSH"] = setting.SSH.Disabled
|
||||
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
|
||||
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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.RenderWithErr(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.RenderWithErr(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.RenderWithErr(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.RenderWithErr(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.RenderWithErr(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); err != nil {
|
||||
ctx.Data["HasSSHError"] = true
|
||||
switch {
|
||||
case asymkey_model.IsErrKeyAlreadyExist(err):
|
||||
loadKeysData(ctx)
|
||||
|
||||
ctx.Data["Err_Content"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
|
||||
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
|
||||
loadKeysData(ctx)
|
||||
|
||||
ctx.Data["Err_Title"] = true
|
||||
ctx.RenderWithErr(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.RenderWithErr(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 csrfToken
|
||||
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")
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
}
|
14
routers/web/user/setting/main_test.go
Normal file
14
routers/web/user/setting/main_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
62
routers/web/user/setting/notifications.go
Normal file
62
routers/web/user/setting/notifications.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/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
|
||||
|
||||
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) {
|
||||
log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.Doer.Name)
|
||||
ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
|
||||
return
|
||||
}
|
||||
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("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
log.Trace("Email notifications preference made %s: %s", preference, ctx.Doer.Name)
|
||||
ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/notifications")
|
||||
}
|
68
routers/web/user/setting/oauth2.go
Normal file
68
routers/web/user/setting/oauth2.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/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")
|
||||
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")
|
||||
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")
|
||||
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
routers/web/user/setting/oauth2_common.go
Normal file
178
routers/web/user/setting/oauth2_common.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/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)
|
||||
}
|
125
routers/web/user/setting/packages.go
Normal file
125
routers/web/user/setting/packages.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
chef_module "code.gitea.io/gitea/modules/packages/chef"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared "code.gitea.io/gitea/routers/web/shared/packages"
|
||||
"code.gitea.io/gitea/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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
shared.SetRuleAddContext(ctx)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
|
||||
}
|
||||
|
||||
func PackagesRuleEdit(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
shared.SetRuleEditContext(ctx, ctx.Doer)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
|
||||
}
|
||||
|
||||
func PackagesRuleAddPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
431
routers/web/user/setting/profile.go
Normal file
431
routers/web/user/setting/profile.go
Normal file
@@ -0,0 +1,431 @@
|
||||
// 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"
|
||||
|
||||
"code.gitea.io/gitea/models/avatars"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"code.gitea.io/gitea/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.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsProfile)
|
||||
}
|
||||
|
||||
// ProfilePost response for change user's profile
|
||||
func ProfilePost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["PageIsSettingsProfile"] = true
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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); 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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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(int(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["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
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 := (opts.Page - 1) * opts.PageSize
|
||||
end := start + opts.PageSize
|
||||
|
||||
adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories)
|
||||
|
||||
ctxUser := ctx.Doer
|
||||
count := 0
|
||||
|
||||
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, count64, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepositories", err)
|
||||
return
|
||||
}
|
||||
count = int(count64)
|
||||
|
||||
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()
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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")
|
||||
ctx.Data["PageIsSettingsAppearance"] = true
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.Flash.Error(ctx.GetErrMsg())
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
|
||||
return
|
||||
}
|
||||
|
||||
if !webtheme.IsThemeAvailable(form.Theme) {
|
||||
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")
|
||||
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")
|
||||
}
|
13
routers/web/user/setting/runner.go
Normal file
13
routers/web/user/setting/runner.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func RedirectToDefaultSetting(ctx *context.Context) {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/actions/runners")
|
||||
}
|
280
routers/web/user/setting/security/2fa.go
Normal file
280
routers/web/user/setting/security/2fa.go
Normal 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"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
}
|
141
routers/web/user/setting/security/openid.go
Normal file
141
routers/web/user/setting/security/openid.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/openid"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/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")
|
||||
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.RenderWithErr(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.RenderWithErr(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.RenderWithErr(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.RenderWithErr(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.RenderWithErr(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 {
|
||||
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")); err != nil {
|
||||
ctx.ServerError("ToggleUserOpenIDVisibility", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
|
||||
}
|
160
routers/web/user/setting/security/security.go
Normal file
160
routers/web/user/setting/security/security.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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 "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/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
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
}
|
141
routers/web/user/setting/security/webauthn.go
Normal file
141
routers/web/user/setting/security/webauthn.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
wa "code.gitea.io/gitea/modules/auth/webauthn"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/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")
|
||||
}
|
26
routers/web/user/setting/settings.go
Normal file
26
routers/web/user/setting/settings.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
49
routers/web/user/setting/webhooks.go
Normal file
49
routers/web/user/setting/webhooks.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/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")
|
||||
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")
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
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")
|
||||
}
|
48
routers/web/user/setting/wechat.go
Normal file
48
routers/web/user/setting/wechat.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
wechat_model "code.gitea.io/gitea/models/wechat"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
context "code.gitea.io/gitea/services/context"
|
||||
wechat_service "code.gitea.io/gitea/services/wechat"
|
||||
)
|
||||
|
||||
// BindWechatQR 将二维码扫描人 OpenID 关联到数据库用户中
|
||||
func BindWechatQR(ctx *context.Context) {
|
||||
|
||||
// 1. 取出 微信公众号二维码 ticket
|
||||
ticket := ctx.Base.Req.URL.Query().Get("ticket")
|
||||
|
||||
// 2. 取出用户openid并清空对应的ticket
|
||||
qrStatus, err := wechat_service.GetWechatQrStatusByTicket(ticket)
|
||||
if err != nil {
|
||||
log.Error("绑定微信失败: " + err.Error())
|
||||
ctx.Flash.Error("绑定微信失败:" + err.Error())
|
||||
ctx.Redirect("/user/settings/account")
|
||||
return
|
||||
}
|
||||
defer wechat_service.DeleteWechatQrByTicket(ticket)
|
||||
|
||||
// 3. 从 Gitea Web Context 中获取用户信息
|
||||
user := ctx.Doer
|
||||
|
||||
// 4. 更新数据库 `user_wechat_openid`
|
||||
err = wechat_model.UpdateOrCreateWechatUser(ctx, user, qrStatus.OpenId)
|
||||
if err != nil {
|
||||
log.Error("绑定微信失败: " + err.Error())
|
||||
ctx.Flash.Error("绑定微信失败: " + err.Error())
|
||||
ctx.Redirect("/user/settings/account")
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 携带扫码成功信息,重定向回用户修改信息页面
|
||||
ctx.Data["wechatQRScanSuccess"] = true
|
||||
Account(ctx)
|
||||
}
|
||||
|
||||
// UnbindWechatQR 解绑微信
|
||||
func UnbindWechatQR(ctx *context.Context) {
|
||||
_ = wechat_model.DeleteWechatUser(ctx, ctx.Doer)
|
||||
ctx.Flash.Success(ctx.Tr("settings.wechat_unbind_success"))
|
||||
ctx.Redirect("/user/settings/account")
|
||||
}
|
Reference in New Issue
Block a user