diff --git a/cmd/admin.go b/cmd/admin.go
index 5492b9a2db..6c79141eab 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -25,6 +25,7 @@ var (
subcmdCreateUser,
subcmdChangePassword,
subcmdRepoSyncReleases,
+ subcmdRegenerate,
},
}
@@ -80,6 +81,41 @@ var (
Usage: "Synchronize repository releases with tags",
Action: runRepoSyncReleases,
}
+
+ subcmdRegenerate = cli.Command{
+ Name: "regenerate",
+ Usage: "Regenerate specific files",
+ Subcommands: []cli.Command{
+ microcmdRegenHooks,
+ microcmdRegenKeys,
+ },
+ }
+
+ microcmdRegenHooks = cli.Command{
+ Name: "hooks",
+ Usage: "Regenerate git-hooks",
+ Action: runRegenerateHooks,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "config, c",
+ Value: "custom/conf/app.ini",
+ Usage: "Custom configuration file path",
+ },
+ },
+ }
+
+ microcmdRegenKeys = cli.Command{
+ Name: "keys",
+ Usage: "Regenerate authorized_keys file",
+ Action: runRegenerateKeys,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "config, c",
+ Value: "custom/conf/app.ini",
+ Usage: "Custom configuration file path",
+ },
+ },
+ }
)
func runChangePassword(c *cli.Context) error {
@@ -195,3 +231,25 @@ func getReleaseCount(id int64) (int64, error) {
},
)
}
+
+func runRegenerateHooks(c *cli.Context) error {
+ if c.IsSet("config") {
+ setting.CustomConf = c.String("config")
+ }
+
+ if err := initDB(); err != nil {
+ return err
+ }
+ return models.SyncRepositoryHooks()
+}
+
+func runRegenerateKeys(c *cli.Context) error {
+ if c.IsSet("config") {
+ setting.CustomConf = c.String("config")
+ }
+
+ if err := initDB(); err != nil {
+ return err
+ }
+ return models.RewriteAllPublicKeys()
+}
diff --git a/docker/etc/s6/gitea/run b/docker/etc/s6/gitea/run
index 1fddb93708..da5fd6b535 100755
--- a/docker/etc/s6/gitea/run
+++ b/docker/etc/s6/gitea/run
@@ -2,5 +2,5 @@
[[ -f ./setup ]] && source ./setup
pushd /app/gitea > /dev/null
- exec su-exec git /app/gitea/gitea web
+ exec su-exec $USER /app/gitea/gitea web
popd
diff --git a/docker/etc/s6/gitea/setup b/docker/etc/s6/gitea/setup
index 8e6441c5c2..6ca9b82123 100755
--- a/docker/etc/s6/gitea/setup
+++ b/docker/etc/s6/gitea/setup
@@ -39,5 +39,5 @@ if [ ! -f /data/gitea/conf/app.ini ]; then
envsubst < /etc/templates/app.ini > /data/gitea/conf/app.ini
fi
-chown -R git:git /data/gitea /app/gitea /data/git
+chown -R ${USER}:git /data/gitea /app/gitea /data/git
chmod 0755 /data/gitea /app/gitea /data/git
diff --git a/docker/usr/bin/entrypoint b/docker/usr/bin/entrypoint
index b374c5aed7..50623bfa66 100755
--- a/docker/usr/bin/entrypoint
+++ b/docker/usr/bin/entrypoint
@@ -1,5 +1,12 @@
#!/bin/sh
+if [ "${USER}" != "git" ]; then
+ # rename user
+ sed -i -e "s/^git\:/${USER}\:/g" /etc/passwd
+ # switch sshd config to different user
+ sed -i -e "s/AllowUsers git/AllowUsers ${USER}/g" /etc/ssh/sshd_config
+fi
+
## Change GID for USER?
if [ -n "${USER_GID}" ] && [ "${USER_GID}" != "`id -g ${USER}`" ]; then
sed -i -e "s/^${USER}:\([^:]*\):[0-9]*/${USER}:\1:${USER_GID}/" /etc/group
diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md
index c2b7b037c3..db0d3b5553 100644
--- a/docs/content/doc/features/comparison.en-us.md
+++ b/docs/content/doc/features/comparison.en-us.md
@@ -537,7 +537,7 @@ _Symbols used in table:_
Webhook support |
- ⁄ |
+ ✓ |
✓ |
✓ |
✓ |
diff --git a/docs/content/doc/usage/command-line.md b/docs/content/doc/usage/command-line.md
index cf6feeaf5e..9c16d49049 100644
--- a/docs/content/doc/usage/command-line.md
+++ b/docs/content/doc/usage/command-line.md
@@ -64,6 +64,13 @@ Admin operations:
- `--password value`, `-p value`: New password. Required.
- Examples:
- `gitea admin change-password --username myname --password asecurepassword`
+ - `regenerate`
+ - Options:
+ - `hooks`: Regenerate git-hooks for all repositories
+ - `keys`: Regenerate authorized_keys file
+ - Examples:
+ - `gitea admin regenerate hooks`
+ - `gitea admin regenerate keys`
#### cert
diff --git a/models/action.go b/models/action.go
index 4f357cb2c5..c3ed9c7c02 100644
--- a/models/action.go
+++ b/models/action.go
@@ -618,6 +618,16 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
case ActionDeleteBranch: // Delete Branch
isHookEventPush = true
+ if err = PrepareWebhooks(repo, HookEventDelete, &api.DeletePayload{
+ Ref: refName,
+ RefType: "branch",
+ PusherType: api.PusherTypeUser,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err)
+ }
+
case ActionPushTag: // Create
isHookEventPush = true
@@ -640,6 +650,16 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
}
case ActionDeleteTag: // Delete Tag
isHookEventPush = true
+
+ if err = PrepareWebhooks(repo, HookEventDelete, &api.DeletePayload{
+ Ref: refName,
+ RefType: "tag",
+ PusherType: api.PusherTypeUser,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err)
+ }
}
if isHookEventPush {
diff --git a/models/issue_comment.go b/models/issue_comment.go
index 6514223711..bb48581b2b 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -89,9 +89,10 @@ const (
type Comment struct {
ID int64 `xorm:"pk autoincr"`
Type CommentType
- PosterID int64 `xorm:"INDEX"`
- Poster *User `xorm:"-"`
- IssueID int64 `xorm:"INDEX"`
+ PosterID int64 `xorm:"INDEX"`
+ Poster *User `xorm:"-"`
+ IssueID int64 `xorm:"INDEX"`
+ Issue *Issue `xorm:"-"`
LabelID int64
Label *Label `xorm:"-"`
OldMilestoneID int64
@@ -127,6 +128,15 @@ type Comment struct {
Invalidated bool
}
+// LoadIssue loads issue from database
+func (c *Comment) LoadIssue() (err error) {
+ if c.Issue != nil {
+ return nil
+ }
+ c.Issue, err = GetIssueByID(c.IssueID)
+ return
+}
+
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func (c *Comment) AfterLoad(session *xorm.Session) {
var err error
@@ -157,40 +167,40 @@ func (c *Comment) AfterDelete() {
// HTMLURL formats a URL-string to the issue-comment
func (c *Comment) HTMLURL() string {
- issue, err := GetIssueByID(c.IssueID)
+ err := c.LoadIssue()
if err != nil { // Silently dropping errors :unamused:
- log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err)
+ log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
return ""
}
- return fmt.Sprintf("%s#%s", issue.HTMLURL(), c.HashTag())
+ return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
}
// IssueURL formats a URL-string to the issue
func (c *Comment) IssueURL() string {
- issue, err := GetIssueByID(c.IssueID)
+ err := c.LoadIssue()
if err != nil { // Silently dropping errors :unamused:
- log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err)
+ log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
return ""
}
- if issue.IsPull {
+ if c.Issue.IsPull {
return ""
}
- return issue.HTMLURL()
+ return c.Issue.HTMLURL()
}
// PRURL formats a URL-string to the pull-request
func (c *Comment) PRURL() string {
- issue, err := GetIssueByID(c.IssueID)
+ err := c.LoadIssue()
if err != nil { // Silently dropping errors :unamused:
- log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err)
+ log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
return ""
}
- if !issue.IsPull {
+ if !c.Issue.IsPull {
return ""
}
- return issue.HTMLURL()
+ return c.Issue.HTMLURL()
}
// APIFormat converts a Comment to the api.Comment format
@@ -207,9 +217,14 @@ func (c *Comment) APIFormat() *api.Comment {
}
}
+// CommentHashTag returns unique hash tag for comment id.
+func CommentHashTag(id int64) string {
+ return fmt.Sprintf("issuecomment-%d", id)
+}
+
// HashTag returns unique hash tag for comment.
func (c *Comment) HashTag() string {
- return "issuecomment-" + com.ToStr(c.ID)
+ return CommentHashTag(c.ID)
}
// EventTag returns unique event hash tag for comment.
@@ -638,7 +653,7 @@ func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
// CreateIssueComment creates a plain issue comment.
func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) {
- return CreateComment(&CreateCommentOptions{
+ comment, err := CreateComment(&CreateCommentOptions{
Type: CommentTypeComment,
Doer: doer,
Repo: repo,
@@ -646,6 +661,21 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri
Content: content,
Attachments: attachments,
})
+ if err != nil {
+ return nil, fmt.Errorf("CreateComment: %v", err)
+ }
+
+ mode, _ := AccessLevel(doer.ID, repo)
+ if err = PrepareWebhooks(repo, HookEventIssueComment, &api.IssueCommentPayload{
+ Action: api.HookIssueCommentCreated,
+ Issue: issue.APIFormat(),
+ Comment: comment.APIFormat(),
+ Repository: repo.APIFormat(mode),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
+ }
+ return comment, nil
}
// CreateCodeComment creates a plain code comment at the specified line / path
@@ -798,17 +828,41 @@ func GetCommentsByRepoIDSince(repoID, since int64) ([]*Comment, error) {
}
// UpdateComment updates information of comment.
-func UpdateComment(c *Comment) error {
+func UpdateComment(doer *User, c *Comment, oldContent string) error {
if _, err := x.ID(c.ID).AllCols().Update(c); err != nil {
return err
} else if c.Type == CommentTypeComment {
UpdateIssueIndexer(c.IssueID)
}
+
+ if err := c.LoadIssue(); err != nil {
+ return err
+ }
+ if err := c.Issue.LoadAttributes(); err != nil {
+ return err
+ }
+
+ mode, _ := AccessLevel(doer.ID, c.Issue.Repo)
+ if err := PrepareWebhooks(c.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{
+ Action: api.HookIssueCommentEdited,
+ Issue: c.Issue.APIFormat(),
+ Comment: c.APIFormat(),
+ Changes: &api.ChangesPayload{
+ Body: &api.ChangesFromPayload{
+ From: oldContent,
+ },
+ },
+ Repository: c.Issue.Repo.APIFormat(mode),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", c.ID, err)
+ }
+
return nil
}
// DeleteComment deletes the comment
-func DeleteComment(comment *Comment) error {
+func DeleteComment(doer *User, comment *Comment) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
@@ -835,6 +889,26 @@ func DeleteComment(comment *Comment) error {
} else if comment.Type == CommentTypeComment {
UpdateIssueIndexer(comment.IssueID)
}
+
+ if err := comment.LoadIssue(); err != nil {
+ return err
+ }
+ if err := comment.Issue.LoadAttributes(); err != nil {
+ return err
+ }
+
+ mode, _ := AccessLevel(doer.ID, comment.Issue.Repo)
+
+ if err := PrepareWebhooks(comment.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{
+ Action: api.HookIssueCommentDeleted,
+ Issue: comment.Issue.APIFormat(),
+ Comment: comment.APIFormat(),
+ Repository: comment.Issue.Repo.APIFormat(mode),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
+ }
+
return nil
}
diff --git a/models/issue_milestone.go b/models/issue_milestone.go
index 8de1f97571..be55dc4f5b 100644
--- a/models/issue_milestone.go
+++ b/models/issue_milestone.go
@@ -5,6 +5,9 @@
package models
import (
+ "fmt"
+
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
api "code.gitea.io/sdk/gitea"
@@ -358,7 +361,49 @@ func ChangeMilestoneAssign(issue *Issue, doer *User, oldMilestoneID int64) (err
if err = changeMilestoneAssign(sess, doer, issue, oldMilestoneID); err != nil {
return err
}
- return sess.Commit()
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ var hookAction api.HookIssueAction
+ if issue.MilestoneID > 0 {
+ hookAction = api.HookIssueMilestoned
+ } else {
+ hookAction = api.HookIssueDemilestoned
+ }
+
+ if err = issue.LoadAttributes(); err != nil {
+ return err
+ }
+
+ mode, _ := AccessLevel(doer.ID, issue.Repo)
+ if issue.IsPull {
+ err = issue.PullRequest.LoadIssue()
+ if err != nil {
+ log.Error(2, "LoadIssue: %v", err)
+ return
+ }
+ err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
+ Action: hookAction,
+ Index: issue.Index,
+ PullRequest: issue.PullRequest.APIFormat(),
+ Repository: issue.Repo.APIFormat(mode),
+ Sender: doer.APIFormat(),
+ })
+ } else {
+ err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuePayload{
+ Action: hookAction,
+ Index: issue.Index,
+ Issue: issue.APIFormat(),
+ Repository: issue.Repo.APIFormat(mode),
+ Sender: doer.APIFormat(),
+ })
+ }
+ if err != nil {
+ log.Error(2, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+ return nil
}
// DeleteMilestoneByRepoID deletes a milestone from a repository.
diff --git a/models/issue_milestone_test.go b/models/issue_milestone_test.go
index c9b53f4f4a..3ea63d2d6b 100644
--- a/models/issue_milestone_test.go
+++ b/models/issue_milestone_test.go
@@ -232,6 +232,8 @@ func TestChangeMilestoneAssign(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{RepoID: 1}).(*Issue)
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
+ assert.NotNil(t, issue)
+ assert.NotNil(t, doer)
oldMilestoneID := issue.MilestoneID
issue.MilestoneID = 2
diff --git a/models/release.go b/models/release.go
index 586f494e7d..bc0260c71d 100644
--- a/models/release.go
+++ b/models/release.go
@@ -10,6 +10,7 @@ import (
"strings"
"code.gitea.io/git"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -190,8 +191,27 @@ func CreateRelease(gitRepo *git.Repository, rel *Release, attachmentUUIDs []stri
}
err = addReleaseAttachments(rel.ID, attachmentUUIDs)
+ if err != nil {
+ return err
+ }
- return err
+ if !rel.IsDraft {
+ if err := rel.LoadAttributes(); err != nil {
+ log.Error(2, "LoadAttributes: %v", err)
+ } else {
+ mode, _ := AccessLevel(rel.PublisherID, rel.Repo)
+ if err := PrepareWebhooks(rel.Repo, HookEventRelease, &api.ReleasePayload{
+ Action: api.HookReleasePublished,
+ Release: rel.APIFormat(),
+ Repository: rel.Repo.APIFormat(mode),
+ Sender: rel.Publisher.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks: %v", err)
+ }
+ }
+ }
+
+ return nil
}
// GetRelease returns release by given ID.
diff --git a/models/repo.go b/models/repo.go
index 4a7eb859c4..f5ec1a9fdd 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -2456,6 +2456,17 @@ func ForkRepository(doer, u *User, oldRepo *Repository, name, desc string) (_ *R
return nil, err
}
+ oldMode, _ := AccessLevel(doer.ID, oldRepo)
+ mode, _ := AccessLevel(doer.ID, repo)
+
+ if err = PrepareWebhooks(oldRepo, HookEventFork, &api.ForkPayload{
+ Forkee: repo.APIFormat(mode),
+ Repo: oldRepo.APIFormat(oldMode),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks [repo_id: %d]: %v", oldRepo.ID, err)
+ }
+
if err = repo.UpdateSize(); err != nil {
log.Error(4, "Failed to update size for repository: %v", err)
}
diff --git a/models/webhook.go b/models/webhook.go
index 62db84f86a..c44ca2960d 100644
--- a/models/webhook.go
+++ b/models/webhook.go
@@ -66,10 +66,15 @@ func IsValidHookContentType(name string) bool {
// HookEvents is a set of web hook events
type HookEvents struct {
- Create bool `json:"create"`
- Push bool `json:"push"`
- PullRequest bool `json:"pull_request"`
- Repository bool `json:"repository"`
+ Create bool `json:"create"`
+ Delete bool `json:"delete"`
+ Fork bool `json:"fork"`
+ Issues bool `json:"issues"`
+ IssueComment bool `json:"issue_comment"`
+ Push bool `json:"push"`
+ PullRequest bool `json:"pull_request"`
+ Repository bool `json:"repository"`
+ Release bool `json:"release"`
}
// HookEvent represents events that will delivery hook.
@@ -155,6 +160,30 @@ func (w *Webhook) HasCreateEvent() bool {
(w.ChooseEvents && w.HookEvents.Create)
}
+// HasDeleteEvent returns true if hook enabled delete event.
+func (w *Webhook) HasDeleteEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Delete)
+}
+
+// HasForkEvent returns true if hook enabled fork event.
+func (w *Webhook) HasForkEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Fork)
+}
+
+// HasIssuesEvent returns true if hook enabled issues event.
+func (w *Webhook) HasIssuesEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Issues)
+}
+
+// HasIssueCommentEvent returns true if hook enabled issue_comment event.
+func (w *Webhook) HasIssueCommentEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueComment)
+}
+
// HasPushEvent returns true if hook enabled push event.
func (w *Webhook) HasPushEvent() bool {
return w.PushOnly || w.SendEverything ||
@@ -167,23 +196,46 @@ func (w *Webhook) HasPullRequestEvent() bool {
(w.ChooseEvents && w.HookEvents.PullRequest)
}
+// HasReleaseEvent returns if hook enabled release event.
+func (w *Webhook) HasReleaseEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Release)
+}
+
// HasRepositoryEvent returns if hook enabled repository event.
func (w *Webhook) HasRepositoryEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Repository)
}
+func (w *Webhook) eventCheckers() []struct {
+ has func() bool
+ typ HookEventType
+} {
+ return []struct {
+ has func() bool
+ typ HookEventType
+ }{
+ {w.HasCreateEvent, HookEventCreate},
+ {w.HasDeleteEvent, HookEventDelete},
+ {w.HasForkEvent, HookEventFork},
+ {w.HasPushEvent, HookEventPush},
+ {w.HasIssuesEvent, HookEventIssues},
+ {w.HasIssueCommentEvent, HookEventIssueComment},
+ {w.HasPullRequestEvent, HookEventPullRequest},
+ {w.HasRepositoryEvent, HookEventRepository},
+ {w.HasReleaseEvent, HookEventRelease},
+ }
+}
+
// EventsArray returns an array of hook events
func (w *Webhook) EventsArray() []string {
- events := make([]string, 0, 3)
- if w.HasCreateEvent() {
- events = append(events, "create")
- }
- if w.HasPushEvent() {
- events = append(events, "push")
- }
- if w.HasPullRequestEvent() {
- events = append(events, "pull_request")
+ events := make([]string, 0, 7)
+
+ for _, c := range w.eventCheckers() {
+ if c.has() {
+ events = append(events, string(c.typ))
+ }
}
return events
}
@@ -373,10 +425,15 @@ type HookEventType string
// Types of hook events
const (
- HookEventCreate HookEventType = "create"
- HookEventPush HookEventType = "push"
- HookEventPullRequest HookEventType = "pull_request"
- HookEventRepository HookEventType = "repository"
+ HookEventCreate HookEventType = "create"
+ HookEventDelete HookEventType = "delete"
+ HookEventFork HookEventType = "fork"
+ HookEventPush HookEventType = "push"
+ HookEventIssues HookEventType = "issues"
+ HookEventIssueComment HookEventType = "issue_comment"
+ HookEventPullRequest HookEventType = "pull_request"
+ HookEventRepository HookEventType = "repository"
+ HookEventRelease HookEventType = "release"
)
// HookRequest represents hook task request information.
@@ -488,22 +545,11 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay
}
func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error {
- switch event {
- case HookEventCreate:
- if !w.HasCreateEvent() {
- return nil
- }
- case HookEventPush:
- if !w.HasPushEvent() {
- return nil
- }
- case HookEventPullRequest:
- if !w.HasPullRequestEvent() {
- return nil
- }
- case HookEventRepository:
- if !w.HasRepositoryEvent() {
- return nil
+ for _, e := range w.eventCheckers() {
+ if event == e.typ {
+ if !e.has() {
+ return nil
+ }
}
}
diff --git a/models/webhook_dingtalk.go b/models/webhook_dingtalk.go
index 719ffcae73..7eb189f9bb 100644
--- a/models/webhook_dingtalk.go
+++ b/models/webhook_dingtalk.go
@@ -49,6 +49,38 @@ func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) {
}, nil
}
+func getDingtalkDeletePayload(p *api.DeletePayload) (*DingtalkPayload, error) {
+ // created tag/branch
+ refName := git.RefEndName(p.Ref)
+ title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
+
+ return &DingtalkPayload{
+ MsgType: "actionCard",
+ ActionCard: dingtalk.ActionCard{
+ Text: title,
+ Title: title,
+ HideAvatar: "0",
+ SingleTitle: fmt.Sprintf("view branch %s", refName),
+ SingleURL: p.Repo.HTMLURL + "/src/" + refName,
+ },
+ }, nil
+}
+
+func getDingtalkForkPayload(p *api.ForkPayload) (*DingtalkPayload, error) {
+ title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
+
+ return &DingtalkPayload{
+ MsgType: "actionCard",
+ ActionCard: dingtalk.ActionCard{
+ Text: title,
+ Title: title,
+ HideAvatar: "0",
+ SingleTitle: fmt.Sprintf("view forked repo %s", p.Repo.FullName),
+ SingleURL: p.Repo.HTMLURL,
+ },
+ }, nil
+}
+
func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) {
var (
branchName = git.RefEndName(p.Ref)
@@ -98,6 +130,80 @@ func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) {
}, nil
}
+func getDingtalkIssuesPayload(p *api.IssuePayload) (*DingtalkPayload, error) {
+ var text, title string
+ switch p.Action {
+ case api.HookIssueOpened:
+ title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ case api.HookIssueClosed:
+ title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ case api.HookIssueReOpened:
+ title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ case api.HookIssueEdited:
+ title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ case api.HookIssueAssigned:
+ title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName,
+ p.Issue.Assignee.UserName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ case api.HookIssueUnassigned:
+ title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ case api.HookIssueLabelUpdated:
+ title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ case api.HookIssueLabelCleared:
+ title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ case api.HookIssueSynchronized:
+ title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ }
+
+ return &DingtalkPayload{
+ MsgType: "actionCard",
+ ActionCard: dingtalk.ActionCard{
+ Text: text,
+ Title: title,
+ HideAvatar: "0",
+ SingleTitle: "view pull request",
+ SingleURL: p.Issue.URL,
+ },
+ }, nil
+}
+
+func getDingtalkIssueCommentPayload(p *api.IssueCommentPayload) (*DingtalkPayload, error) {
+ title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
+ url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID))
+ var content string
+ switch p.Action {
+ case api.HookIssueCommentCreated:
+ title = "New comment: " + title
+ content = p.Comment.Body
+ case api.HookIssueCommentEdited:
+ title = "Comment edited: " + title
+ content = p.Comment.Body
+ case api.HookIssueCommentDeleted:
+ title = "Comment deleted: " + title
+ url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
+ content = p.Comment.Body
+ }
+
+ return &DingtalkPayload{
+ MsgType: "actionCard",
+ ActionCard: dingtalk.ActionCard{
+ Text: content,
+ Title: title,
+ HideAvatar: "0",
+ SingleTitle: "view pull request",
+ SingleURL: url,
+ },
+ }, nil
+}
+
func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) {
var text, title string
switch p.Action {
@@ -182,6 +288,27 @@ func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, e
return nil, nil
}
+func getDingtalkReleasePayload(p *api.ReleasePayload) (*DingtalkPayload, error) {
+ var title, url string
+ switch p.Action {
+ case api.HookReleasePublished:
+ title = fmt.Sprintf("[%s] Release created", p.Release.TagName)
+ url = p.Release.URL
+ return &DingtalkPayload{
+ MsgType: "actionCard",
+ ActionCard: dingtalk.ActionCard{
+ Text: title,
+ Title: title,
+ HideAvatar: "0",
+ SingleTitle: "view repository",
+ SingleURL: url,
+ },
+ }, nil
+ }
+
+ return nil, nil
+}
+
// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload
func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*DingtalkPayload, error) {
s := new(DingtalkPayload)
@@ -189,12 +316,22 @@ func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*Din
switch event {
case HookEventCreate:
return getDingtalkCreatePayload(p.(*api.CreatePayload))
+ case HookEventDelete:
+ return getDingtalkDeletePayload(p.(*api.DeletePayload))
+ case HookEventFork:
+ return getDingtalkForkPayload(p.(*api.ForkPayload))
+ case HookEventIssues:
+ return getDingtalkIssuesPayload(p.(*api.IssuePayload))
+ case HookEventIssueComment:
+ return getDingtalkIssueCommentPayload(p.(*api.IssueCommentPayload))
case HookEventPush:
return getDingtalkPushPayload(p.(*api.PushPayload))
case HookEventPullRequest:
return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload))
case HookEventRepository:
return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload))
+ case HookEventRelease:
+ return getDingtalkReleasePayload(p.(*api.ReleasePayload))
}
return s, nil
diff --git a/models/webhook_discord.go b/models/webhook_discord.go
index 40d9d58992..04ebbc293f 100644
--- a/models/webhook_discord.go
+++ b/models/webhook_discord.go
@@ -115,6 +115,51 @@ func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordP
}, nil
}
+func getDiscordDeletePayload(p *api.DeletePayload, meta *DiscordMeta) (*DiscordPayload, error) {
+ // deleted tag/branch
+ refName := git.RefEndName(p.Ref)
+ title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
+
+ return &DiscordPayload{
+ Username: meta.Username,
+ AvatarURL: meta.IconURL,
+ Embeds: []DiscordEmbed{
+ {
+ Title: title,
+ URL: p.Repo.HTMLURL + "/src/" + refName,
+ Color: warnColor,
+ Author: DiscordEmbedAuthor{
+ Name: p.Sender.UserName,
+ URL: setting.AppURL + p.Sender.UserName,
+ IconURL: p.Sender.AvatarURL,
+ },
+ },
+ },
+ }, nil
+}
+
+func getDiscordForkPayload(p *api.ForkPayload, meta *DiscordMeta) (*DiscordPayload, error) {
+ // fork
+ title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
+
+ return &DiscordPayload{
+ Username: meta.Username,
+ AvatarURL: meta.IconURL,
+ Embeds: []DiscordEmbed{
+ {
+ Title: title,
+ URL: p.Repo.HTMLURL,
+ Color: successColor,
+ Author: DiscordEmbedAuthor{
+ Name: p.Sender.UserName,
+ URL: setting.AppURL + p.Sender.UserName,
+ IconURL: p.Sender.AvatarURL,
+ },
+ },
+ },
+ }, nil
+}
+
func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPayload, error) {
var (
branchName = git.RefEndName(p.Ref)
@@ -165,6 +210,108 @@ func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPaylo
}, nil
}
+func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPayload, error) {
+ var text, title string
+ var color int
+ switch p.Action {
+ case api.HookIssueOpened:
+ title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ color = warnColor
+ case api.HookIssueClosed:
+ title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ color = failedColor
+ text = p.Issue.Body
+ case api.HookIssueReOpened:
+ title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ color = warnColor
+ case api.HookIssueEdited:
+ title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ color = warnColor
+ case api.HookIssueAssigned:
+ title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName,
+ p.Issue.Assignee.UserName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ color = successColor
+ case api.HookIssueUnassigned:
+ title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ color = warnColor
+ case api.HookIssueLabelUpdated:
+ title = fmt.Sprintf("[%s] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ color = warnColor
+ case api.HookIssueLabelCleared:
+ title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ color = warnColor
+ case api.HookIssueSynchronized:
+ title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
+ text = p.Issue.Body
+ color = warnColor
+ }
+
+ return &DiscordPayload{
+ Username: meta.Username,
+ AvatarURL: meta.IconURL,
+ Embeds: []DiscordEmbed{
+ {
+ Title: title,
+ Description: text,
+ URL: p.Issue.URL,
+ Color: color,
+ Author: DiscordEmbedAuthor{
+ Name: p.Sender.UserName,
+ URL: setting.AppURL + p.Sender.UserName,
+ IconURL: p.Sender.AvatarURL,
+ },
+ },
+ },
+ }, nil
+}
+
+func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, discord *DiscordMeta) (*DiscordPayload, error) {
+ title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
+ url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID))
+ content := ""
+ var color int
+ switch p.Action {
+ case api.HookIssueCommentCreated:
+ title = "New comment: " + title
+ content = p.Comment.Body
+ color = successColor
+ case api.HookIssueCommentEdited:
+ title = "Comment edited: " + title
+ content = p.Comment.Body
+ color = warnColor
+ case api.HookIssueCommentDeleted:
+ title = "Comment deleted: " + title
+ url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
+ content = p.Comment.Body
+ color = warnColor
+ }
+
+ return &DiscordPayload{
+ Username: discord.Username,
+ AvatarURL: discord.IconURL,
+ Embeds: []DiscordEmbed{
+ {
+ Title: title,
+ Description: content,
+ URL: url,
+ Color: color,
+ Author: DiscordEmbedAuthor{
+ Name: p.Sender.UserName,
+ URL: setting.AppURL + p.Sender.UserName,
+ IconURL: p.Sender.AvatarURL,
+ },
+ },
+ },
+ }, nil
+}
+
func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) {
var text, title string
var color int
@@ -267,6 +414,35 @@ func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*
}, nil
}
+func getDiscordReleasePayload(p *api.ReleasePayload, meta *DiscordMeta) (*DiscordPayload, error) {
+ var title, url string
+ var color int
+ switch p.Action {
+ case api.HookReleasePublished:
+ title = fmt.Sprintf("[%s] Release created", p.Release.TagName)
+ url = p.Release.URL
+ color = successColor
+ }
+
+ return &DiscordPayload{
+ Username: meta.Username,
+ AvatarURL: meta.IconURL,
+ Embeds: []DiscordEmbed{
+ {
+ Title: title,
+ Description: fmt.Sprintf("%s", p.Release.Note),
+ URL: url,
+ Color: color,
+ Author: DiscordEmbedAuthor{
+ Name: p.Sender.UserName,
+ URL: setting.AppURL + p.Sender.UserName,
+ IconURL: p.Sender.AvatarURL,
+ },
+ },
+ },
+ }, nil
+}
+
// GetDiscordPayload converts a discord webhook into a DiscordPayload
func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) {
s := new(DiscordPayload)
@@ -279,12 +455,22 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*Disc
switch event {
case HookEventCreate:
return getDiscordCreatePayload(p.(*api.CreatePayload), discord)
+ case HookEventDelete:
+ return getDiscordDeletePayload(p.(*api.DeletePayload), discord)
+ case HookEventFork:
+ return getDiscordForkPayload(p.(*api.ForkPayload), discord)
+ case HookEventIssues:
+ return getDiscordIssuesPayload(p.(*api.IssuePayload), discord)
+ case HookEventIssueComment:
+ return getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), discord)
case HookEventPush:
return getDiscordPushPayload(p.(*api.PushPayload), discord)
case HookEventPullRequest:
return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
case HookEventRepository:
return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord)
+ case HookEventRelease:
+ return getDiscordReleasePayload(p.(*api.ReleasePayload), discord)
}
return s, nil
diff --git a/models/webhook_slack.go b/models/webhook_slack.go
index 256819adc5..7b18fe3278 100644
--- a/models/webhook_slack.go
+++ b/models/webhook_slack.go
@@ -106,6 +106,122 @@ func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayloa
}, nil
}
+// getSlackDeletePayload composes Slack payload for delete a branch or tag.
+func getSlackDeletePayload(p *api.DeletePayload, slack *SlackMeta) (*SlackPayload, error) {
+ refName := git.RefEndName(p.Ref)
+ repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
+ return &SlackPayload{
+ Channel: slack.Channel,
+ Text: text,
+ Username: slack.Username,
+ IconURL: slack.IconURL,
+ }, nil
+}
+
+// getSlackForkPayload composes Slack payload for forked by a repository.
+func getSlackForkPayload(p *api.ForkPayload, slack *SlackMeta) (*SlackPayload, error) {
+ baseLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ forkLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
+ text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
+ return &SlackPayload{
+ Channel: slack.Channel,
+ Text: text,
+ Username: slack.Username,
+ IconURL: slack.IconURL,
+ }, nil
+}
+
+func getSlackIssuesPayload(p *api.IssuePayload, slack *SlackMeta) (*SlackPayload, error) {
+ senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+ titleLink := SlackLinkFormatter(fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index),
+ fmt.Sprintf("#%d %s", p.Index, p.Issue.Title))
+ var text, title, attachmentText string
+ switch p.Action {
+ case api.HookIssueOpened:
+ text = fmt.Sprintf("[%s] Issue submitted by %s", p.Repository.FullName, senderLink)
+ title = titleLink
+ attachmentText = SlackTextFormatter(p.Issue.Body)
+ case api.HookIssueClosed:
+ text = fmt.Sprintf("[%s] Issue closed: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HookIssueReOpened:
+ text = fmt.Sprintf("[%s] Issue re-opened: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HookIssueEdited:
+ text = fmt.Sprintf("[%s] Issue edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ attachmentText = SlackTextFormatter(p.Issue.Body)
+ case api.HookIssueAssigned:
+ text = fmt.Sprintf("[%s] Issue assigned to %s: %s by %s", p.Repository.FullName,
+ SlackLinkFormatter(setting.AppURL+p.Issue.Assignee.UserName, p.Issue.Assignee.UserName),
+ titleLink, senderLink)
+ case api.HookIssueUnassigned:
+ text = fmt.Sprintf("[%s] Issue unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HookIssueLabelUpdated:
+ text = fmt.Sprintf("[%s] Issue labels updated: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HookIssueLabelCleared:
+ text = fmt.Sprintf("[%s] Issue labels cleared: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HookIssueSynchronized:
+ text = fmt.Sprintf("[%s] Issue synchronized: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ }
+
+ return &SlackPayload{
+ Channel: slack.Channel,
+ Text: text,
+ Username: slack.Username,
+ IconURL: slack.IconURL,
+ Attachments: []SlackAttachment{{
+ Color: slack.Color,
+ Title: title,
+ Text: attachmentText,
+ }},
+ }, nil
+}
+
+func getSlackIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) (*SlackPayload, error) {
+ senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+ titleLink := SlackLinkFormatter(fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)),
+ fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title))
+ var text, title, attachmentText string
+ switch p.Action {
+ case api.HookIssueCommentCreated:
+ text = fmt.Sprintf("[%s] New comment created by %s", p.Repository.FullName, senderLink)
+ title = titleLink
+ attachmentText = SlackTextFormatter(p.Comment.Body)
+ case api.HookIssueCommentEdited:
+ text = fmt.Sprintf("[%s] Comment edited by %s", p.Repository.FullName, senderLink)
+ title = titleLink
+ attachmentText = SlackTextFormatter(p.Comment.Body)
+ case api.HookIssueCommentDeleted:
+ text = fmt.Sprintf("[%s] Comment deleted by %s", p.Repository.FullName, senderLink)
+ title = SlackLinkFormatter(fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index),
+ fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title))
+ attachmentText = SlackTextFormatter(p.Comment.Body)
+ }
+
+ return &SlackPayload{
+ Channel: slack.Channel,
+ Text: text,
+ Username: slack.Username,
+ IconURL: slack.IconURL,
+ Attachments: []SlackAttachment{{
+ Color: slack.Color,
+ Title: title,
+ Text: attachmentText,
+ }},
+ }, nil
+}
+
+func getSlackReleasePayload(p *api.ReleasePayload, slack *SlackMeta) (*SlackPayload, error) {
+ repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.Name)
+ refLink := SlackLinkFormatter(p.Repository.HTMLURL+"/src/"+p.Release.TagName, p.Release.TagName)
+ text := fmt.Sprintf("[%s] new release %s published by %s", repoLink, refLink, p.Sender.UserName)
+ return &SlackPayload{
+ Channel: slack.Channel,
+ Text: text,
+ Username: slack.Username,
+ IconURL: slack.IconURL,
+ }, nil
+}
+
func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) {
// n new commits
var (
@@ -238,12 +354,22 @@ func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackP
switch event {
case HookEventCreate:
return getSlackCreatePayload(p.(*api.CreatePayload), slack)
+ case HookEventDelete:
+ return getSlackDeletePayload(p.(*api.DeletePayload), slack)
+ case HookEventFork:
+ return getSlackForkPayload(p.(*api.ForkPayload), slack)
+ case HookEventIssues:
+ return getSlackIssuesPayload(p.(*api.IssuePayload), slack)
+ case HookEventIssueComment:
+ return getSlackIssueCommentPayload(p.(*api.IssueCommentPayload), slack)
case HookEventPush:
return getSlackPushPayload(p.(*api.PushPayload), slack)
case HookEventPullRequest:
return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack)
case HookEventRepository:
return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack)
+ case HookEventRelease:
+ return getSlackReleasePayload(p.(*api.ReleasePayload), slack)
}
return s, nil
diff --git a/models/webhook_test.go b/models/webhook_test.go
index eeae7efbcb..50106a3792 100644
--- a/models/webhook_test.go
+++ b/models/webhook_test.go
@@ -73,7 +73,7 @@ func TestWebhook_UpdateEvent(t *testing.T) {
}
func TestWebhook_EventsArray(t *testing.T) {
- assert.Equal(t, []string{"create", "push", "pull_request"},
+ assert.Equal(t, []string{"create", "delete", "fork", "push", "issues", "issue_comment", "pull_request", "repository", "release"},
(&Webhook{
HookEvent: &HookEvent{SendEverything: true},
}).EventsArray(),
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index 8b57e7eda9..a8c2270a2d 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -155,12 +155,17 @@ func (f *ProtectBranchForm) Validate(ctx *macaron.Context, errs binding.Errors)
// WebhookForm form for changing web hook
type WebhookForm struct {
- Events string
- Create bool
- Push bool
- PullRequest bool
- Repository bool
- Active bool
+ Events string
+ Create bool
+ Delete bool
+ Fork bool
+ Issues bool
+ IssueComment bool
+ Release bool
+ Push bool
+ PullRequest bool
+ Repository bool
+ Active bool
}
// PushOnly if the hook will be triggered when push
diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go
index 5906abcd1d..1b00f62634 100644
--- a/modules/auth/user_form.go
+++ b/modules/auth/user_form.go
@@ -184,7 +184,7 @@ func (f *AddKeyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding
// NewAccessTokenForm form for creating access token
type NewAccessTokenForm struct {
- Name string `binding:"Required"`
+ Name string `binding:"Required;MaxSize(255)"`
}
// Validate valideates the fields
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index b5a6d537bd..156bf9175a 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -311,7 +311,6 @@ security=Sicherheit
avatar=Profilbild
ssh_gpg_keys=SSH / GPG Schlüssel
social=Soziale Konten
-applications=Zugriffstoken
orgs=Organisationen verwalten
repos=Repositories
delete=Konto löschen
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 2544e37d26..91847f9e8f 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1011,6 +1011,16 @@ settings.event_send_everything = All Events
settings.event_choose = Custom Events…
settings.event_create = Create
settings.event_create_desc = Branch or tag created.
+settings.event_delete = Delete
+settings.event_delete_desc = Branch or tag deleted
+settings.event_fork = Fork
+settings.event_fork_desc = Repository forked
+settings.event_issues = Issues
+settings.event_issues_desc = Issue opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared, milestoned, or demilestoned.
+settings.event_issue_comment = Issue Comment
+settings.event_issue_comment_desc = Issue comment created, edited, or deleted.
+settings.event_release = Release
+settings.event_release_desc = Release published in a repository.
settings.event_pull_request = Pull Request
settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared or synchronized.
settings.event_push = Push
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index 7aed99a37c..090063bae0 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -306,12 +306,13 @@ form.name_pattern_not_allowed=O padrão de '%s' não é permitido em um nome de
[settings]
profile=Perfil
+account=Conta
password=Senha
security=Segurança
avatar=Avatar
ssh_gpg_keys=Chaves SSH / GPG
social=Contas sociais
-applications=Tokens de acesso
+applications=Aplicações
orgs=Gerenciar organizações
repos=Repositórios
delete=Excluir conta
@@ -334,7 +335,7 @@ continue=Continuar
cancel=Cancelar
language=Idioma
-lookup_avatar_by_mail=Procure o avatar do endereço de e-mail
+lookup_avatar_by_mail=Procurar o avatar do endereço de e-mail
federated_avatar_lookup=Busca de avatar federativo
enable_custom_avatar=Habilitar avatar customizado
choose_new_avatar=Escolha um novo avatar
@@ -344,7 +345,7 @@ uploaded_avatar_not_a_image=O arquivo enviado não é uma imagem.
update_avatar_success=Seu avatar foi atualizado.
change_password=Atualizar senha
-old_password=Senha Atual
+old_password=Senha atual
new_password=Nova senha
retype_new_password=Digite a nova senha novamente
password_incorrect=A senha atual está incorreta.
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index c6487d7c4e..8ae7de9fa1 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -155,14 +155,14 @@ org_no_results=Відповідних організацій не знайден
code_search_results=Результати пошуку '%s'
[auth]
-create_new_account=Реєстрація аккаунта
+create_new_account=Реєстрація облікового запису
register_helper_msg=Вже зареєстровані? Увійдіть зараз!
disable_register_prompt=Вибачте, можливість реєстрації відключена. Будь ласка, зв'яжіться з адміністратором сайту.
disable_register_mail=Підтвердження реєстрації електронною поштою вимкнено.
remember_me=Запам'ятати мене
forgot_password_title=Забув пароль
forgot_password=Забули пароль?
-sign_up_now=Потрібен аккаунт? Зареєструватися.
+sign_up_now=Потрібен обліковий запис? Зареєструйтеся зараз.
confirmation_mail_sent_prompt=Новий лист для підтвердження було відправлено на %s, будь ласка, перевірте вашу поштову скриньку протягом %s для завершення реєстрації.
reset_password_mail_sent_prompt=Лист для підтвердження було відправлено на %s. Будь ласка, перевірте вашу поштову скриньку протягом %s для скидання пароля.
active_your_account=Активувати обліковий запис
@@ -264,16 +264,18 @@ form.name_reserved=Ім'я користувача "%s" зарезервован
[settings]
profile=Профіль
+account=Обліковий запис
password=Пароль
security=Безпека
avatar=Аватар
ssh_gpg_keys=SSH / GPG ключі
-social=Соціальні акаунти
-applications=Токени Доступу
+social=Соціальні облікові записи
+applications=Додатки
orgs=Керування організаціями
repos=Репозиторії
delete=Видалити обліковий запис
twofa=Двофакторна авторизація
+account_link=Прив'язані облікові записи
organization=Організації
uid=Ідентифікатор Uid
@@ -339,7 +341,7 @@ show_openid=Показати у профілю
hide_openid=Не показувати у профілі
ssh_disabled=SSH вимкнено
-manage_social=Керувати зв'язаними аккаунтами соціальних мереж
+manage_social=Керувати зв'язаними обліковими записами соціальних мереж
unbind=Від'єднати
generate_new_token=Згенерувати новий токен
@@ -350,6 +352,8 @@ delete_token=Видалити
twofa_disable=Вимкнути двофакторну автентифікацію
or_enter_secret=Або введіть секрет: %s
+manage_account_links=Керування обліковими записами
+remove_account_link=Видалити облікові записи
delete_account=Видалити ваш обліковий запис
@@ -479,6 +483,7 @@ issues.new.open_milestone=Активні етапи
issues.new.closed_milestone=Закриті етапи
issues.new.assignees=Виконавеці
issues.new.clear_assignees=Прибрати виконавеців
+issues.new.no_assignees=Ніхто не призначений
issues.no_ref=Не вказана гілка або тег
issues.create=Створити проблему
issues.new_label=Нова мітка
@@ -592,6 +597,7 @@ pulls.tab_commits=Коміти
pulls.tab_files=Змінені файли
pulls.reopen_to_merge=Будь ласка перевідкрийте цей запит щоб здіснити операцію злиття.
pulls.merged=Злито
+pulls.has_merged=Запит на злиття було об'єднано.
pulls.can_auto_merge_desc=Цей запит можна об'єднати автоматично.
pulls.merge_pull_request=Об'єднати запит на злиття
@@ -671,6 +677,7 @@ search.search_repo=Пошук репозиторію
settings=Налаштування
settings.options=Репозиторій
+settings.collaboration=Співробітники
settings.collaboration.admin=Адміністратор
settings.collaboration.write=Запис
settings.collaboration.read=Читати
@@ -696,6 +703,7 @@ settings.admin_settings=Налаштування адміністратора
settings.danger_zone=Небезпечна зона
settings.new_owner_has_same_repo=Новий власник вже має репозиторій з такою назвою. Будь ласка, виберіть інше ім'я.
settings.convert=Перетворити на звичайний репозиторій
+settings.convert_desc=Ви можете сконвертувати це дзеркало у звичайний репозиторій. Це не може бути скасовано.
settings.transfer=Передати новому власнику
settings.wiki_delete=Видалити Wiki-дані
settings.confirm_wiki_delete=Видалити Wiki-дані
@@ -724,6 +732,7 @@ settings.slack_icon_url=URL іконки
settings.discord_username=Ім'я кристувача
settings.discord_icon_url=URL іконки
settings.slack_color=Колір
+settings.event_push_only=Push події
settings.event_send_everything=Всі події
settings.event_create=Створити
settings.event_create_desc=Гілку або тег створено.
@@ -811,6 +820,7 @@ org_desc=Опис
team_name=Назва команди
team_desc=Опис
team_permission_desc=Права доступу
+team_unit_desc=Дозволити доступ до розділів репозиторію
settings=Налаштування
@@ -862,11 +872,13 @@ last_page=Остання
total=Разом: %d
dashboard.statistic=Підсумок
+dashboard.system_status=Статус системи
dashboard.operation_name=Назва операції
dashboard.operation_switch=Перемкнути
dashboard.operation_run=Запустити
dashboard.delete_inactivate_accounts=Видалити всі неактивні облікові записи
dashboard.delete_inactivate_accounts_success=Усі неактивні облікові записи успішно видалено.
+dashboard.git_gc_repos_success=Всі репозиторії завершили збирання сміття.
dashboard.server_uptime=Uptime серверу
dashboard.current_memory_usage=Поточне використання пам'яті
dashboard.total_memory_allocated=Виділено пам'яті загалом
@@ -877,9 +889,14 @@ dashboard.mspan_structures_obtained=Отримано структур MSpan
dashboard.mcache_structures_usage=Використання структур MCache
dashboard.mcache_structures_obtained=Отримано структур MCache
dashboard.profiling_bucket_hash_table_obtained=Отримано хеш-таблиць профілювання
-dashboard.gc_metadata_obtained=Отримано метаданих GC
+dashboard.gc_metadata_obtained=Отримано метаданих збирача сміття (GC)
dashboard.other_system_allocation_obtained=Отримання інших виділень пам'яті
-dashboard.next_gc_recycle=Наступний цикл GC
+dashboard.next_gc_recycle=Наступний цикл збирача сміття (GC)
+dashboard.last_gc_time=З останнього запуску збирача сміття (GC)
+dashboard.total_gc_time=Загальна пауза збирача сміття (GC)
+dashboard.total_gc_pause=Загальна пауза збирача сміття (GC)
+dashboard.last_gc_pause=Остання пауза збирача сміття (GC)
+dashboard.gc_times=Кількість запусків збирача сміття (GC)
users.user_manage_panel=Керування обліковими записами користувачів
users.new_account=Створити обліковий запис
@@ -893,6 +910,7 @@ users.send_register_notify=Надіслати повідомлення про р
users.edit=Редагувати
users.auth_source=Джерело автентифікації
users.local=Локальні
+users.edit_account=Редагувати обліковий запис
users.max_repo_creation=Максимальна кількість репозиторіїв
users.max_repo_creation_desc=(Введіть -1, щоб використовувати глобальний ліміт за замовчуванням.)
users.is_activated=Обліковий запис користувача увімкнено
@@ -1036,7 +1054,7 @@ config.session_provider=Провайдер сесії
config.provider_config=Конфігурація постачальника
config.cookie_name=Ім'я файлу cookie
config.enable_set_cookie=Увімкнути встановлення cookie
-config.gc_interval_time=Інтервал запуску GC
+config.gc_interval_time=Інтервал запуску збирача сміття (GC)
config.session_life_time=Час життя сесії
config.https_only=Тільки HTTPS
config.cookie_life_time=Час життя cookie-файлу
@@ -1050,12 +1068,12 @@ config.git_disable_diff_highlight=Вимкнути підсвітку синта
config.git_max_diff_lines=Максимум рядків на diff (на один файл)
config.git_max_diff_line_characters=Максимум символів на diff (на одну строку)
config.git_max_diff_files=Максимум diff-файлів (для показу)
-config.git_gc_args=Аргументи GC
+config.git_gc_args=Аргументи збирача сміття (GC)
config.git_migrate_timeout=Тайм-аут міграції
config.git_mirror_timeout=Тайм-аут оновлення дзеркала
config.git_clone_timeout=Тайм-аут операції клонування
config.git_pull_timeout=Тайм-аут операції Pull
-config.git_gc_timeout=Тайм-аут операції GC
+config.git_gc_timeout=Тайм-аут операції збирача сміття (GC)
config.log_config=Конфігурація журналу
config.log_mode=Режим журналювання
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index e82892f08b..84db165ff6 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -306,12 +306,13 @@ form.name_pattern_not_allowed=用户名中不允许使用 "%s"。
[settings]
profile=个人信息
+account=账号
password=修改密码
security=安全
avatar=头像设置
ssh_gpg_keys=SSH / GPG 密钥
social=社交帐号绑定
-applications=访问令牌(Access Tokens)
+applications=应用
orgs=管理组织
repos=仓库列表
delete=删除帐户
@@ -329,7 +330,7 @@ location=所在地区
update_profile=更新信息
update_profile_success=您的资料信息已经更新
change_username=您的用户名已更改。
-change_username_prompt=注意:更改账户名将同时改变账户的URL
+change_username_prompt=注意:更改账号名将同时改变账号的URL
continue=继续操作
cancel=取消操作
language=界面语言
@@ -379,7 +380,7 @@ manage_ssh_keys=管理 SSH 密钥
manage_gpg_keys=管理 GPG 密钥
add_key=增加密钥
ssh_desc=这些 SSH 公钥已经关联到你的账号。相应的私钥拥有完全操作你的仓库的权限。
-gpg_desc=这些 GPG 公钥已经关联到你的账户。请妥善保管你的私钥因为他们将被用于认证提交。
+gpg_desc=这些 GPG 公钥已经关联到你的账号。请妥善保管你的私钥因为他们将被用于认证提交。
ssh_helper=需要帮助? 请查看有关 如何生成 SSH 密钥 或 常见 SSH 问题 寻找答案。
gpg_helper=需要帮助吗?看一看 GitHub 关于GPG 的指导。
add_new_key=增加 SSH 密钥
@@ -422,31 +423,31 @@ unbind_success=社会帐户已从您的帐户中解除绑定。
manage_access_token=管理Access Tokens
generate_new_token=生成新的令牌
tokens_desc=这些令牌拥有通过 Gitea API 对您的帐户的访问权限。
-new_token_desc=使用令牌的应用拥有完全访问你的账户的权限。
+new_token_desc=使用令牌的应用拥有完全访问你的账号的权限。
token_name=令牌名称
generate_token=生成令牌
generate_token_success=新令牌生成成功。请拷贝因为令牌将只会显示一次。
delete_token=删除令牌
access_token_deletion=删除Access Tokens
access_token_deletion_desc=删除一个令牌将会组织通过它访问你账号的应用。是否继续?
-delete_token_success=令牌已经被删除。使用该令牌的应用将不再能够访问你的账户。
+delete_token_success=令牌已经被删除。使用该令牌的应用将不再能够访问你的账号。
-twofa_desc=两步验证可以加强你的账户安全性。
-twofa_is_enrolled=你的账户已启用了两步验证。
+twofa_desc=两步验证可以加强你的账号安全性。
+twofa_is_enrolled=你的账号已启用了两步验证。
twofa_not_enrolled=你的账号未开启两步验证。
twofa_disable=禁用两步认证
twofa_scratch_token_regenerate=重新生成初始令牌
twofa_scratch_token_regenerated=你的初始令牌是 %s。请将它保存到一个安全的地方。
twofa_enroll=启用两步验证
twofa_disable_note=如果需要, 可以禁用双因素身份验证。
-twofa_disable_desc=关掉两步验证会使得您的账户不安全,继续执行?
+twofa_disable_desc=关掉两步验证会使得您的账号不安全,继续执行?
regenerate_scratch_token_desc=如果您丢失了您的验证口令或已经使用它登录, 您可以在这里重置它。
twofa_disabled=两步验证已被禁用。
scan_this_image=使用您的授权应用扫描这张图片:
or_enter_secret=或者输入密钥:%s
then_enter_passcode=并输入应用程序中显示的密码:
passcode_invalid=密码不正确。再试一次。
-twofa_enrolled=你的账户已经启用了两步验证。请保存初始令牌(%s)到一个安全的地方,此令牌仅当前显示一次。
+twofa_enrolled=你的账号已经启用了两步验证。请保存初始令牌(%s)到一个安全的地方,此令牌仅当前显示一次。
manage_account_links=管理绑定过的账号
manage_account_links_desc=这些外部帐户已经绑定到您的 Gitea 帐户。
@@ -999,6 +1000,16 @@ settings.event_send_everything=所有事件
settings.event_choose=自定义事件...
settings.event_create=创建
settings.event_create_desc=创建分支或标签
+settings.event_delete=刪除
+settings.event_delete_desc=删除分支或标签
+settings.event_fork=派生
+settings.event_fork_desc=仓库被派生
+settings.event_issues=工单
+settings.event_issues_desc=工单被开启、关闭、重新开启、编辑、指派、取消指派、更新标签、清除标签、设置里程碑或取消设置里程碑
+settings.event_issue_comment=工单评论
+settings.event_issue_comment_desc=工单评论被创建、编辑或删除
+settings.event_release=版本发布
+settings.event_release_desc=仓库发布新的版本。
settings.event_pull_request=合并请求
settings.event_pull_request_desc=开启、关闭、重新开启、编辑、指派、取消指派、更新标签、清除标签或同步合并请求
settings.event_push=推送
@@ -1129,7 +1140,7 @@ branch.restore_failed=未能还原分支%s。
branch.protected_deletion_failed=分支 '%s' 已被保护,不可删除。
topic.manage_topics=管理主题
-topic.done=已完成
+topic.done=保存
[org]
org_name_holder=组织名称
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index a9258849ea..2865ea9165 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -261,8 +261,9 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return
}
+ oldContent := comment.Content
comment.Content = form.Body
- if err := models.UpdateComment(comment); err != nil {
+ if err := models.UpdateComment(ctx.User, comment, oldContent); err != nil {
ctx.Error(500, "UpdateComment", err)
return
}
@@ -348,7 +349,7 @@ func deleteIssueComment(ctx *context.APIContext) {
return
}
- if err = models.DeleteComment(comment); err != nil {
+ if err = models.DeleteComment(ctx.User, comment); err != nil {
ctx.Error(500, "DeleteCommentByID", err)
return
}
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index e1533da94c..d0538ec54f 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -7,12 +7,13 @@ package utils
import (
api "code.gitea.io/sdk/gitea"
+ "encoding/json"
+ "net/http"
+
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/convert"
- "encoding/json"
"github.com/Unknwon/com"
- "net/http"
)
// GetOrgHook get an organization's webhook. If there is an error, write to
@@ -98,9 +99,15 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
HookEvent: &models.HookEvent{
ChooseEvents: true,
HookEvents: models.HookEvents{
- Create: com.IsSliceContainsStr(form.Events, string(models.HookEventCreate)),
- Push: com.IsSliceContainsStr(form.Events, string(models.HookEventPush)),
- PullRequest: com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest)),
+ Create: com.IsSliceContainsStr(form.Events, string(models.HookEventCreate)),
+ Delete: com.IsSliceContainsStr(form.Events, string(models.HookEventDelete)),
+ Fork: com.IsSliceContainsStr(form.Events, string(models.HookEventFork)),
+ Issues: com.IsSliceContainsStr(form.Events, string(models.HookEventIssues)),
+ IssueComment: com.IsSliceContainsStr(form.Events, string(models.HookEventIssueComment)),
+ Push: com.IsSliceContainsStr(form.Events, string(models.HookEventPush)),
+ PullRequest: com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest)),
+ Repository: com.IsSliceContainsStr(form.Events, string(models.HookEventRepository)),
+ Release: com.IsSliceContainsStr(form.Events, string(models.HookEventRelease)),
},
},
IsActive: form.Active,
@@ -211,6 +218,16 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *models.Webho
w.Create = com.IsSliceContainsStr(form.Events, string(models.HookEventCreate))
w.Push = com.IsSliceContainsStr(form.Events, string(models.HookEventPush))
w.PullRequest = com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest))
+ w.Create = com.IsSliceContainsStr(form.Events, string(models.HookEventCreate))
+ w.Delete = com.IsSliceContainsStr(form.Events, string(models.HookEventDelete))
+ w.Fork = com.IsSliceContainsStr(form.Events, string(models.HookEventFork))
+ w.Issues = com.IsSliceContainsStr(form.Events, string(models.HookEventIssues))
+ w.IssueComment = com.IsSliceContainsStr(form.Events, string(models.HookEventIssueComment))
+ w.Push = com.IsSliceContainsStr(form.Events, string(models.HookEventPush))
+ w.PullRequest = com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest))
+ w.Repository = com.IsSliceContainsStr(form.Events, string(models.HookEventRepository))
+ w.Release = com.IsSliceContainsStr(form.Events, string(models.HookEventRelease))
+
if err := w.UpdateEvent(); err != nil {
ctx.Error(500, "UpdateEvent", err)
return false
diff --git a/routers/org/setting.go b/routers/org/setting.go
index 937697d07e..7f652c11d6 100644
--- a/routers/org/setting.go
+++ b/routers/org/setting.go
@@ -13,7 +13,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/routers/user"
+ userSetting "code.gitea.io/gitea/routers/user/setting"
)
const (
@@ -91,7 +91,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) {
// SettingsAvatar response for change avatar on settings page
func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) {
form.Source = auth.AvatarLocal
- if err := user.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil {
+ if err := userSetting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil {
ctx.Flash.Error(err.Error())
} else {
ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success"))
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index c18f0bb352..19bf346cf8 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -1102,6 +1102,7 @@ func UpdateCommentContent(ctx *context.Context) {
return
}
+ oldContent := comment.Content
comment.Content = ctx.Query("content")
if len(comment.Content) == 0 {
ctx.JSON(200, map[string]interface{}{
@@ -1109,7 +1110,7 @@ func UpdateCommentContent(ctx *context.Context) {
})
return
}
- if err = models.UpdateComment(comment); err != nil {
+ if err = models.UpdateComment(ctx.User, comment, oldContent); err != nil {
ctx.ServerError("UpdateComment", err)
return
}
@@ -1135,7 +1136,7 @@ func DeleteComment(ctx *context.Context) {
return
}
- if err = models.DeleteComment(comment); err != nil {
+ if err = models.DeleteComment(ctx.User, comment); err != nil {
ctx.ServerError("DeleteCommentByID", err)
return
}
diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go
index 35fdf796b5..6994aa3344 100644
--- a/routers/repo/webhook.go
+++ b/routers/repo/webhook.go
@@ -23,9 +23,9 @@ import (
)
const (
- tplHooks base.TplName = "repo/settings/hooks"
- tplHookNew base.TplName = "repo/settings/hook_new"
- tplOrgHookNew base.TplName = "org/settings/hook_new"
+ tplHooks base.TplName = "repo/settings/webhook/base"
+ tplHookNew base.TplName = "repo/settings/webhook/new"
+ tplOrgHookNew base.TplName = "org/settings/webhook/new"
)
// Webhooks render web hooks list page
@@ -118,10 +118,15 @@ func ParseHookEvent(form auth.WebhookForm) *models.HookEvent {
SendEverything: form.SendEverything(),
ChooseEvents: form.ChooseEvents(),
HookEvents: models.HookEvents{
- Create: form.Create,
- Push: form.Push,
- PullRequest: form.PullRequest,
- Repository: form.Repository,
+ Create: form.Create,
+ Delete: form.Delete,
+ Fork: form.Fork,
+ Issues: form.Issues,
+ IssueComment: form.IssueComment,
+ Release: form.Release,
+ Push: form.Push,
+ PullRequest: form.PullRequest,
+ Repository: form.Repository,
},
}
}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index f51d62d4d4..6cd84e9f34 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/routers/private"
"code.gitea.io/gitea/routers/repo"
"code.gitea.io/gitea/routers/user"
+ userSetting "code.gitea.io/gitea/routers/user/setting"
"github.com/go-macaron/binding"
"github.com/go-macaron/cache"
@@ -216,39 +217,39 @@ func RegisterRoutes(m *macaron.Macaron) {
}, reqSignOut)
m.Group("/user/settings", func() {
- m.Get("", user.Settings)
- m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost)
- m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), user.SettingsAvatarPost)
- m.Post("/avatar/delete", user.SettingsDeleteAvatar)
+ m.Get("", userSetting.Profile)
+ m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost)
+ m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), userSetting.AvatarPost)
+ m.Post("/avatar/delete", userSetting.DeleteAvatar)
m.Group("/account", func() {
- m.Combo("").Get(user.SettingsAccount).Post(bindIgnErr(auth.ChangePasswordForm{}), user.SettingsAccountPost)
- m.Post("/email", bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost)
- m.Post("/email/delete", user.DeleteEmail)
- m.Post("/delete", user.SettingsDelete)
+ m.Combo("").Get(userSetting.Account).Post(bindIgnErr(auth.ChangePasswordForm{}), userSetting.AccountPost)
+ m.Post("/email", bindIgnErr(auth.AddEmailForm{}), userSetting.EmailPost)
+ m.Post("/email/delete", userSetting.DeleteEmail)
+ m.Post("/delete", userSetting.DeleteAccount)
})
m.Group("/security", func() {
- m.Get("", user.SettingsSecurity)
+ m.Get("", userSetting.Security)
m.Group("/two_factor", func() {
- m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch)
- m.Post("/disable", user.SettingsTwoFactorDisable)
- m.Get("/enroll", user.SettingsTwoFactorEnroll)
- m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), user.SettingsTwoFactorEnrollPost)
+ m.Post("/regenerate_scratch", userSetting.RegenerateScratchTwoFactor)
+ m.Post("/disable", userSetting.DisableTwoFactor)
+ m.Get("/enroll", userSetting.EnrollTwoFactor)
+ m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost)
})
m.Group("/openid", func() {
- m.Post("", bindIgnErr(auth.AddOpenIDForm{}), user.SettingsOpenIDPost)
- m.Post("/delete", user.DeleteOpenID)
- m.Post("/toggle_visibility", user.ToggleOpenIDVisibility)
+ m.Post("", bindIgnErr(auth.AddOpenIDForm{}), userSetting.OpenIDPost)
+ m.Post("/delete", userSetting.DeleteOpenID)
+ m.Post("/toggle_visibility", userSetting.ToggleOpenIDVisibility)
}, openIDSignInEnabled)
- m.Post("/account_link", user.SettingsDeleteAccountLink)
+ m.Post("/account_link", userSetting.DeleteAccountLink)
})
- m.Combo("/applications").Get(user.SettingsApplications).
- Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
- m.Post("/applications/delete", user.SettingsDeleteApplication)
- m.Combo("/keys").Get(user.SettingsKeys).
- Post(bindIgnErr(auth.AddKeyForm{}), user.SettingsKeysPost)
- m.Post("/keys/delete", user.DeleteKey)
- m.Get("/organization", user.SettingsOrganization)
- m.Get("/repos", user.SettingsRepos)
+ m.Combo("/applications").Get(userSetting.Applications).
+ Post(bindIgnErr(auth.NewAccessTokenForm{}), userSetting.ApplicationsPost)
+ m.Post("/applications/delete", userSetting.DeleteApplication)
+ m.Combo("/keys").Get(userSetting.Keys).
+ Post(bindIgnErr(auth.AddKeyForm{}), userSetting.KeysPost)
+ m.Post("/keys/delete", userSetting.DeleteKey)
+ m.Get("/organization", userSetting.Organization)
+ m.Get("/repos", userSetting.Repos)
// redirects from old settings urls to new ones
// TODO: can be removed on next major version
diff --git a/routers/user/setting.go b/routers/user/setting.go
deleted file mode 100644
index 1c760e210c..0000000000
--- a/routers/user/setting.go
+++ /dev/null
@@ -1,808 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package user
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io/ioutil"
- "strings"
-
- "github.com/Unknwon/com"
- "github.com/Unknwon/i18n"
- "github.com/pquerna/otp"
- "github.com/pquerna/otp/totp"
-
- "encoding/base64"
- "html/template"
- "image/png"
-
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/modules/auth"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
-)
-
-const (
- tplSettingsProfile base.TplName = "user/settings/profile"
- tplSettingsAccount base.TplName = "user/settings/account"
- tplSettingsSecurity base.TplName = "user/settings/security"
- tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll"
- tplSettingsApplications base.TplName = "user/settings/applications"
- tplSettingsKeys base.TplName = "user/settings/keys"
- tplSettingsOrganization base.TplName = "user/settings/organization"
- tplSettingsRepositories base.TplName = "user/settings/repos"
-)
-
-// Settings render user's profile page
-func Settings(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsProfile"] = true
- ctx.HTML(200, tplSettingsProfile)
-}
-
-func handleUsernameChange(ctx *context.Context, newName string) {
- // Non-local users are not allowed to change their username.
- if len(newName) == 0 || !ctx.User.IsLocal() {
- return
- }
-
- // Check if user name has been changed
- if ctx.User.LowerName != strings.ToLower(newName) {
- if err := models.ChangeUserName(ctx.User, newName); err != nil {
- switch {
- case models.IsErrUserAlreadyExist(err):
- ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
- ctx.Redirect(setting.AppSubURL + "/user/settings")
- case models.IsErrEmailAlreadyUsed(err):
- ctx.Flash.Error(ctx.Tr("form.email_been_used"))
- ctx.Redirect(setting.AppSubURL + "/user/settings")
- case models.IsErrNameReserved(err):
- ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
- ctx.Redirect(setting.AppSubURL + "/user/settings")
- case models.IsErrNamePatternNotAllowed(err):
- ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
- ctx.Redirect(setting.AppSubURL + "/user/settings")
- default:
- ctx.ServerError("ChangeUserName", err)
- }
- return
- }
- log.Trace("User name changed: %s -> %s", ctx.User.Name, newName)
- }
-
- // In case it's just a case change
- ctx.User.Name = newName
- ctx.User.LowerName = strings.ToLower(newName)
-}
-
-// SettingsPost response for change user's profile
-func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsProfile"] = true
-
- if ctx.HasError() {
- ctx.HTML(200, tplSettingsProfile)
- return
- }
-
- handleUsernameChange(ctx, form.Name)
- if ctx.Written() {
- return
- }
-
- ctx.User.FullName = form.FullName
- ctx.User.Email = form.Email
- ctx.User.KeepEmailPrivate = form.KeepEmailPrivate
- ctx.User.Website = form.Website
- ctx.User.Location = form.Location
- ctx.User.Language = form.Language
- if err := models.UpdateUserSetting(ctx.User); err != nil {
- if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
- ctx.Flash.Error(ctx.Tr("form.email_been_used"))
- ctx.Redirect(setting.AppSubURL + "/user/settings")
- return
- }
- ctx.ServerError("UpdateUser", err)
- return
- }
-
- // Update the language to the one we just set
- ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL)
-
- log.Trace("User settings updated: %s", ctx.User.Name)
- ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success"))
- ctx.Redirect(setting.AppSubURL + "/user/settings")
-}
-
-// UpdateAvatarSetting update user's avatar
-// FIXME: limit size.
-func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *models.User) error {
- ctxUser.UseCustomAvatar = form.Source == auth.AvatarLocal
- if len(form.Gravatar) > 0 {
- ctxUser.Avatar = base.EncodeMD5(form.Gravatar)
- ctxUser.AvatarEmail = form.Gravatar
- }
-
- if form.Avatar != nil {
- fr, err := form.Avatar.Open()
- if err != nil {
- return fmt.Errorf("Avatar.Open: %v", err)
- }
- defer fr.Close()
-
- data, err := ioutil.ReadAll(fr)
- if err != nil {
- return fmt.Errorf("ioutil.ReadAll: %v", err)
- }
- if !base.IsImageFile(data) {
- return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
- }
- if err = ctxUser.UploadAvatar(data); err != nil {
- return fmt.Errorf("UploadAvatar: %v", err)
- }
- } else {
- // No avatar is uploaded but setting has been changed to enable,
- // generate a random one when needed.
- if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) {
- if err := ctxUser.GenerateRandomAvatar(); err != nil {
- log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err)
- }
- }
- }
-
- if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
- return fmt.Errorf("UpdateUser: %v", err)
- }
-
- return nil
-}
-
-// SettingsAvatarPost response for change user's avatar request
-func SettingsAvatarPost(ctx *context.Context, form auth.AvatarForm) {
- if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil {
- ctx.Flash.Error(err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("settings.update_avatar_success"))
- }
-
- ctx.Redirect(setting.AppSubURL + "/user/settings")
-}
-
-// SettingsDeleteAvatar render delete avatar page
-func SettingsDeleteAvatar(ctx *context.Context) {
- if err := ctx.User.DeleteAvatar(); err != nil {
- ctx.Flash.Error(err.Error())
- }
-
- ctx.Redirect(setting.AppSubURL + "/user/settings")
-}
-
-// SettingsAccount renders change user's password, user's email and user suicide page
-func SettingsAccount(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsAccount"] = true
- ctx.Data["Email"] = ctx.User.Email
-
- emails, err := models.GetEmailAddresses(ctx.User.ID)
- if err != nil {
- ctx.ServerError("GetEmailAddresses", err)
- return
- }
- ctx.Data["Emails"] = emails
-
- ctx.HTML(200, tplSettingsAccount)
-}
-
-// SettingsAccountPost response for change user's password
-func SettingsAccountPost(ctx *context.Context, form auth.ChangePasswordForm) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsAccount"] = true
-
- if ctx.HasError() {
- ctx.HTML(200, tplSettingsAccount)
- return
- }
-
- if len(form.Password) < setting.MinPasswordLength {
- ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
- } else if ctx.User.IsPasswordSet() && !ctx.User.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 {
- var err error
- if ctx.User.Salt, err = models.GetUserSalt(); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- ctx.User.HashPassword(form.Password)
- if err := models.UpdateUserCols(ctx.User, "salt", "passwd"); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- log.Trace("User password updated: %s", ctx.User.Name)
- ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
- }
-
- ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-}
-
-// SettingsEmailPost response for change user's email
-func SettingsEmailPost(ctx *context.Context, form auth.AddEmailForm) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsAccount"] = true
-
- // Make emailaddress primary.
- if ctx.Query("_method") == "PRIMARY" {
- if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil {
- ctx.ServerError("MakeEmailPrimary", err)
- return
- }
-
- log.Trace("Email made primary: %s", ctx.User.Name)
- ctx.Redirect(setting.AppSubURL + "/user/settings/account")
- return
- }
-
- // Add Email address.
- emails, err := models.GetEmailAddresses(ctx.User.ID)
- if err != nil {
- ctx.ServerError("GetEmailAddresses", err)
- return
- }
- ctx.Data["Emails"] = emails
-
- if ctx.HasError() {
- ctx.HTML(200, tplSettingsAccount)
- return
- }
-
- email := &models.EmailAddress{
- UID: ctx.User.ID,
- Email: form.Email,
- IsActivated: !setting.Service.RegisterEmailConfirm,
- }
- if err := models.AddEmailAddress(email); err != nil {
- if models.IsErrEmailAlreadyUsed(err) {
- ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
- return
- }
- ctx.ServerError("AddEmailAddress", err)
- return
- }
-
- // Send confirmation email
- if setting.Service.RegisterEmailConfirm {
- models.SendActivateEmailMail(ctx.Context, ctx.User, email)
-
- if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
- log.Error(4, "Set cache(MailResendLimit) fail: %v", err)
- }
- ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
- } else {
- ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
- }
-
- log.Trace("Email address added: %s", email.Email)
- ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-}
-
-// DeleteEmail response for delete user's email
-func DeleteEmail(ctx *context.Context) {
- if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil {
- ctx.ServerError("DeleteEmail", err)
- return
- }
- log.Trace("Email address deleted: %s", ctx.User.Name)
-
- ctx.Flash.Success(ctx.Tr("settings.email_deletion_success"))
- ctx.JSON(200, map[string]interface{}{
- "redirect": setting.AppSubURL + "/user/settings/account",
- })
-}
-
-// SettingsDelete render user suicide page and response for delete user himself
-func SettingsDelete(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsAccount"] = true
-
- if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
- if models.IsErrUserNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
- } else {
- ctx.ServerError("UserSignIn", err)
- }
- return
- }
-
- if err := models.DeleteUser(ctx.User); err != nil {
- switch {
- case models.IsErrUserOwnRepos(err):
- ctx.Flash.Error(ctx.Tr("form.still_own_repo"))
- ctx.Redirect(setting.AppSubURL + "/user/settings/account")
- case models.IsErrUserHasOrgs(err):
- ctx.Flash.Error(ctx.Tr("form.still_has_org"))
- ctx.Redirect(setting.AppSubURL + "/user/settings/account")
- default:
- ctx.ServerError("DeleteUser", err)
- }
- } else {
- log.Trace("Account deleted: %s", ctx.User.Name)
- ctx.Redirect(setting.AppSubURL + "/")
- }
-}
-
-// SettingsSecurity render change user's password page and 2FA
-func SettingsSecurity(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsSecurity"] = true
-
- enrolled := true
- _, err := models.GetTwoFactorByUID(ctx.User.ID)
- if err != nil {
- if models.IsErrTwoFactorNotEnrolled(err) {
- enrolled = false
- } else {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
- }
- ctx.Data["TwofaEnrolled"] = enrolled
-
- accountLinks, err := models.ListAccountLinks(ctx.User)
- if err != nil {
- ctx.ServerError("ListAccountLinks", err)
- return
- }
-
- // map the provider display name with the LoginSource
- sources := make(map[*models.LoginSource]string)
- for _, externalAccount := range accountLinks {
- if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
- var providerDisplayName string
- if loginSource.IsOAuth2() {
- providerTechnicalName := loginSource.OAuth2().Provider
- providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
- } else {
- providerDisplayName = loginSource.Name
- }
- sources[loginSource] = providerDisplayName
- }
- }
- ctx.Data["AccountLinks"] = sources
-
- if ctx.Query("openid.return_to") != "" {
- settingsOpenIDVerify(ctx)
- return
- }
-
- openid, err := models.GetUserOpenIDs(ctx.User.ID)
- if err != nil {
- ctx.ServerError("GetUserOpenIDs", err)
- return
- }
- ctx.Data["OpenIDs"] = openid
-
- ctx.HTML(200, tplSettingsSecurity)
-}
-
-// SettingsDeleteAccountLink delete a single account link
-func SettingsDeleteAccountLink(ctx *context.Context) {
- if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil {
- ctx.Flash.Error("RemoveAccountLink: " + err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
- }
-
- ctx.JSON(200, map[string]interface{}{
- "redirect": setting.AppSubURL + "/user/settings/security",
- })
-}
-
-// SettingsApplications render manage access token page
-func SettingsApplications(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsApplications"] = true
-
- tokens, err := models.ListAccessTokens(ctx.User.ID)
- if err != nil {
- ctx.ServerError("ListAccessTokens", err)
- return
- }
- ctx.Data["Tokens"] = tokens
-
- ctx.HTML(200, tplSettingsApplications)
-}
-
-// SettingsApplicationsPost response for add user's access token
-func SettingsApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsApplications"] = true
-
- if ctx.HasError() {
- tokens, err := models.ListAccessTokens(ctx.User.ID)
- if err != nil {
- ctx.ServerError("ListAccessTokens", err)
- return
- }
- ctx.Data["Tokens"] = tokens
- ctx.HTML(200, tplSettingsApplications)
- return
- }
-
- t := &models.AccessToken{
- UID: ctx.User.ID,
- Name: form.Name,
- }
- if err := models.NewAccessToken(t); err != nil {
- ctx.ServerError("NewAccessToken", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
- ctx.Flash.Info(t.Sha1)
-
- ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
-}
-
-// SettingsDeleteApplication response for delete user access token
-func SettingsDeleteApplication(ctx *context.Context) {
- if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
- ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
- }
-
- ctx.JSON(200, map[string]interface{}{
- "redirect": setting.AppSubURL + "/user/settings/applications",
- })
-}
-
-// SettingsKeys render user's SSH/GPG public keys page
-func SettingsKeys(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsKeys"] = true
- ctx.Data["DisableSSH"] = setting.SSH.Disabled
-
- keys, err := models.ListPublicKeys(ctx.User.ID)
- if err != nil {
- ctx.ServerError("ListPublicKeys", err)
- return
- }
- ctx.Data["Keys"] = keys
-
- gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
- if err != nil {
- ctx.ServerError("ListGPGKeys", err)
- return
- }
- ctx.Data["GPGKeys"] = gpgkeys
-
- ctx.HTML(200, tplSettingsKeys)
-}
-
-// SettingsKeysPost response for change user's SSH/GPG keys
-func SettingsKeysPost(ctx *context.Context, form auth.AddKeyForm) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsKeys"] = true
-
- keys, err := models.ListPublicKeys(ctx.User.ID)
- if err != nil {
- ctx.ServerError("ListPublicKeys", err)
- return
- }
- ctx.Data["Keys"] = keys
-
- gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
- if err != nil {
- ctx.ServerError("ListGPGKeys", err)
- return
- }
- ctx.Data["GPGKeys"] = gpgkeys
-
- if ctx.HasError() {
- ctx.HTML(200, tplSettingsKeys)
- return
- }
- switch form.Type {
- case "gpg":
- key, err := models.AddGPGKey(ctx.User.ID, form.Content)
- if err != nil {
- ctx.Data["HasGPGError"] = true
- switch {
- case models.IsErrGPGKeyParsing(err):
- ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
- ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
- case models.IsErrGPGKeyIDAlreadyUsed(err):
- ctx.Data["Err_Content"] = true
- ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
- case models.IsErrGPGNoEmailFound(err):
- ctx.Data["Err_Content"] = true
- ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
- default:
- ctx.ServerError("AddPublicKey", err)
- }
- return
- }
- ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", key.KeyID))
- ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
- case "ssh":
- content, err := models.CheckPublicKeyString(form.Content)
- if err != nil {
- if models.IsErrSSHDisabled(err) {
- ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
- } else if models.IsErrKeyUnableVerify(err) {
- ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
- } else {
- ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
- }
- ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
- return
- }
-
- if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil {
- ctx.Data["HasSSHError"] = true
- switch {
- case models.IsErrKeyAlreadyExist(err):
- ctx.Data["Err_Content"] = true
- ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
- case models.IsErrKeyNameAlreadyUsed(err):
- ctx.Data["Err_Title"] = true
- ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
- default:
- ctx.ServerError("AddPublicKey", err)
- }
- return
- }
- ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
- 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.Query("type") {
- case "gpg":
- if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil {
- ctx.Flash.Error("DeleteGPGKey: " + err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
- }
- case "ssh":
- if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
- ctx.Flash.Error("DeletePublicKey: " + err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
- }
- default:
- ctx.Flash.Warning("Function not implemented")
- ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
- }
- ctx.JSON(200, map[string]interface{}{
- "redirect": setting.AppSubURL + "/user/settings/keys",
- })
-}
-
-// SettingsTwoFactorRegenerateScratch regenerates the user's 2FA scratch code.
-func SettingsTwoFactorRegenerateScratch(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsSecurity"] = true
-
- t, err := models.GetTwoFactorByUID(ctx.User.ID)
- if err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
-
- if err = t.GenerateScratchToken(); err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
-
- if err = models.UpdateTwoFactor(t); err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken))
- ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-}
-
-// SettingsTwoFactorDisable deletes the user's 2FA settings.
-func SettingsTwoFactorDisable(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsSecurity"] = true
-
- t, err := models.GetTwoFactorByUID(ctx.User.ID)
- if err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
-
- if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil {
- ctx.ServerError("SettingsTwoFactor", 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 otpKey == nil {
- err = nil // clear the error, in case the URL was invalid
- otpKey, err = totp.Generate(totp.GenerateOpts{
- Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")",
- AccountName: ctx.User.Name,
- })
- if err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return false
- }
- }
-
- ctx.Data["TwofaSecret"] = otpKey.Secret()
- img, err := otpKey.Image(320, 240)
- if err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return false
- }
-
- var imgBytes bytes.Buffer
- if err = png.Encode(&imgBytes, img); err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return false
- }
-
- ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
- ctx.Session.Set("twofaSecret", otpKey.Secret())
- ctx.Session.Set("twofaUri", otpKey.String())
- return true
-}
-
-// SettingsTwoFactorEnroll shows the page where the user can enroll into 2FA.
-func SettingsTwoFactorEnroll(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsSecurity"] = true
-
- t, err := models.GetTwoFactorByUID(ctx.User.ID)
- if t != nil {
- // already enrolled
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
- if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
-
- if !twofaGenerateSecretAndQr(ctx) {
- return
- }
-
- ctx.HTML(200, tplSettingsTwofaEnroll)
-}
-
-// SettingsTwoFactorEnrollPost handles enrolling the user into 2FA.
-func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthForm) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsSecurity"] = true
-
- t, err := models.GetTwoFactorByUID(ctx.User.ID)
- if t != nil {
- // already enrolled
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
- if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
-
- if ctx.HasError() {
- if !twofaGenerateSecretAndQr(ctx) {
- return
- }
- ctx.HTML(200, tplSettingsTwofaEnroll)
- return
- }
-
- secret := ctx.Session.Get("twofaSecret").(string)
- if !totp.Validate(form.Passcode, secret) {
- if !twofaGenerateSecretAndQr(ctx) {
- return
- }
- ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
- ctx.HTML(200, tplSettingsTwofaEnroll)
- return
- }
-
- t = &models.TwoFactor{
- UID: ctx.User.ID,
- }
- err = t.SetSecret(secret)
- if err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
- err = t.GenerateScratchToken()
- if err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
-
- if err = models.NewTwoFactor(t); err != nil {
- ctx.ServerError("SettingsTwoFactor", err)
- return
- }
-
- ctx.Session.Delete("twofaSecret")
- ctx.Session.Delete("twofaUri")
- ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken))
- ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-}
-
-// SettingsOrganization render all the organization of the user
-func SettingsOrganization(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsOrganization"] = true
- orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned)
- if err != nil {
- ctx.ServerError("GetOrgsByUserID", err)
- return
- }
- ctx.Data["Orgs"] = orgs
- ctx.HTML(200, tplSettingsOrganization)
-}
-
-// SettingsRepos display a list of all repositories of the user
-func SettingsRepos(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsRepos"] = true
- ctxUser := ctx.User
-
- var err error
- if err = ctxUser.GetRepositories(1, setting.UI.User.RepoPagingNum); err != nil {
- ctx.ServerError("GetRepositories", err)
- return
- }
- repos := ctxUser.Repos
-
- for i := range repos {
- if repos[i].IsFork {
- err := repos[i].GetBaseRepo()
- if err != nil {
- ctx.ServerError("GetBaseRepo", err)
- return
- }
- err = repos[i].BaseRepo.GetOwner()
- if err != nil {
- ctx.ServerError("GetOwner", err)
- return
- }
- }
- }
-
- ctx.Data["Owner"] = ctxUser
- ctx.Data["Repos"] = repos
-
- ctx.HTML(200, tplSettingsRepositories)
-}
diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go
new file mode 100644
index 0000000000..966d96aeda
--- /dev/null
+++ b/routers/user/setting/account.go
@@ -0,0 +1,174 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package setting
+
+import (
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/auth"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+ tplSettingsAccount base.TplName = "user/settings/account"
+)
+
+// Account renders change user's password, user's email and user suicide page
+func Account(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsAccount"] = true
+ ctx.Data["Email"] = ctx.User.Email
+
+ emails, err := models.GetEmailAddresses(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("GetEmailAddresses", err)
+ return
+ }
+ ctx.Data["Emails"] = emails
+
+ ctx.HTML(200, tplSettingsAccount)
+}
+
+// AccountPost response for change user's password
+func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsAccount"] = true
+
+ if ctx.HasError() {
+ ctx.HTML(200, tplSettingsAccount)
+ return
+ }
+
+ if len(form.Password) < setting.MinPasswordLength {
+ ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
+ } else if ctx.User.IsPasswordSet() && !ctx.User.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 {
+ var err error
+ if ctx.User.Salt, err = models.GetUserSalt(); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+ ctx.User.HashPassword(form.Password)
+ if err := models.UpdateUserCols(ctx.User, "salt", "passwd"); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+ log.Trace("User password updated: %s", ctx.User.Name)
+ 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, form auth.AddEmailForm) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsAccount"] = true
+
+ // Make emailaddress primary.
+ if ctx.Query("_method") == "PRIMARY" {
+ if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil {
+ ctx.ServerError("MakeEmailPrimary", err)
+ return
+ }
+
+ log.Trace("Email made primary: %s", ctx.User.Name)
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+ return
+ }
+
+ // Add Email address.
+ emails, err := models.GetEmailAddresses(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("GetEmailAddresses", err)
+ return
+ }
+ ctx.Data["Emails"] = emails
+
+ if ctx.HasError() {
+ ctx.HTML(200, tplSettingsAccount)
+ return
+ }
+
+ email := &models.EmailAddress{
+ UID: ctx.User.ID,
+ Email: form.Email,
+ IsActivated: !setting.Service.RegisterEmailConfirm,
+ }
+ if err := models.AddEmailAddress(email); err != nil {
+ if models.IsErrEmailAlreadyUsed(err) {
+ ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
+ return
+ }
+ ctx.ServerError("AddEmailAddress", err)
+ return
+ }
+
+ // Send confirmation email
+ if setting.Service.RegisterEmailConfirm {
+ models.SendActivateEmailMail(ctx.Context, ctx.User, email)
+
+ if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
+ log.Error(4, "Set cache(MailResendLimit) fail: %v", err)
+ }
+ ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
+ } else {
+ ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
+ }
+
+ log.Trace("Email address added: %s", email.Email)
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+}
+
+// DeleteEmail response for delete user's email
+func DeleteEmail(ctx *context.Context) {
+ if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil {
+ ctx.ServerError("DeleteEmail", err)
+ return
+ }
+ log.Trace("Email address deleted: %s", ctx.User.Name)
+
+ ctx.Flash.Success(ctx.Tr("settings.email_deletion_success"))
+ ctx.JSON(200, map[string]interface{}{
+ "redirect": setting.AppSubURL + "/user/settings/account",
+ })
+}
+
+// DeleteAccount render user suicide page and response for delete user himself
+func DeleteAccount(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsAccount"] = true
+
+ if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
+ if models.IsErrUserNotExist(err) {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
+ } else {
+ ctx.ServerError("UserSignIn", err)
+ }
+ return
+ }
+
+ if err := models.DeleteUser(ctx.User); err != nil {
+ switch {
+ case models.IsErrUserOwnRepos(err):
+ ctx.Flash.Error(ctx.Tr("form.still_own_repo"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+ case models.IsErrUserHasOrgs(err):
+ ctx.Flash.Error(ctx.Tr("form.still_has_org"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+ default:
+ ctx.ServerError("DeleteUser", err)
+ }
+ } else {
+ log.Trace("Account deleted: %s", ctx.User.Name)
+ ctx.Redirect(setting.AppSubURL + "/")
+ }
+}
diff --git a/routers/user/setting_test.go b/routers/user/setting/account_test.go
similarity index 91%
rename from routers/user/setting_test.go
rename to routers/user/setting/account_test.go
index 6aa9a07439..59fbda1569 100644
--- a/routers/user/setting_test.go
+++ b/routers/user/setting/account_test.go
@@ -1,8 +1,8 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
-package user
+package setting
import (
"net/http"
@@ -56,7 +56,7 @@ func TestChangePassword(t *testing.T) {
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
- SettingsAccountPost(ctx, auth.ChangePasswordForm{
+ AccountPost(ctx, auth.ChangePasswordForm{
OldPassword: req.OldPassword,
Password: req.NewPassword,
Retype: req.Retype,
diff --git a/routers/user/setting/applications.go b/routers/user/setting/applications.go
new file mode 100644
index 0000000000..f292b65d70
--- /dev/null
+++ b/routers/user/setting/applications.go
@@ -0,0 +1,77 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package setting
+
+import (
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/auth"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+ tplSettingsApplications base.TplName = "user/settings/applications"
+)
+
+// Applications render manage access token page
+func Applications(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsApplications"] = true
+
+ tokens, err := models.ListAccessTokens(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("ListAccessTokens", err)
+ return
+ }
+ ctx.Data["Tokens"] = tokens
+
+ ctx.HTML(200, tplSettingsApplications)
+}
+
+// ApplicationsPost response for add user's access token
+func ApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsApplications"] = true
+
+ if ctx.HasError() {
+ tokens, err := models.ListAccessTokens(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("ListAccessTokens", err)
+ return
+ }
+ ctx.Data["Tokens"] = tokens
+ ctx.HTML(200, tplSettingsApplications)
+ return
+ }
+
+ t := &models.AccessToken{
+ UID: ctx.User.ID,
+ Name: form.Name,
+ }
+ if err := models.NewAccessToken(t); err != nil {
+ ctx.ServerError("NewAccessToken", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
+ ctx.Flash.Info(t.Sha1)
+
+ ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
+}
+
+// DeleteApplication response for delete user access token
+func DeleteApplication(ctx *context.Context) {
+ if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
+ ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
+ }
+
+ ctx.JSON(200, map[string]interface{}{
+ "redirect": setting.AppSubURL + "/user/settings/applications",
+ })
+}
diff --git a/routers/user/setting/keys.go b/routers/user/setting/keys.go
new file mode 100644
index 0000000000..5c28fa6e6d
--- /dev/null
+++ b/routers/user/setting/keys.go
@@ -0,0 +1,149 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package setting
+
+import (
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/auth"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+ tplSettingsKeys base.TplName = "user/settings/keys"
+)
+
+// Keys render user's SSH/GPG public keys page
+func Keys(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsKeys"] = true
+ ctx.Data["DisableSSH"] = setting.SSH.Disabled
+
+ keys, err := models.ListPublicKeys(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("ListPublicKeys", err)
+ return
+ }
+ ctx.Data["Keys"] = keys
+
+ gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("ListGPGKeys", err)
+ return
+ }
+ ctx.Data["GPGKeys"] = gpgkeys
+
+ ctx.HTML(200, tplSettingsKeys)
+}
+
+// KeysPost response for change user's SSH/GPG keys
+func KeysPost(ctx *context.Context, form auth.AddKeyForm) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsKeys"] = true
+
+ keys, err := models.ListPublicKeys(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("ListPublicKeys", err)
+ return
+ }
+ ctx.Data["Keys"] = keys
+
+ gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("ListGPGKeys", err)
+ return
+ }
+ ctx.Data["GPGKeys"] = gpgkeys
+
+ if ctx.HasError() {
+ ctx.HTML(200, tplSettingsKeys)
+ return
+ }
+ switch form.Type {
+ case "gpg":
+ key, err := models.AddGPGKey(ctx.User.ID, form.Content)
+ if err != nil {
+ ctx.Data["HasGPGError"] = true
+ switch {
+ case models.IsErrGPGKeyParsing(err):
+ ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+ case models.IsErrGPGKeyIDAlreadyUsed(err):
+ ctx.Data["Err_Content"] = true
+ ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
+ case models.IsErrGPGNoEmailFound(err):
+ ctx.Data["Err_Content"] = true
+ ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
+ default:
+ ctx.ServerError("AddPublicKey", err)
+ }
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", key.KeyID))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+ case "ssh":
+ content, err := models.CheckPublicKeyString(form.Content)
+ if err != nil {
+ if models.IsErrSSHDisabled(err) {
+ ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
+ } else if models.IsErrKeyUnableVerify(err) {
+ ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
+ } else {
+ ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
+ }
+ ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+ return
+ }
+
+ if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil {
+ ctx.Data["HasSSHError"] = true
+ switch {
+ case models.IsErrKeyAlreadyExist(err):
+ ctx.Data["Err_Content"] = true
+ ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
+ case models.IsErrKeyNameAlreadyUsed(err):
+ ctx.Data["Err_Title"] = true
+ ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
+ default:
+ ctx.ServerError("AddPublicKey", err)
+ }
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
+ 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.Query("type") {
+ case "gpg":
+ if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil {
+ ctx.Flash.Error("DeleteGPGKey: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
+ }
+ case "ssh":
+ if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
+ ctx.Flash.Error("DeletePublicKey: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
+ }
+ default:
+ ctx.Flash.Warning("Function not implemented")
+ ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+ }
+ ctx.JSON(200, map[string]interface{}{
+ "redirect": setting.AppSubURL + "/user/settings/keys",
+ })
+}
diff --git a/routers/user/setting/main_test.go b/routers/user/setting/main_test.go
new file mode 100644
index 0000000000..d343c02f48
--- /dev/null
+++ b/routers/user/setting/main_test.go
@@ -0,0 +1,16 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package setting
+
+import (
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+)
+
+func TestMain(m *testing.M) {
+ models.MainTest(m, filepath.Join("..", "..", ".."))
+}
diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go
new file mode 100644
index 0000000000..2ca64ad2e5
--- /dev/null
+++ b/routers/user/setting/profile.go
@@ -0,0 +1,220 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package setting
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/auth"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/Unknwon/com"
+ "github.com/Unknwon/i18n"
+)
+
+const (
+ tplSettingsProfile base.TplName = "user/settings/profile"
+ tplSettingsOrganization base.TplName = "user/settings/organization"
+ tplSettingsRepositories base.TplName = "user/settings/repos"
+)
+
+// Profile render user's profile page
+func Profile(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsProfile"] = true
+ ctx.HTML(200, tplSettingsProfile)
+}
+
+func handleUsernameChange(ctx *context.Context, newName string) {
+ // Non-local users are not allowed to change their username.
+ if len(newName) == 0 || !ctx.User.IsLocal() {
+ return
+ }
+
+ // Check if user name has been changed
+ if ctx.User.LowerName != strings.ToLower(newName) {
+ if err := models.ChangeUserName(ctx.User, newName); err != nil {
+ switch {
+ case models.IsErrUserAlreadyExist(err):
+ ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings")
+ case models.IsErrEmailAlreadyUsed(err):
+ ctx.Flash.Error(ctx.Tr("form.email_been_used"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings")
+ case models.IsErrNameReserved(err):
+ ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
+ ctx.Redirect(setting.AppSubURL + "/user/settings")
+ case models.IsErrNamePatternNotAllowed(err):
+ ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
+ ctx.Redirect(setting.AppSubURL + "/user/settings")
+ default:
+ ctx.ServerError("ChangeUserName", err)
+ }
+ return
+ }
+ log.Trace("User name changed: %s -> %s", ctx.User.Name, newName)
+ }
+
+ // In case it's just a case change
+ ctx.User.Name = newName
+ ctx.User.LowerName = strings.ToLower(newName)
+}
+
+// ProfilePost response for change user's profile
+func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsProfile"] = true
+
+ if ctx.HasError() {
+ ctx.HTML(200, tplSettingsProfile)
+ return
+ }
+
+ handleUsernameChange(ctx, form.Name)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.User.FullName = form.FullName
+ ctx.User.Email = form.Email
+ ctx.User.KeepEmailPrivate = form.KeepEmailPrivate
+ ctx.User.Website = form.Website
+ ctx.User.Location = form.Location
+ ctx.User.Language = form.Language
+ if err := models.UpdateUserSetting(ctx.User); err != nil {
+ if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
+ ctx.Flash.Error(ctx.Tr("form.email_been_used"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings")
+ return
+ }
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+
+ // Update the language to the one we just set
+ ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL)
+
+ log.Trace("User settings updated: %s", ctx.User.Name)
+ ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings")
+}
+
+// UpdateAvatarSetting update user's avatar
+// FIXME: limit size.
+func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *models.User) error {
+ ctxUser.UseCustomAvatar = form.Source == auth.AvatarLocal
+ if len(form.Gravatar) > 0 {
+ ctxUser.Avatar = base.EncodeMD5(form.Gravatar)
+ ctxUser.AvatarEmail = form.Gravatar
+ }
+
+ if form.Avatar != nil {
+ fr, err := form.Avatar.Open()
+ if err != nil {
+ return fmt.Errorf("Avatar.Open: %v", err)
+ }
+ defer fr.Close()
+
+ data, err := ioutil.ReadAll(fr)
+ if err != nil {
+ return fmt.Errorf("ioutil.ReadAll: %v", err)
+ }
+ if !base.IsImageFile(data) {
+ return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+ }
+ if err = ctxUser.UploadAvatar(data); err != nil {
+ return fmt.Errorf("UploadAvatar: %v", err)
+ }
+ } else {
+ // No avatar is uploaded but setting has been changed to enable,
+ // generate a random one when needed.
+ if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) {
+ if err := ctxUser.GenerateRandomAvatar(); err != nil {
+ log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err)
+ }
+ }
+ }
+
+ if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
+ return fmt.Errorf("UpdateUser: %v", err)
+ }
+
+ return nil
+}
+
+// AvatarPost response for change user's avatar request
+func AvatarPost(ctx *context.Context, form auth.AvatarForm) {
+ if err := UpdateAvatarSetting(ctx, form, ctx.User); 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 := ctx.User.DeleteAvatar(); err != nil {
+ ctx.Flash.Error(err.Error())
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/user/settings")
+}
+
+// Organization render all the organization of the user
+func Organization(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsOrganization"] = true
+ orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned)
+ if err != nil {
+ ctx.ServerError("GetOrgsByUserID", err)
+ return
+ }
+ ctx.Data["Orgs"] = orgs
+ ctx.HTML(200, tplSettingsOrganization)
+}
+
+// Repos display a list of all repositories of the user
+func Repos(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsRepos"] = true
+ ctxUser := ctx.User
+
+ var err error
+ if err = ctxUser.GetRepositories(1, setting.UI.User.RepoPagingNum); err != nil {
+ ctx.ServerError("GetRepositories", err)
+ return
+ }
+ repos := ctxUser.Repos
+
+ for i := range repos {
+ if repos[i].IsFork {
+ err := repos[i].GetBaseRepo()
+ if err != nil {
+ ctx.ServerError("GetBaseRepo", err)
+ return
+ }
+ err = repos[i].BaseRepo.GetOwner()
+ if err != nil {
+ ctx.ServerError("GetOwner", err)
+ return
+ }
+ }
+ }
+
+ ctx.Data["Owner"] = ctxUser
+ ctx.Data["Repos"] = repos
+
+ ctx.HTML(200, tplSettingsRepositories)
+}
diff --git a/routers/user/setting/security.go b/routers/user/setting/security.go
new file mode 100644
index 0000000000..5346f349ff
--- /dev/null
+++ b/routers/user/setting/security.go
@@ -0,0 +1,92 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package setting
+
+import (
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+ tplSettingsSecurity base.TplName = "user/settings/security"
+ tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll"
+)
+
+// Security render change user's password page and 2FA
+func Security(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsSecurity"] = true
+
+ enrolled := true
+ _, err := models.GetTwoFactorByUID(ctx.User.ID)
+ if err != nil {
+ if models.IsErrTwoFactorNotEnrolled(err) {
+ enrolled = false
+ } else {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+ }
+ ctx.Data["TwofaEnrolled"] = enrolled
+
+ tokens, err := models.ListAccessTokens(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("ListAccessTokens", err)
+ return
+ }
+ ctx.Data["Tokens"] = tokens
+
+ accountLinks, err := models.ListAccountLinks(ctx.User)
+ if err != nil {
+ ctx.ServerError("ListAccountLinks", err)
+ return
+ }
+
+ // map the provider display name with the LoginSource
+ sources := make(map[*models.LoginSource]string)
+ for _, externalAccount := range accountLinks {
+ if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
+ var providerDisplayName string
+ if loginSource.IsOAuth2() {
+ providerTechnicalName := loginSource.OAuth2().Provider
+ providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
+ } else {
+ providerDisplayName = loginSource.Name
+ }
+ sources[loginSource] = providerDisplayName
+ }
+ }
+ ctx.Data["AccountLinks"] = sources
+
+ if ctx.Query("openid.return_to") != "" {
+ settingsOpenIDVerify(ctx)
+ return
+ }
+
+ openid, err := models.GetUserOpenIDs(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("GetUserOpenIDs", err)
+ return
+ }
+ ctx.Data["OpenIDs"] = openid
+
+ ctx.HTML(200, tplSettingsSecurity)
+}
+
+// DeleteAccountLink delete a single account link
+func DeleteAccountLink(ctx *context.Context) {
+ if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil {
+ ctx.Flash.Error("RemoveAccountLink: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
+ }
+
+ ctx.JSON(200, map[string]interface{}{
+ "redirect": setting.AppSubURL + "/user/settings/security",
+ })
+}
diff --git a/routers/user/setting_openid.go b/routers/user/setting/security_openid.go
similarity index 94%
rename from routers/user/setting_openid.go
rename to routers/user/setting/security_openid.go
index 7716466120..c98dc2cda9 100644
--- a/routers/user/setting_openid.go
+++ b/routers/user/setting/security_openid.go
@@ -1,8 +1,8 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
-package user
+package setting
import (
"code.gitea.io/gitea/models"
@@ -13,8 +13,8 @@ import (
"code.gitea.io/gitea/modules/setting"
)
-// SettingsOpenIDPost response for change user's openid
-func SettingsOpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) {
+// OpenIDPost response for change user's openid
+func OpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
diff --git a/routers/user/setting/security_twofa.go b/routers/user/setting/security_twofa.go
new file mode 100644
index 0000000000..55101ed1a4
--- /dev/null
+++ b/routers/user/setting/security_twofa.go
@@ -0,0 +1,187 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package setting
+
+import (
+ "bytes"
+ "encoding/base64"
+ "html/template"
+ "image/png"
+ "strings"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/auth"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/pquerna/otp"
+ "github.com/pquerna/otp/totp"
+)
+
+// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
+func RegenerateScratchTwoFactor(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsSecurity"] = true
+
+ t, err := models.GetTwoFactorByUID(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+
+ if err = t.GenerateScratchToken(); err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+
+ if err = models.UpdateTwoFactor(t); err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+}
+
+// DisableTwoFactor deletes the user's 2FA settings.
+func DisableTwoFactor(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsSecurity"] = true
+
+ t, err := models.GetTwoFactorByUID(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+
+ if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil {
+ ctx.ServerError("SettingsTwoFactor", 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 otpKey == nil {
+ err = nil // clear the error, in case the URL was invalid
+ otpKey, err = totp.Generate(totp.GenerateOpts{
+ Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")",
+ AccountName: ctx.User.Name,
+ })
+ if err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return false
+ }
+ }
+
+ ctx.Data["TwofaSecret"] = otpKey.Secret()
+ img, err := otpKey.Image(320, 240)
+ if err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return false
+ }
+
+ var imgBytes bytes.Buffer
+ if err = png.Encode(&imgBytes, img); err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return false
+ }
+
+ ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
+ ctx.Session.Set("twofaSecret", otpKey.Secret())
+ ctx.Session.Set("twofaUri", otpKey.String())
+ return true
+}
+
+// EnrollTwoFactor shows the page where the user can enroll into 2FA.
+func EnrollTwoFactor(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsSecurity"] = true
+
+ t, err := models.GetTwoFactorByUID(ctx.User.ID)
+ if t != nil {
+ // already enrolled
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+ if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+
+ if !twofaGenerateSecretAndQr(ctx) {
+ return
+ }
+
+ ctx.HTML(200, tplSettingsTwofaEnroll)
+}
+
+// EnrollTwoFactorPost handles enrolling the user into 2FA.
+func EnrollTwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsSecurity"] = true
+
+ t, err := models.GetTwoFactorByUID(ctx.User.ID)
+ if t != nil {
+ // already enrolled
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+ if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+
+ if ctx.HasError() {
+ if !twofaGenerateSecretAndQr(ctx) {
+ return
+ }
+ ctx.HTML(200, tplSettingsTwofaEnroll)
+ return
+ }
+
+ secret := ctx.Session.Get("twofaSecret").(string)
+ if !totp.Validate(form.Passcode, secret) {
+ if !twofaGenerateSecretAndQr(ctx) {
+ return
+ }
+ ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
+ ctx.HTML(200, tplSettingsTwofaEnroll)
+ return
+ }
+
+ t = &models.TwoFactor{
+ UID: ctx.User.ID,
+ }
+ err = t.SetSecret(secret)
+ if err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+ err = t.GenerateScratchToken()
+ if err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+
+ if err = models.NewTwoFactor(t); err != nil {
+ ctx.ServerError("SettingsTwoFactor", err)
+ return
+ }
+
+ ctx.Session.Delete("twofaSecret")
+ ctx.Session.Delete("twofaUri")
+ ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+}
diff --git a/templates/repo/settings/hooks.tmpl b/templates/repo/settings/webhook/base.tmpl
similarity index 82%
rename from templates/repo/settings/hooks.tmpl
rename to templates/repo/settings/webhook/base.tmpl
index 34c5cfbb6c..6f486131f7 100644
--- a/templates/repo/settings/hooks.tmpl
+++ b/templates/repo/settings/webhook/base.tmpl
@@ -3,7 +3,7 @@
{{template "repo/header" .}}
{{template "repo/settings/navbar" .}}
- {{template "repo/settings/hook_list" .}}
+ {{template "repo/settings/webhook/list" .}}
{{template "base/footer" .}}
diff --git a/templates/repo/settings/hook_delete_modal.tmpl b/templates/repo/settings/webhook/delete_modal.tmpl
similarity index 100%
rename from templates/repo/settings/hook_delete_modal.tmpl
rename to templates/repo/settings/webhook/delete_modal.tmpl
diff --git a/templates/repo/settings/hook_dingtalk.tmpl b/templates/repo/settings/webhook/dingtalk.tmpl
similarity index 91%
rename from templates/repo/settings/hook_dingtalk.tmpl
rename to templates/repo/settings/webhook/dingtalk.tmpl
index 37271a7db5..3e6504f651 100644
--- a/templates/repo/settings/hook_dingtalk.tmpl
+++ b/templates/repo/settings/webhook/dingtalk.tmpl
@@ -6,6 +6,6 @@
- {{template "repo/settings/hook_settings" .}}
+ {{template "repo/settings/webhook/settings" .}}
{{end}}
diff --git a/templates/repo/settings/hook_discord.tmpl b/templates/repo/settings/webhook/discord.tmpl
similarity index 95%
rename from templates/repo/settings/hook_discord.tmpl
rename to templates/repo/settings/webhook/discord.tmpl
index 901e7e6311..75c31efb51 100644
--- a/templates/repo/settings/hook_discord.tmpl
+++ b/templates/repo/settings/webhook/discord.tmpl
@@ -14,6 +14,6 @@
- {{template "repo/settings/hook_settings" .}}
+ {{template "repo/settings/webhook/settings" .}}
{{end}}
diff --git a/templates/repo/settings/hook_gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl
similarity index 94%
rename from templates/repo/settings/hook_gitea.tmpl
rename to templates/repo/settings/webhook/gitea.tmpl
index fc5e35e068..87a8813d0e 100644
--- a/templates/repo/settings/hook_gitea.tmpl
+++ b/templates/repo/settings/webhook/gitea.tmpl
@@ -23,6 +23,6 @@
- {{template "repo/settings/hook_settings" .}}
+ {{template "repo/settings/webhook/settings" .}}
{{end}}
diff --git a/templates/repo/settings/hook_gogs.tmpl b/templates/repo/settings/webhook/gogs.tmpl
similarity index 96%
rename from templates/repo/settings/hook_gogs.tmpl
rename to templates/repo/settings/webhook/gogs.tmpl
index 28098d14ec..649fb54aea 100644
--- a/templates/repo/settings/hook_gogs.tmpl
+++ b/templates/repo/settings/webhook/gogs.tmpl
@@ -23,6 +23,6 @@
- {{template "repo/settings/hook_settings" .}}
+ {{template "repo/settings/webhook/settings" .}}
{{end}}
diff --git a/templates/repo/settings/hook_history.tmpl b/templates/repo/settings/webhook/history.tmpl
similarity index 100%
rename from templates/repo/settings/hook_history.tmpl
rename to templates/repo/settings/webhook/history.tmpl
diff --git a/templates/repo/settings/hook_list.tmpl b/templates/repo/settings/webhook/list.tmpl
similarity index 97%
rename from templates/repo/settings/hook_list.tmpl
rename to templates/repo/settings/webhook/list.tmpl
index 4e61ba7a07..de6bd2c5f2 100644
--- a/templates/repo/settings/hook_list.tmpl
+++ b/templates/repo/settings/webhook/list.tmpl
@@ -48,4 +48,4 @@
-{{template "repo/settings/hook_delete_modal" .}}
+{{template "repo/settings/webhook/delete_modal" .}}
diff --git a/templates/repo/settings/hook_new.tmpl b/templates/repo/settings/webhook/new.tmpl
similarity index 77%
rename from templates/repo/settings/hook_new.tmpl
rename to templates/repo/settings/webhook/new.tmpl
index 7e3cf3c8cf..1b3d114577 100644
--- a/templates/repo/settings/hook_new.tmpl
+++ b/templates/repo/settings/webhook/new.tmpl
@@ -21,14 +21,14 @@
- {{template "repo/settings/hook_gitea" .}}
- {{template "repo/settings/hook_gogs" .}}
- {{template "repo/settings/hook_slack" .}}
- {{template "repo/settings/hook_discord" .}}
- {{template "repo/settings/hook_dingtalk" .}}
+ {{template "repo/settings/webhook/gitea" .}}
+ {{template "repo/settings/webhook/gogs" .}}
+ {{template "repo/settings/webhook/slack" .}}
+ {{template "repo/settings/webhook/discord" .}}
+ {{template "repo/settings/webhook/dingtalk" .}}
- {{template "repo/settings/hook_history" .}}
+ {{template "repo/settings/webhook/history" .}}
{{template "base/footer" .}}
diff --git a/templates/repo/settings/hook_settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
similarity index 63%
rename from templates/repo/settings/hook_settings.tmpl
rename to templates/repo/settings/webhook/settings.tmpl
index 7f3406588f..f04c25a0a3 100644
--- a/templates/repo/settings/hook_settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -32,6 +32,26 @@
+
+
+
+
+
+
+ {{.i18n.Tr "repo.settings.event_delete_desc"}}
+
+
+
+
+
+
+
+
+
+ {{.i18n.Tr "repo.settings.event_fork_desc"}}
+
+
+
+
+
+
+
+
+
+ {{.i18n.Tr "repo.settings.event_issues_desc"}}
+
+
+
+
+
+
+
+
+
+ {{.i18n.Tr "repo.settings.event_issue_comment_desc"}}
+
+
+
+
+
+
+
+
+
+ {{.i18n.Tr "repo.settings.event_release_desc"}}
+
+
+
@@ -83,4 +133,4 @@
{{end}}
-{{template "repo/settings/hook_delete_modal" .}}
+{{template "repo/settings/webhook/delete_modal" .}}
diff --git a/templates/repo/settings/hook_slack.tmpl b/templates/repo/settings/webhook/slack.tmpl
similarity index 96%
rename from templates/repo/settings/hook_slack.tmpl
rename to templates/repo/settings/webhook/slack.tmpl
index 16e1859470..c35a679da7 100644
--- a/templates/repo/settings/hook_slack.tmpl
+++ b/templates/repo/settings/webhook/slack.tmpl
@@ -23,6 +23,6 @@
- {{template "repo/settings/hook_settings" .}}
+ {{template "repo/settings/webhook/settings" .}}
{{end}}
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index f1a3e48115..d842644185 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -2,6 +2,7 @@
{{template "user/settings/navbar" .}}
+ {{template "base/alert" .}}
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index e5bb2df011..6a654fda70 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -43,7 +43,7 @@
-
+
{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}