mirror of
https://github.com/go-gitea/gitea
synced 2024-12-22 15:37:54 +01:00
Merge branch 'master' into feat/approval-new
This commit is contained in:
commit
0f64dad74b
58
cmd/admin.go
58
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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -537,7 +537,7 @@ _Symbols used in table:_
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Webhook support</td>
|
||||
<td>⁄</td>
|
||||
<td>✓</td>
|
||||
<td>✓</td>
|
||||
<td>✓</td>
|
||||
<td>✓</td>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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=Новий лист для підтвердження було відправлено на <b>%s</b>, будь ласка, перевірте вашу поштову скриньку протягом %s для завершення реєстрації.
|
||||
reset_password_mail_sent_prompt=Лист для підтвердження було відправлено на <b>%s</b>. Будь ласка, перевірте вашу поштову скриньку протягом %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=Режим журналювання
|
||||
|
@ -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=<strong>需要帮助?</strong> 请查看有关 <a href="%s">如何生成 SSH 密钥</a> 或 <a href="%s">常见 SSH 问题</a> 寻找答案。
|
||||
gpg_helper=<strong>需要帮助吗?</strong>看一看 GitHub <a href="%s">关于GPG</a> 的指导。
|
||||
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=你的账户<strong>已启用</strong>了两步验证。
|
||||
twofa_desc=两步验证可以加强你的账号安全性。
|
||||
twofa_is_enrolled=你的账号<strong>已启用</strong>了两步验证。
|
||||
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=组织名称
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
174
routers/user/setting/account.go
Normal file
174
routers/user/setting/account.go
Normal file
@ -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 + "/")
|
||||
}
|
||||
}
|
@ -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,
|
77
routers/user/setting/applications.go
Normal file
77
routers/user/setting/applications.go
Normal file
@ -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",
|
||||
})
|
||||
}
|
149
routers/user/setting/keys.go
Normal file
149
routers/user/setting/keys.go
Normal file
@ -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",
|
||||
})
|
||||
}
|
16
routers/user/setting/main_test.go
Normal file
16
routers/user/setting/main_test.go
Normal file
@ -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("..", "..", ".."))
|
||||
}
|
220
routers/user/setting/profile.go
Normal file
220
routers/user/setting/profile.go
Normal file
@ -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)
|
||||
}
|
92
routers/user/setting/security.go
Normal file
92
routers/user/setting/security.go
Normal file
@ -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",
|
||||
})
|
||||
}
|
@ -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
|
||||
|
187
routers/user/setting/security_twofa.go
Normal file
187
routers/user/setting/security_twofa.go
Normal file
@ -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")
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
{{template "repo/header" .}}
|
||||
{{template "repo/settings/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "repo/settings/hook_list" .}}
|
||||
{{template "repo/settings/webhook/list" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
@ -6,6 +6,6 @@
|
||||
<label for="payload_url">{{.i18n.Tr "repo.settings.payload_url"}}</label>
|
||||
<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
|
||||
</div>
|
||||
{{template "repo/settings/hook_settings" .}}
|
||||
{{template "repo/settings/webhook/settings" .}}
|
||||
</form>
|
||||
{{end}}
|
@ -14,6 +14,6 @@
|
||||
<label for="icon_url">{{.i18n.Tr "repo.settings.discord_icon_url"}}</label>
|
||||
<input id="icon_url" name="icon_url" value="{{.DiscordHook.IconURL}}" placeholder="e.g. https://example.com/img/favicon.png">
|
||||
</div>
|
||||
{{template "repo/settings/hook_settings" .}}
|
||||
{{template "repo/settings/webhook/settings" .}}
|
||||
</form>
|
||||
{{end}}
|
@ -23,6 +23,6 @@
|
||||
<label for="secret">{{.i18n.Tr "repo.settings.secret"}}</label>
|
||||
<input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off">
|
||||
</div>
|
||||
{{template "repo/settings/hook_settings" .}}
|
||||
{{template "repo/settings/webhook/settings" .}}
|
||||
</form>
|
||||
{{end}}
|
@ -23,6 +23,6 @@
|
||||
<label for="secret">{{.i18n.Tr "repo.settings.secret"}}</label>
|
||||
<input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off">
|
||||
</div>
|
||||
{{template "repo/settings/hook_settings" .}}
|
||||
{{template "repo/settings/webhook/settings" .}}
|
||||
</form>
|
||||
{{end}}
|
@ -48,4 +48,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "repo/settings/hook_delete_modal" .}}
|
||||
{{template "repo/settings/webhook/delete_modal" .}}
|
@ -21,14 +21,14 @@
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{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" .}}
|
||||
</div>
|
||||
|
||||
{{template "repo/settings/hook_history" .}}
|
||||
{{template "repo/settings/webhook/history" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
@ -32,6 +32,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete -->
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input class="hidden" name="delete" type="checkbox" tabindex="0" {{if .Webhook.Delete}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.event_delete"}}</label>
|
||||
<span class="help">{{.i18n.Tr "repo.settings.event_delete_desc"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Fork -->
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input class="hidden" name="fork" type="checkbox" tabindex="0" {{if .Webhook.Fork}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.event_fork"}}</label>
|
||||
<span class="help">{{.i18n.Tr "repo.settings.event_fork_desc"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Push -->
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
@ -42,6 +62,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Issues -->
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input class="hidden" name="issues" type="checkbox" tabindex="0" {{if .Webhook.Issues}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.event_issues"}}</label>
|
||||
<span class="help">{{.i18n.Tr "repo.settings.event_issues_desc"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Issue Comment -->
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input class="hidden" name="issue_comment" type="checkbox" tabindex="0" {{if .Webhook.IssueComment}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.event_issue_comment"}}</label>
|
||||
<span class="help">{{.i18n.Tr "repo.settings.event_issue_comment_desc"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pull Request -->
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
@ -62,6 +102,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Release -->
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input class="hidden" name="release" type="checkbox" tabindex="0" {{if .Webhook.Release}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.event_release"}}</label>
|
||||
<span class="help">{{.i18n.Tr "repo.settings.event_release_desc"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -83,4 +133,4 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "repo/settings/hook_delete_modal" .}}
|
||||
{{template "repo/settings/webhook/delete_modal" .}}
|
@ -23,6 +23,6 @@
|
||||
<label for="color">{{.i18n.Tr "repo.settings.slack_color"}}</label>
|
||||
<input id="color" name="color" value="{{.SlackHook.Color}}" placeholder="e.g. #dd4b39, good, warning, danger">
|
||||
</div>
|
||||
{{template "repo/settings/hook_settings" .}}
|
||||
{{template "repo/settings/webhook/settings" .}}
|
||||
</form>
|
||||
{{end}}
|
@ -2,6 +2,7 @@
|
||||
<div class="user settings applications">
|
||||
{{template "user/settings/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.manage_access_token"}}
|
||||
</h4>
|
||||
|
@ -43,7 +43,7 @@
|
||||
<div class="field">
|
||||
<label for="language">{{.i18n.Tr "settings.language"}}</label>
|
||||
<div class="ui language selection dropdown" id="language">
|
||||
<input name="language" type="hidden">
|
||||
<input name="language" type="hidden" value="{{.SignedUser.Language}}">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="text">{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}</div>
|
||||
<div class="menu">
|
||||
|
122
vendor/code.gitea.io/sdk/gitea/hook.go
generated
vendored
122
vendor/code.gitea.io/sdk/gitea/hook.go
generated
vendored
@ -172,9 +172,14 @@ type PayloadCommitVerification struct {
|
||||
|
||||
var (
|
||||
_ Payloader = &CreatePayload{}
|
||||
_ Payloader = &DeletePayload{}
|
||||
_ Payloader = &ForkPayload{}
|
||||
_ Payloader = &PushPayload{}
|
||||
_ Payloader = &IssuePayload{}
|
||||
_ Payloader = &IssueCommentPayload{}
|
||||
_ Payloader = &PullRequestPayload{}
|
||||
_ Payloader = &RepositoryPayload{}
|
||||
_ Payloader = &ReleasePayload{}
|
||||
)
|
||||
|
||||
// _________ __
|
||||
@ -224,6 +229,123 @@ func ParseCreateHook(raw []byte) (*CreatePayload, error) {
|
||||
return hook, nil
|
||||
}
|
||||
|
||||
// ________ .__ __
|
||||
// \______ \ ____ | | _____/ |_ ____
|
||||
// | | \_/ __ \| | _/ __ \ __\/ __ \
|
||||
// | ` \ ___/| |_\ ___/| | \ ___/
|
||||
// /_______ /\___ >____/\___ >__| \___ >
|
||||
// \/ \/ \/ \/
|
||||
|
||||
// PusherType define the type to push
|
||||
type PusherType string
|
||||
|
||||
// describe all the PusherTypes
|
||||
const (
|
||||
PusherTypeUser PusherType = "user"
|
||||
)
|
||||
|
||||
// DeletePayload represents delete payload
|
||||
type DeletePayload struct {
|
||||
Ref string `json:"ref"`
|
||||
RefType string `json:"ref_type"`
|
||||
PusherType PusherType `json:"pusher_type"`
|
||||
Repo *Repository `json:"repository"`
|
||||
Sender *User `json:"sender"`
|
||||
}
|
||||
|
||||
// SetSecret implements Payload
|
||||
func (p *DeletePayload) SetSecret(secret string) {
|
||||
}
|
||||
|
||||
// JSONPayload implements Payload
|
||||
func (p *DeletePayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// ___________ __
|
||||
// \_ _____/__________| | __
|
||||
// | __)/ _ \_ __ \ |/ /
|
||||
// | \( <_> ) | \/ <
|
||||
// \___ / \____/|__| |__|_ \
|
||||
// \/ \/
|
||||
|
||||
// ForkPayload represents fork payload
|
||||
type ForkPayload struct {
|
||||
Forkee *Repository `json:"forkee"`
|
||||
Repo *Repository `json:"repository"`
|
||||
Sender *User `json:"sender"`
|
||||
}
|
||||
|
||||
// SetSecret implements Payload
|
||||
func (p *ForkPayload) SetSecret(secret string) {
|
||||
}
|
||||
|
||||
// JSONPayload implements Payload
|
||||
func (p *ForkPayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// HookIssueCommentAction defines hook issue comment action
|
||||
type HookIssueCommentAction string
|
||||
|
||||
// all issue comment actions
|
||||
const (
|
||||
HookIssueCommentCreated HookIssueCommentAction = "created"
|
||||
HookIssueCommentEdited HookIssueCommentAction = "edited"
|
||||
HookIssueCommentDeleted HookIssueCommentAction = "deleted"
|
||||
)
|
||||
|
||||
// IssueCommentPayload represents a payload information of issue comment event.
|
||||
type IssueCommentPayload struct {
|
||||
Action HookIssueCommentAction `json:"action"`
|
||||
Issue *Issue `json:"issue"`
|
||||
Comment *Comment `json:"comment"`
|
||||
Changes *ChangesPayload `json:"changes,omitempty"`
|
||||
Repository *Repository `json:"repository"`
|
||||
Sender *User `json:"sender"`
|
||||
}
|
||||
|
||||
// SetSecret implements Payload
|
||||
func (p *IssueCommentPayload) SetSecret(secret string) {
|
||||
}
|
||||
|
||||
// JSONPayload implements Payload
|
||||
func (p *IssueCommentPayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// __________ .__
|
||||
// \______ \ ____ | | ____ _____ ______ ____
|
||||
// | _// __ \| | _/ __ \\__ \ / ___// __ \
|
||||
// | | \ ___/| |_\ ___/ / __ \_\___ \\ ___/
|
||||
// |____|_ /\___ >____/\___ >____ /____ >\___ >
|
||||
// \/ \/ \/ \/ \/ \/
|
||||
|
||||
// HookReleaseAction defines hook release action type
|
||||
type HookReleaseAction string
|
||||
|
||||
// all release actions
|
||||
const (
|
||||
HookReleasePublished HookReleaseAction = "published"
|
||||
)
|
||||
|
||||
// ReleasePayload represents a payload information of release event.
|
||||
type ReleasePayload struct {
|
||||
Action HookReleaseAction `json:"action"`
|
||||
Release *Release `json:"release"`
|
||||
Repository *Repository `json:"repository"`
|
||||
Sender *User `json:"sender"`
|
||||
}
|
||||
|
||||
// SetSecret implements Payload
|
||||
func (p *ReleasePayload) SetSecret(secret string) {
|
||||
}
|
||||
|
||||
// JSONPayload implements Payload
|
||||
func (p *ReleasePayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// __________ .__
|
||||
// \______ \__ __ _____| |__
|
||||
// | ___/ | \/ ___/ | \
|
||||
|
28
vendor/code.gitea.io/sdk/gitea/issue.go
generated
vendored
28
vendor/code.gitea.io/sdk/gitea/issue.go
generated
vendored
@ -118,14 +118,14 @@ func (c *Client) CreateIssue(owner, repo string, opt CreateIssueOption) (*Issue,
|
||||
|
||||
// EditIssueOption options for editing an issue
|
||||
type EditIssueOption struct {
|
||||
Title string `json:"title"`
|
||||
Body *string `json:"body"`
|
||||
Assignee *string `json:"assignee"`
|
||||
Assignees []string `json:"assignees"`
|
||||
Milestone *int64 `json:"milestone"`
|
||||
State *string `json:"state"`
|
||||
Title string `json:"title"`
|
||||
Body *string `json:"body"`
|
||||
Assignee *string `json:"assignee"`
|
||||
Assignees []string `json:"assignees"`
|
||||
Milestone *int64 `json:"milestone"`
|
||||
State *string `json:"state"`
|
||||
// swagger:strfmt date-time
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
}
|
||||
|
||||
// EditIssue modify an existing issue for a given repository
|
||||
@ -138,3 +138,17 @@ func (c *Client) EditIssue(owner, repo string, index int64, opt EditIssueOption)
|
||||
return issue, c.getParsedResponse("PATCH", fmt.Sprintf("/repos/%s/%s/issues/%d", owner, repo, index),
|
||||
jsonHeader, bytes.NewReader(body), issue)
|
||||
}
|
||||
|
||||
// EditDeadlineOption options for creating a deadline
|
||||
type EditDeadlineOption struct {
|
||||
// required:true
|
||||
// swagger:strfmt date-time
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
}
|
||||
|
||||
// IssueDeadline represents an issue deadline
|
||||
// swagger:model
|
||||
type IssueDeadline struct {
|
||||
// swagger:strfmt date-time
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
}
|
||||
|
34
vendor/code.gitea.io/sdk/gitea/pull.go
generated
vendored
34
vendor/code.gitea.io/sdk/gitea/pull.go
generated
vendored
@ -85,16 +85,16 @@ func (c *Client) GetPullRequest(owner, repo string, index int64) (*PullRequest,
|
||||
|
||||
// CreatePullRequestOption options when creating a pull request
|
||||
type CreatePullRequestOption struct {
|
||||
Head string `json:"head" binding:"Required"`
|
||||
Base string `json:"base" binding:"Required"`
|
||||
Title string `json:"title" binding:"Required"`
|
||||
Body string `json:"body"`
|
||||
Assignee string `json:"assignee"`
|
||||
Assignees []string `json:"assignees"`
|
||||
Milestone int64 `json:"milestone"`
|
||||
Labels []int64 `json:"labels"`
|
||||
Head string `json:"head" binding:"Required"`
|
||||
Base string `json:"base" binding:"Required"`
|
||||
Title string `json:"title" binding:"Required"`
|
||||
Body string `json:"body"`
|
||||
Assignee string `json:"assignee"`
|
||||
Assignees []string `json:"assignees"`
|
||||
Milestone int64 `json:"milestone"`
|
||||
Labels []int64 `json:"labels"`
|
||||
// swagger:strfmt date-time
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
}
|
||||
|
||||
// CreatePullRequest create pull request with options
|
||||
@ -110,15 +110,15 @@ func (c *Client) CreatePullRequest(owner, repo string, opt CreatePullRequestOpti
|
||||
|
||||
// EditPullRequestOption options when modify pull request
|
||||
type EditPullRequestOption struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Assignee string `json:"assignee"`
|
||||
Assignees []string `json:"assignees"`
|
||||
Milestone int64 `json:"milestone"`
|
||||
Labels []int64 `json:"labels"`
|
||||
State *string `json:"state"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Assignee string `json:"assignee"`
|
||||
Assignees []string `json:"assignees"`
|
||||
Milestone int64 `json:"milestone"`
|
||||
Labels []int64 `json:"labels"`
|
||||
State *string `json:"state"`
|
||||
// swagger:strfmt date-time
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
}
|
||||
|
||||
// EditPullRequest modify pull request with PR id and options
|
||||
|
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
@ -9,10 +9,10 @@
|
||||
"revisionTime": "2018-05-17T01:19:24Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "WMD6+Qh2+5hd9uiq910pF/Ihylw=",
|
||||
"checksumSHA1": "LnxY/6xD4h9dCCJ5nxKEfZZs1Vk=",
|
||||
"path": "code.gitea.io/sdk/gitea",
|
||||
"revision": "1c8d12f79a51605ed91587aa6b86cf38fc0f987f",
|
||||
"revisionTime": "2018-05-01T11:15:19Z"
|
||||
"revision": "7fa627fa5d67d18c39d6dd3c6c4db836916bf234",
|
||||
"revisionTime": "2018-05-10T12:54:05Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=",
|
||||
|
Loading…
Reference in New Issue
Block a user