From e45331d6d23870975111373e53c4a2a8c36ba5ad Mon Sep 17 00:00:00 2001 From: David Schneiderbauer Date: Tue, 15 May 2018 16:14:40 +0200 Subject: [PATCH 01/10] add user language value to hidden input to enable saving of profile without changing language (#3967) --- templates/user/settings/profile.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index e5bb2df011..6a654fda70 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -43,7 +43,7 @@
- {{template "repo/settings/hook_settings" .}} + {{template "repo/settings/webhook/settings" .}} {{end}} diff --git a/templates/repo/settings/hook_discord.tmpl b/templates/repo/settings/webhook/discord.tmpl similarity index 95% rename from templates/repo/settings/hook_discord.tmpl rename to templates/repo/settings/webhook/discord.tmpl index 901e7e6311..75c31efb51 100644 --- a/templates/repo/settings/hook_discord.tmpl +++ b/templates/repo/settings/webhook/discord.tmpl @@ -14,6 +14,6 @@
- {{template "repo/settings/hook_settings" .}} + {{template "repo/settings/webhook/settings" .}} {{end}} diff --git a/templates/repo/settings/hook_gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl similarity index 94% rename from templates/repo/settings/hook_gitea.tmpl rename to templates/repo/settings/webhook/gitea.tmpl index fc5e35e068..87a8813d0e 100644 --- a/templates/repo/settings/hook_gitea.tmpl +++ b/templates/repo/settings/webhook/gitea.tmpl @@ -23,6 +23,6 @@ - {{template "repo/settings/hook_settings" .}} + {{template "repo/settings/webhook/settings" .}} {{end}} diff --git a/templates/repo/settings/hook_gogs.tmpl b/templates/repo/settings/webhook/gogs.tmpl similarity index 96% rename from templates/repo/settings/hook_gogs.tmpl rename to templates/repo/settings/webhook/gogs.tmpl index 28098d14ec..649fb54aea 100644 --- a/templates/repo/settings/hook_gogs.tmpl +++ b/templates/repo/settings/webhook/gogs.tmpl @@ -23,6 +23,6 @@ - {{template "repo/settings/hook_settings" .}} + {{template "repo/settings/webhook/settings" .}} {{end}} diff --git a/templates/repo/settings/hook_history.tmpl b/templates/repo/settings/webhook/history.tmpl similarity index 100% rename from templates/repo/settings/hook_history.tmpl rename to templates/repo/settings/webhook/history.tmpl diff --git a/templates/repo/settings/hook_list.tmpl b/templates/repo/settings/webhook/list.tmpl similarity index 97% rename from templates/repo/settings/hook_list.tmpl rename to templates/repo/settings/webhook/list.tmpl index 4e61ba7a07..de6bd2c5f2 100644 --- a/templates/repo/settings/hook_list.tmpl +++ b/templates/repo/settings/webhook/list.tmpl @@ -48,4 +48,4 @@ -{{template "repo/settings/hook_delete_modal" .}} +{{template "repo/settings/webhook/delete_modal" .}} diff --git a/templates/repo/settings/hook_new.tmpl b/templates/repo/settings/webhook/new.tmpl similarity index 77% rename from templates/repo/settings/hook_new.tmpl rename to templates/repo/settings/webhook/new.tmpl index 7e3cf3c8cf..1b3d114577 100644 --- a/templates/repo/settings/hook_new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -21,14 +21,14 @@
- {{template "repo/settings/hook_gitea" .}} - {{template "repo/settings/hook_gogs" .}} - {{template "repo/settings/hook_slack" .}} - {{template "repo/settings/hook_discord" .}} - {{template "repo/settings/hook_dingtalk" .}} + {{template "repo/settings/webhook/gitea" .}} + {{template "repo/settings/webhook/gogs" .}} + {{template "repo/settings/webhook/slack" .}} + {{template "repo/settings/webhook/discord" .}} + {{template "repo/settings/webhook/dingtalk" .}}
- {{template "repo/settings/hook_history" .}} + {{template "repo/settings/webhook/history" .}} {{template "base/footer" .}} diff --git a/templates/repo/settings/hook_settings.tmpl b/templates/repo/settings/webhook/settings.tmpl similarity index 63% rename from templates/repo/settings/hook_settings.tmpl rename to templates/repo/settings/webhook/settings.tmpl index 7f3406588f..f04c25a0a3 100644 --- a/templates/repo/settings/hook_settings.tmpl +++ b/templates/repo/settings/webhook/settings.tmpl @@ -32,6 +32,26 @@ + +
+
+
+ + + {{.i18n.Tr "repo.settings.event_delete_desc"}} +
+
+
+ +
+
+
+ + + {{.i18n.Tr "repo.settings.event_fork_desc"}} +
+
+
@@ -42,6 +62,26 @@
+ +
+
+
+ + + {{.i18n.Tr "repo.settings.event_issues_desc"}} +
+
+
+ +
+
+
+ + + {{.i18n.Tr "repo.settings.event_issue_comment_desc"}} +
+
+
@@ -62,6 +102,16 @@
+ +
+
+
+ + + {{.i18n.Tr "repo.settings.event_release_desc"}} +
+
+
@@ -83,4 +133,4 @@ {{end}} -{{template "repo/settings/hook_delete_modal" .}} +{{template "repo/settings/webhook/delete_modal" .}} diff --git a/templates/repo/settings/hook_slack.tmpl b/templates/repo/settings/webhook/slack.tmpl similarity index 96% rename from templates/repo/settings/hook_slack.tmpl rename to templates/repo/settings/webhook/slack.tmpl index 16e1859470..c35a679da7 100644 --- a/templates/repo/settings/hook_slack.tmpl +++ b/templates/repo/settings/webhook/slack.tmpl @@ -23,6 +23,6 @@ - {{template "repo/settings/hook_settings" .}} + {{template "repo/settings/webhook/settings" .}} {{end}} diff --git a/vendor/code.gitea.io/sdk/gitea/hook.go b/vendor/code.gitea.io/sdk/gitea/hook.go index a9b0bdbd06..85d99652dc 100644 --- a/vendor/code.gitea.io/sdk/gitea/hook.go +++ b/vendor/code.gitea.io/sdk/gitea/hook.go @@ -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, "", " ") +} + // __________ .__ // \______ \__ __ _____| |__ // | ___/ | \/ ___/ | \ diff --git a/vendor/code.gitea.io/sdk/gitea/issue.go b/vendor/code.gitea.io/sdk/gitea/issue.go index 27809ca3b4..fee7cd6f9f 100644 --- a/vendor/code.gitea.io/sdk/gitea/issue.go +++ b/vendor/code.gitea.io/sdk/gitea/issue.go @@ -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"` +} diff --git a/vendor/code.gitea.io/sdk/gitea/pull.go b/vendor/code.gitea.io/sdk/gitea/pull.go index 7d38b14d7d..6fcdd1d41b 100644 --- a/vendor/code.gitea.io/sdk/gitea/pull.go +++ b/vendor/code.gitea.io/sdk/gitea/pull.go @@ -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 diff --git a/vendor/vendor.json b/vendor/vendor.json index 8b8972b16f..712706b96e 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -9,10 +9,10 @@ "revisionTime": "2018-04-21T01:08:19Z" }, { - "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=", From d79829fc472f7374228bcd5eb30efcbfcad2a4c2 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 16 May 2018 14:03:37 +0000 Subject: [PATCH 04/10] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-BR.ini | 4 ++-- options/locale/locale_uk-UA.ini | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 8608126a49..090063bae0 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -335,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 @@ -345,7 +345,7 @@ uploaded_avatar_not_a_image=O arquivo enviado não é uma imagem. update_avatar_success=Seu avatar foi atualizado. change_password=Atualizar senha -old_password=Senha Atual +old_password=Senha atual new_password=Nova senha retype_new_password=Digite a nova senha novamente password_incorrect=A senha atual está incorreta. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 4d1c07e9d3..99f52af115 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -591,6 +591,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=Об'єднати запит на злиття From 80d1998981252bd5956b618b11115f1bf67cfd68 Mon Sep 17 00:00:00 2001 From: David Schneiderbauer Date: Wed, 16 May 2018 16:18:13 +0200 Subject: [PATCH 05/10] add missing token validation and fix missing alert on application settings page (#3976) --- modules/auth/user_form.go | 2 +- templates/user/settings/applications.tmpl | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index 5906abcd1d..1b00f62634 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -184,7 +184,7 @@ func (f *AddKeyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding // NewAccessTokenForm form for creating access token type NewAccessTokenForm struct { - Name string `binding:"Required"` + Name string `binding:"Required;MaxSize(255)"` } // Validate valideates the fields diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl index f1a3e48115..d842644185 100644 --- a/templates/user/settings/applications.tmpl +++ b/templates/user/settings/applications.tmpl @@ -2,6 +2,7 @@
{{template "user/settings/navbar" .}}
+ {{template "base/alert" .}}

{{.i18n.Tr "settings.manage_access_token"}}

From 4ceb92f311f8c27790378ff2b9d9820308ce5451 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 16 May 2018 14:26:58 +0000 Subject: [PATCH 06/10] [skip ci] Updated translations via Crowdin --- options/locale/locale_zh-CN.ini | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 3a0322e4e5..2d39a175b9 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -306,11 +306,13 @@ form.name_pattern_not_allowed=用户名中不允许使用 "%s"。 [settings] profile=个人信息 +account=账号 password=修改密码 security=安全 avatar=头像设置 ssh_gpg_keys=SSH / GPG 密钥 social=社交帐号绑定 +applications=应用 orgs=管理组织 repos=仓库列表 delete=删除帐户 @@ -328,7 +330,7 @@ location=所在地区 update_profile=更新信息 update_profile_success=您的资料信息已经更新 change_username=您的用户名已更改。 -change_username_prompt=注意:更改账户名将同时改变账户的URL +change_username_prompt=注意:更改账号名将同时改变账号的URL continue=继续操作 cancel=取消操作 language=界面语言 @@ -378,7 +380,7 @@ manage_ssh_keys=管理 SSH 密钥 manage_gpg_keys=管理 GPG 密钥 add_key=增加密钥 ssh_desc=这些 SSH 公钥已经关联到你的账号。相应的私钥拥有完全操作你的仓库的权限。 -gpg_desc=这些 GPG 公钥已经关联到你的账户。请妥善保管你的私钥因为他们将被用于认证提交。 +gpg_desc=这些 GPG 公钥已经关联到你的账号。请妥善保管你的私钥因为他们将被用于认证提交。 ssh_helper=需要帮助? 请查看有关 如何生成 SSH 密钥常见 SSH 问题 寻找答案。 gpg_helper=需要帮助吗?看一看 GitHub 关于GPG 的指导。 add_new_key=增加 SSH 密钥 @@ -421,31 +423,31 @@ unbind_success=社会帐户已从您的帐户中解除绑定。 manage_access_token=管理Access Tokens generate_new_token=生成新的令牌 tokens_desc=这些令牌拥有通过 Gitea API 对您的帐户的访问权限。 -new_token_desc=使用令牌的应用拥有完全访问你的账户的权限。 +new_token_desc=使用令牌的应用拥有完全访问你的账号的权限。 token_name=令牌名称 generate_token=生成令牌 generate_token_success=新令牌生成成功。请拷贝因为令牌将只会显示一次。 delete_token=删除令牌 access_token_deletion=删除Access Tokens access_token_deletion_desc=删除一个令牌将会组织通过它访问你账号的应用。是否继续? -delete_token_success=令牌已经被删除。使用该令牌的应用将不再能够访问你的账户。 +delete_token_success=令牌已经被删除。使用该令牌的应用将不再能够访问你的账号。 -twofa_desc=两步验证可以加强你的账户安全性。 -twofa_is_enrolled=你的账户已启用了两步验证。 +twofa_desc=两步验证可以加强你的账号安全性。 +twofa_is_enrolled=你的账号已启用了两步验证。 twofa_not_enrolled=你的账号未开启两步验证。 twofa_disable=禁用两步认证 twofa_scratch_token_regenerate=重新生成初始令牌 twofa_scratch_token_regenerated=你的初始令牌是 %s。请将它保存到一个安全的地方。 twofa_enroll=启用两步验证 twofa_disable_note=如果需要, 可以禁用双因素身份验证。 -twofa_disable_desc=关掉两步验证会使得您的账户不安全,继续执行? +twofa_disable_desc=关掉两步验证会使得您的账号不安全,继续执行? regenerate_scratch_token_desc=如果您丢失了您的验证口令或已经使用它登录, 您可以在这里重置它。 twofa_disabled=两步验证已被禁用。 scan_this_image=使用您的授权应用扫描这张图片: or_enter_secret=或者输入密钥:%s then_enter_passcode=并输入应用程序中显示的密码: passcode_invalid=密码不正确。再试一次。 -twofa_enrolled=你的账户已经启用了两步验证。请保存初始令牌(%s)到一个安全的地方,此令牌仅当前显示一次。 +twofa_enrolled=你的账号已经启用了两步验证。请保存初始令牌(%s)到一个安全的地方,此令牌仅当前显示一次。 manage_account_links=管理绑定过的账号 manage_account_links_desc=这些外部帐户已经绑定到您的 Gitea 帐户。 From ecfc401eaa707914d487574134fcd9e3bbeac60d Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Wed, 16 May 2018 11:58:44 -0400 Subject: [PATCH 07/10] Allow Gitea to run as different USER in Docker (#3961) * If using a different $USER then rename git user * Chown based on $USER env * Target only one part of passwd * su-exec based on $USER not a hardcoded value --- docker/etc/s6/gitea/run | 2 +- docker/etc/s6/gitea/setup | 2 +- docker/usr/bin/entrypoint | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docker/etc/s6/gitea/run b/docker/etc/s6/gitea/run index 1fddb93708..da5fd6b535 100755 --- a/docker/etc/s6/gitea/run +++ b/docker/etc/s6/gitea/run @@ -2,5 +2,5 @@ [[ -f ./setup ]] && source ./setup pushd /app/gitea > /dev/null - exec su-exec git /app/gitea/gitea web + exec su-exec $USER /app/gitea/gitea web popd diff --git a/docker/etc/s6/gitea/setup b/docker/etc/s6/gitea/setup index 8e6441c5c2..6ca9b82123 100755 --- a/docker/etc/s6/gitea/setup +++ b/docker/etc/s6/gitea/setup @@ -39,5 +39,5 @@ if [ ! -f /data/gitea/conf/app.ini ]; then envsubst < /etc/templates/app.ini > /data/gitea/conf/app.ini fi -chown -R git:git /data/gitea /app/gitea /data/git +chown -R ${USER}:git /data/gitea /app/gitea /data/git chmod 0755 /data/gitea /app/gitea /data/git diff --git a/docker/usr/bin/entrypoint b/docker/usr/bin/entrypoint index b374c5aed7..50623bfa66 100755 --- a/docker/usr/bin/entrypoint +++ b/docker/usr/bin/entrypoint @@ -1,5 +1,12 @@ #!/bin/sh +if [ "${USER}" != "git" ]; then + # rename user + sed -i -e "s/^git\:/${USER}\:/g" /etc/passwd + # switch sshd config to different user + sed -i -e "s/AllowUsers git/AllowUsers ${USER}/g" /etc/ssh/sshd_config +fi + ## Change GID for USER? if [ -n "${USER_GID}" ] && [ "${USER_GID}" != "`id -g ${USER}`" ]; then sed -i -e "s/^${USER}:\([^:]*\):[0-9]*/${USER}:\1:${USER_GID}/" /etc/group From 8176345c0ed7f947d9748a9b1774ad00f5199281 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Wed, 16 May 2018 21:35:07 -0400 Subject: [PATCH 08/10] Add cli commands to regen hooks & keys (#3979) * Add cli commands to regen hooks & keys * make fmt * Allow passing path to config as an option * add docs --- cmd/admin.go | 58 ++++++++++++++++++++++++++ docs/content/doc/usage/command-line.md | 7 ++++ 2 files changed, 65 insertions(+) diff --git a/cmd/admin.go b/cmd/admin.go index 5492b9a2db..6c79141eab 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -25,6 +25,7 @@ var ( subcmdCreateUser, subcmdChangePassword, subcmdRepoSyncReleases, + subcmdRegenerate, }, } @@ -80,6 +81,41 @@ var ( Usage: "Synchronize repository releases with tags", Action: runRepoSyncReleases, } + + subcmdRegenerate = cli.Command{ + Name: "regenerate", + Usage: "Regenerate specific files", + Subcommands: []cli.Command{ + microcmdRegenHooks, + microcmdRegenKeys, + }, + } + + microcmdRegenHooks = cli.Command{ + Name: "hooks", + Usage: "Regenerate git-hooks", + Action: runRegenerateHooks, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "config, c", + Value: "custom/conf/app.ini", + Usage: "Custom configuration file path", + }, + }, + } + + microcmdRegenKeys = cli.Command{ + Name: "keys", + Usage: "Regenerate authorized_keys file", + Action: runRegenerateKeys, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "config, c", + Value: "custom/conf/app.ini", + Usage: "Custom configuration file path", + }, + }, + } ) func runChangePassword(c *cli.Context) error { @@ -195,3 +231,25 @@ func getReleaseCount(id int64) (int64, error) { }, ) } + +func runRegenerateHooks(c *cli.Context) error { + if c.IsSet("config") { + setting.CustomConf = c.String("config") + } + + if err := initDB(); err != nil { + return err + } + return models.SyncRepositoryHooks() +} + +func runRegenerateKeys(c *cli.Context) error { + if c.IsSet("config") { + setting.CustomConf = c.String("config") + } + + if err := initDB(); err != nil { + return err + } + return models.RewriteAllPublicKeys() +} diff --git a/docs/content/doc/usage/command-line.md b/docs/content/doc/usage/command-line.md index cf6feeaf5e..9c16d49049 100644 --- a/docs/content/doc/usage/command-line.md +++ b/docs/content/doc/usage/command-line.md @@ -64,6 +64,13 @@ Admin operations: - `--password value`, `-p value`: New password. Required. - Examples: - `gitea admin change-password --username myname --password asecurepassword` + - `regenerate` + - Options: + - `hooks`: Regenerate git-hooks for all repositories + - `keys`: Regenerate authorized_keys file + - Examples: + - `gitea admin regenerate hooks` + - `gitea admin regenerate keys` #### cert From 8f4d11af0b031451a93d39533ff7fa679195db2a Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 17 May 2018 01:36:23 +0000 Subject: [PATCH 09/10] [skip ci] Updated translations via Crowdin --- options/locale/locale_uk-UA.ini | 36 ++++++++++++++++++++++++--------- options/locale/locale_zh-CN.ini | 12 ++++++++++- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 99f52af115..8ae7de9fa1 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -155,14 +155,14 @@ org_no_results=Відповідних організацій не знайден code_search_results=Результати пошуку '%s' [auth] -create_new_account=Реєстрація аккаунта +create_new_account=Реєстрація облікового запису register_helper_msg=Вже зареєстровані? Увійдіть зараз! disable_register_prompt=Вибачте, можливість реєстрації відключена. Будь ласка, зв'яжіться з адміністратором сайту. disable_register_mail=Підтвердження реєстрації електронною поштою вимкнено. remember_me=Запам'ятати мене forgot_password_title=Забув пароль forgot_password=Забули пароль? -sign_up_now=Потрібен аккаунт? Зареєструватися. +sign_up_now=Потрібен обліковий запис? Зареєструйтеся зараз. confirmation_mail_sent_prompt=Новий лист для підтвердження було відправлено на %s, будь ласка, перевірте вашу поштову скриньку протягом %s для завершення реєстрації. reset_password_mail_sent_prompt=Лист для підтвердження було відправлено на %s. Будь ласка, перевірте вашу поштову скриньку протягом %s для скидання пароля. active_your_account=Активувати обліковий запис @@ -264,15 +264,18 @@ form.name_reserved=Ім'я користувача "%s" зарезервован [settings] profile=Профіль +account=Обліковий запис password=Пароль security=Безпека avatar=Аватар ssh_gpg_keys=SSH / GPG ключі -social=Соціальні акаунти +social=Соціальні облікові записи +applications=Додатки orgs=Керування організаціями repos=Репозиторії delete=Видалити обліковий запис twofa=Двофакторна авторизація +account_link=Прив'язані облікові записи organization=Організації uid=Ідентифікатор Uid @@ -338,7 +341,7 @@ show_openid=Показати у профілю hide_openid=Не показувати у профілі ssh_disabled=SSH вимкнено -manage_social=Керувати зв'язаними аккаунтами соціальних мереж +manage_social=Керувати зв'язаними обліковими записами соціальних мереж unbind=Від'єднати generate_new_token=Згенерувати новий токен @@ -349,6 +352,8 @@ delete_token=Видалити twofa_disable=Вимкнути двофакторну автентифікацію or_enter_secret=Або введіть секрет: %s +manage_account_links=Керування обліковими записами +remove_account_link=Видалити облікові записи delete_account=Видалити ваш обліковий запис @@ -478,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=Нова мітка @@ -671,6 +677,7 @@ search.search_repo=Пошук репозиторію settings=Налаштування settings.options=Репозиторій +settings.collaboration=Співробітники settings.collaboration.admin=Адміністратор settings.collaboration.write=Запис settings.collaboration.read=Читати @@ -696,6 +703,7 @@ settings.admin_settings=Налаштування адміністратора settings.danger_zone=Небезпечна зона settings.new_owner_has_same_repo=Новий власник вже має репозиторій з такою назвою. Будь ласка, виберіть інше ім'я. settings.convert=Перетворити на звичайний репозиторій +settings.convert_desc=Ви можете сконвертувати це дзеркало у звичайний репозиторій. Це не може бути скасовано. settings.transfer=Передати новому власнику settings.wiki_delete=Видалити Wiki-дані settings.confirm_wiki_delete=Видалити Wiki-дані @@ -724,6 +732,7 @@ settings.slack_icon_url=URL іконки settings.discord_username=Ім'я кристувача settings.discord_icon_url=URL іконки settings.slack_color=Колір +settings.event_push_only=Push події settings.event_send_everything=Всі події settings.event_create=Створити settings.event_create_desc=Гілку або тег створено. @@ -811,6 +820,7 @@ org_desc=Опис team_name=Назва команди team_desc=Опис team_permission_desc=Права доступу +team_unit_desc=Дозволити доступ до розділів репозиторію settings=Налаштування @@ -862,11 +872,13 @@ last_page=Остання total=Разом: %d dashboard.statistic=Підсумок +dashboard.system_status=Статус системи dashboard.operation_name=Назва операції dashboard.operation_switch=Перемкнути dashboard.operation_run=Запустити dashboard.delete_inactivate_accounts=Видалити всі неактивні облікові записи dashboard.delete_inactivate_accounts_success=Усі неактивні облікові записи успішно видалено. +dashboard.git_gc_repos_success=Всі репозиторії завершили збирання сміття. dashboard.server_uptime=Uptime серверу dashboard.current_memory_usage=Поточне використання пам'яті dashboard.total_memory_allocated=Виділено пам'яті загалом @@ -877,9 +889,14 @@ dashboard.mspan_structures_obtained=Отримано структур MSpan dashboard.mcache_structures_usage=Використання структур MCache dashboard.mcache_structures_obtained=Отримано структур MCache dashboard.profiling_bucket_hash_table_obtained=Отримано хеш-таблиць профілювання -dashboard.gc_metadata_obtained=Отримано метаданих GC +dashboard.gc_metadata_obtained=Отримано метаданих збирача сміття (GC) dashboard.other_system_allocation_obtained=Отримання інших виділень пам'яті -dashboard.next_gc_recycle=Наступний цикл GC +dashboard.next_gc_recycle=Наступний цикл збирача сміття (GC) +dashboard.last_gc_time=З останнього запуску збирача сміття (GC) +dashboard.total_gc_time=Загальна пауза збирача сміття (GC) +dashboard.total_gc_pause=Загальна пауза збирача сміття (GC) +dashboard.last_gc_pause=Остання пауза збирача сміття (GC) +dashboard.gc_times=Кількість запусків збирача сміття (GC) users.user_manage_panel=Керування обліковими записами користувачів users.new_account=Створити обліковий запис @@ -893,6 +910,7 @@ users.send_register_notify=Надіслати повідомлення про р users.edit=Редагувати users.auth_source=Джерело автентифікації users.local=Локальні +users.edit_account=Редагувати обліковий запис users.max_repo_creation=Максимальна кількість репозиторіїв users.max_repo_creation_desc=(Введіть -1, щоб використовувати глобальний ліміт за замовчуванням.) users.is_activated=Обліковий запис користувача увімкнено @@ -1036,7 +1054,7 @@ config.session_provider=Провайдер сесії config.provider_config=Конфігурація постачальника config.cookie_name=Ім'я файлу cookie config.enable_set_cookie=Увімкнути встановлення cookie -config.gc_interval_time=Інтервал запуску GC +config.gc_interval_time=Інтервал запуску збирача сміття (GC) config.session_life_time=Час життя сесії config.https_only=Тільки HTTPS config.cookie_life_time=Час життя cookie-файлу @@ -1050,12 +1068,12 @@ config.git_disable_diff_highlight=Вимкнути підсвітку синта config.git_max_diff_lines=Максимум рядків на diff (на один файл) config.git_max_diff_line_characters=Максимум символів на diff (на одну строку) config.git_max_diff_files=Максимум diff-файлів (для показу) -config.git_gc_args=Аргументи GC +config.git_gc_args=Аргументи збирача сміття (GC) config.git_migrate_timeout=Тайм-аут міграції config.git_mirror_timeout=Тайм-аут оновлення дзеркала config.git_clone_timeout=Тайм-аут операції клонування config.git_pull_timeout=Тайм-аут операції Pull -config.git_gc_timeout=Тайм-аут операції GC +config.git_gc_timeout=Тайм-аут операції збирача сміття (GC) config.log_config=Конфігурація журналу config.log_mode=Режим журналювання diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 2d39a175b9..84db165ff6 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1000,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=推送 @@ -1130,7 +1140,7 @@ branch.restore_failed=未能还原分支%s。 branch.protected_deletion_failed=分支 '%s' 已被保护,不可删除。 topic.manage_topics=管理主题 -topic.done=已完成 +topic.done=保存 [org] org_name_holder=组织名称 From 2aabfc1afa8b808374c76bdb20b936847ba50c86 Mon Sep 17 00:00:00 2001 From: David Schneiderbauer Date: Thu, 17 May 2018 06:05:00 +0200 Subject: [PATCH 10/10] Splitted the user settings code into several files to be more maintainable (#3968) * refactor setting router code splitted up one huge router settings file into the smaller files representing the actual page structure * move code to subfolder * rename functions * renamed files * add copyright information --- routers/org/setting.go | 4 +- routers/routes/routes.go | 51 +- routers/user/setting.go | 808 ------------------ routers/user/setting/account.go | 174 ++++ .../account_test.go} | 6 +- routers/user/setting/applications.go | 77 ++ routers/user/setting/keys.go | 149 ++++ routers/user/setting/main_test.go | 16 + routers/user/setting/profile.go | 220 +++++ routers/user/setting/security.go | 92 ++ .../security_openid.go} | 8 +- routers/user/setting/security_twofa.go | 187 ++++ 12 files changed, 950 insertions(+), 842 deletions(-) delete mode 100644 routers/user/setting.go create mode 100644 routers/user/setting/account.go rename routers/user/{setting_test.go => setting/account_test.go} (91%) create mode 100644 routers/user/setting/applications.go create mode 100644 routers/user/setting/keys.go create mode 100644 routers/user/setting/main_test.go create mode 100644 routers/user/setting/profile.go create mode 100644 routers/user/setting/security.go rename routers/user/{setting_openid.go => setting/security_openid.go} (94%) create mode 100644 routers/user/setting/security_twofa.go diff --git a/routers/org/setting.go b/routers/org/setting.go index 937697d07e..7f652c11d6 100644 --- a/routers/org/setting.go +++ b/routers/org/setting.go @@ -13,7 +13,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/routers/user" + userSetting "code.gitea.io/gitea/routers/user/setting" ) const ( @@ -91,7 +91,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) { // SettingsAvatar response for change avatar on settings page func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) { form.Source = auth.AvatarLocal - if err := user.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil { + if err := userSetting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil { ctx.Flash.Error(err.Error()) } else { ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success")) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 40b5f4bfb3..07be6653a6 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -27,6 +27,7 @@ import ( "code.gitea.io/gitea/routers/private" "code.gitea.io/gitea/routers/repo" "code.gitea.io/gitea/routers/user" + userSetting "code.gitea.io/gitea/routers/user/setting" "github.com/go-macaron/binding" "github.com/go-macaron/cache" @@ -216,39 +217,39 @@ func RegisterRoutes(m *macaron.Macaron) { }, reqSignOut) m.Group("/user/settings", func() { - m.Get("", user.Settings) - m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost) - m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), user.SettingsAvatarPost) - m.Post("/avatar/delete", user.SettingsDeleteAvatar) + m.Get("", userSetting.Profile) + m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost) + m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), userSetting.AvatarPost) + m.Post("/avatar/delete", userSetting.DeleteAvatar) m.Group("/account", func() { - m.Combo("").Get(user.SettingsAccount).Post(bindIgnErr(auth.ChangePasswordForm{}), user.SettingsAccountPost) - m.Post("/email", bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost) - m.Post("/email/delete", user.DeleteEmail) - m.Post("/delete", user.SettingsDelete) + m.Combo("").Get(userSetting.Account).Post(bindIgnErr(auth.ChangePasswordForm{}), userSetting.AccountPost) + m.Post("/email", bindIgnErr(auth.AddEmailForm{}), userSetting.EmailPost) + m.Post("/email/delete", userSetting.DeleteEmail) + m.Post("/delete", userSetting.DeleteAccount) }) m.Group("/security", func() { - m.Get("", user.SettingsSecurity) + m.Get("", userSetting.Security) m.Group("/two_factor", func() { - m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch) - m.Post("/disable", user.SettingsTwoFactorDisable) - m.Get("/enroll", user.SettingsTwoFactorEnroll) - m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), user.SettingsTwoFactorEnrollPost) + m.Post("/regenerate_scratch", userSetting.RegenerateScratchTwoFactor) + m.Post("/disable", userSetting.DisableTwoFactor) + m.Get("/enroll", userSetting.EnrollTwoFactor) + m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost) }) m.Group("/openid", func() { - m.Post("", bindIgnErr(auth.AddOpenIDForm{}), user.SettingsOpenIDPost) - m.Post("/delete", user.DeleteOpenID) - m.Post("/toggle_visibility", user.ToggleOpenIDVisibility) + m.Post("", bindIgnErr(auth.AddOpenIDForm{}), userSetting.OpenIDPost) + m.Post("/delete", userSetting.DeleteOpenID) + m.Post("/toggle_visibility", userSetting.ToggleOpenIDVisibility) }, openIDSignInEnabled) - m.Post("/account_link", user.SettingsDeleteAccountLink) + m.Post("/account_link", userSetting.DeleteAccountLink) }) - m.Combo("/applications").Get(user.SettingsApplications). - Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost) - m.Post("/applications/delete", user.SettingsDeleteApplication) - m.Combo("/keys").Get(user.SettingsKeys). - Post(bindIgnErr(auth.AddKeyForm{}), user.SettingsKeysPost) - m.Post("/keys/delete", user.DeleteKey) - m.Get("/organization", user.SettingsOrganization) - m.Get("/repos", user.SettingsRepos) + m.Combo("/applications").Get(userSetting.Applications). + Post(bindIgnErr(auth.NewAccessTokenForm{}), userSetting.ApplicationsPost) + m.Post("/applications/delete", userSetting.DeleteApplication) + m.Combo("/keys").Get(userSetting.Keys). + Post(bindIgnErr(auth.AddKeyForm{}), userSetting.KeysPost) + m.Post("/keys/delete", userSetting.DeleteKey) + m.Get("/organization", userSetting.Organization) + m.Get("/repos", userSetting.Repos) // redirects from old settings urls to new ones // TODO: can be removed on next major version diff --git a/routers/user/setting.go b/routers/user/setting.go deleted file mode 100644 index 1c760e210c..0000000000 --- a/routers/user/setting.go +++ /dev/null @@ -1,808 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package user - -import ( - "bytes" - "errors" - "fmt" - "io/ioutil" - "strings" - - "github.com/Unknwon/com" - "github.com/Unknwon/i18n" - "github.com/pquerna/otp" - "github.com/pquerna/otp/totp" - - "encoding/base64" - "html/template" - "image/png" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/auth" - "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -const ( - tplSettingsProfile base.TplName = "user/settings/profile" - tplSettingsAccount base.TplName = "user/settings/account" - tplSettingsSecurity base.TplName = "user/settings/security" - tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll" - tplSettingsApplications base.TplName = "user/settings/applications" - tplSettingsKeys base.TplName = "user/settings/keys" - tplSettingsOrganization base.TplName = "user/settings/organization" - tplSettingsRepositories base.TplName = "user/settings/repos" -) - -// Settings render user's profile page -func Settings(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsProfile"] = true - ctx.HTML(200, tplSettingsProfile) -} - -func handleUsernameChange(ctx *context.Context, newName string) { - // Non-local users are not allowed to change their username. - if len(newName) == 0 || !ctx.User.IsLocal() { - return - } - - // Check if user name has been changed - if ctx.User.LowerName != strings.ToLower(newName) { - if err := models.ChangeUserName(ctx.User, newName); err != nil { - switch { - case models.IsErrUserAlreadyExist(err): - ctx.Flash.Error(ctx.Tr("form.username_been_taken")) - ctx.Redirect(setting.AppSubURL + "/user/settings") - case models.IsErrEmailAlreadyUsed(err): - ctx.Flash.Error(ctx.Tr("form.email_been_used")) - ctx.Redirect(setting.AppSubURL + "/user/settings") - case models.IsErrNameReserved(err): - ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName)) - ctx.Redirect(setting.AppSubURL + "/user/settings") - case models.IsErrNamePatternNotAllowed(err): - ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) - ctx.Redirect(setting.AppSubURL + "/user/settings") - default: - ctx.ServerError("ChangeUserName", err) - } - return - } - log.Trace("User name changed: %s -> %s", ctx.User.Name, newName) - } - - // In case it's just a case change - ctx.User.Name = newName - ctx.User.LowerName = strings.ToLower(newName) -} - -// SettingsPost response for change user's profile -func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsProfile"] = true - - if ctx.HasError() { - ctx.HTML(200, tplSettingsProfile) - return - } - - handleUsernameChange(ctx, form.Name) - if ctx.Written() { - return - } - - ctx.User.FullName = form.FullName - ctx.User.Email = form.Email - ctx.User.KeepEmailPrivate = form.KeepEmailPrivate - ctx.User.Website = form.Website - ctx.User.Location = form.Location - ctx.User.Language = form.Language - if err := models.UpdateUserSetting(ctx.User); err != nil { - if _, ok := err.(models.ErrEmailAlreadyUsed); ok { - ctx.Flash.Error(ctx.Tr("form.email_been_used")) - ctx.Redirect(setting.AppSubURL + "/user/settings") - return - } - ctx.ServerError("UpdateUser", err) - return - } - - // Update the language to the one we just set - ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL) - - log.Trace("User settings updated: %s", ctx.User.Name) - ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success")) - ctx.Redirect(setting.AppSubURL + "/user/settings") -} - -// UpdateAvatarSetting update user's avatar -// FIXME: limit size. -func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *models.User) error { - ctxUser.UseCustomAvatar = form.Source == auth.AvatarLocal - if len(form.Gravatar) > 0 { - ctxUser.Avatar = base.EncodeMD5(form.Gravatar) - ctxUser.AvatarEmail = form.Gravatar - } - - if form.Avatar != nil { - fr, err := form.Avatar.Open() - if err != nil { - return fmt.Errorf("Avatar.Open: %v", err) - } - defer fr.Close() - - data, err := ioutil.ReadAll(fr) - if err != nil { - return fmt.Errorf("ioutil.ReadAll: %v", err) - } - if !base.IsImageFile(data) { - return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) - } - if err = ctxUser.UploadAvatar(data); err != nil { - return fmt.Errorf("UploadAvatar: %v", err) - } - } else { - // No avatar is uploaded but setting has been changed to enable, - // generate a random one when needed. - if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) { - if err := ctxUser.GenerateRandomAvatar(); err != nil { - log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err) - } - } - } - - if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil { - return fmt.Errorf("UpdateUser: %v", err) - } - - return nil -} - -// SettingsAvatarPost response for change user's avatar request -func SettingsAvatarPost(ctx *context.Context, form auth.AvatarForm) { - if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil { - ctx.Flash.Error(err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.update_avatar_success")) - } - - ctx.Redirect(setting.AppSubURL + "/user/settings") -} - -// SettingsDeleteAvatar render delete avatar page -func SettingsDeleteAvatar(ctx *context.Context) { - if err := ctx.User.DeleteAvatar(); err != nil { - ctx.Flash.Error(err.Error()) - } - - ctx.Redirect(setting.AppSubURL + "/user/settings") -} - -// SettingsAccount renders change user's password, user's email and user suicide page -func SettingsAccount(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsAccount"] = true - ctx.Data["Email"] = ctx.User.Email - - emails, err := models.GetEmailAddresses(ctx.User.ID) - if err != nil { - ctx.ServerError("GetEmailAddresses", err) - return - } - ctx.Data["Emails"] = emails - - ctx.HTML(200, tplSettingsAccount) -} - -// SettingsAccountPost response for change user's password -func SettingsAccountPost(ctx *context.Context, form auth.ChangePasswordForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsAccount"] = true - - if ctx.HasError() { - ctx.HTML(200, tplSettingsAccount) - return - } - - if len(form.Password) < setting.MinPasswordLength { - ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength)) - } else if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) { - ctx.Flash.Error(ctx.Tr("settings.password_incorrect")) - } else if form.Password != form.Retype { - ctx.Flash.Error(ctx.Tr("form.password_not_match")) - } else { - var err error - if ctx.User.Salt, err = models.GetUserSalt(); err != nil { - ctx.ServerError("UpdateUser", err) - return - } - ctx.User.HashPassword(form.Password) - if err := models.UpdateUserCols(ctx.User, "salt", "passwd"); err != nil { - ctx.ServerError("UpdateUser", err) - return - } - log.Trace("User password updated: %s", ctx.User.Name) - ctx.Flash.Success(ctx.Tr("settings.change_password_success")) - } - - ctx.Redirect(setting.AppSubURL + "/user/settings/account") -} - -// SettingsEmailPost response for change user's email -func SettingsEmailPost(ctx *context.Context, form auth.AddEmailForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsAccount"] = true - - // Make emailaddress primary. - if ctx.Query("_method") == "PRIMARY" { - if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil { - ctx.ServerError("MakeEmailPrimary", err) - return - } - - log.Trace("Email made primary: %s", ctx.User.Name) - ctx.Redirect(setting.AppSubURL + "/user/settings/account") - return - } - - // Add Email address. - emails, err := models.GetEmailAddresses(ctx.User.ID) - if err != nil { - ctx.ServerError("GetEmailAddresses", err) - return - } - ctx.Data["Emails"] = emails - - if ctx.HasError() { - ctx.HTML(200, tplSettingsAccount) - return - } - - email := &models.EmailAddress{ - UID: ctx.User.ID, - Email: form.Email, - IsActivated: !setting.Service.RegisterEmailConfirm, - } - if err := models.AddEmailAddress(email); err != nil { - if models.IsErrEmailAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) - return - } - ctx.ServerError("AddEmailAddress", err) - return - } - - // Send confirmation email - if setting.Service.RegisterEmailConfirm { - models.SendActivateEmailMail(ctx.Context, ctx.User, email) - - if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil { - log.Error(4, "Set cache(MailResendLimit) fail: %v", err) - } - ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language()))) - } else { - ctx.Flash.Success(ctx.Tr("settings.add_email_success")) - } - - log.Trace("Email address added: %s", email.Email) - ctx.Redirect(setting.AppSubURL + "/user/settings/account") -} - -// DeleteEmail response for delete user's email -func DeleteEmail(ctx *context.Context) { - if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil { - ctx.ServerError("DeleteEmail", err) - return - } - log.Trace("Email address deleted: %s", ctx.User.Name) - - ctx.Flash.Success(ctx.Tr("settings.email_deletion_success")) - ctx.JSON(200, map[string]interface{}{ - "redirect": setting.AppSubURL + "/user/settings/account", - }) -} - -// SettingsDelete render user suicide page and response for delete user himself -func SettingsDelete(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsAccount"] = true - - if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil { - if models.IsErrUserNotExist(err) { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) - } else { - ctx.ServerError("UserSignIn", err) - } - return - } - - if err := models.DeleteUser(ctx.User); err != nil { - switch { - case models.IsErrUserOwnRepos(err): - ctx.Flash.Error(ctx.Tr("form.still_own_repo")) - ctx.Redirect(setting.AppSubURL + "/user/settings/account") - case models.IsErrUserHasOrgs(err): - ctx.Flash.Error(ctx.Tr("form.still_has_org")) - ctx.Redirect(setting.AppSubURL + "/user/settings/account") - default: - ctx.ServerError("DeleteUser", err) - } - } else { - log.Trace("Account deleted: %s", ctx.User.Name) - ctx.Redirect(setting.AppSubURL + "/") - } -} - -// SettingsSecurity render change user's password page and 2FA -func SettingsSecurity(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - enrolled := true - _, err := models.GetTwoFactorByUID(ctx.User.ID) - if err != nil { - if models.IsErrTwoFactorNotEnrolled(err) { - enrolled = false - } else { - ctx.ServerError("SettingsTwoFactor", err) - return - } - } - ctx.Data["TwofaEnrolled"] = enrolled - - accountLinks, err := models.ListAccountLinks(ctx.User) - if err != nil { - ctx.ServerError("ListAccountLinks", err) - return - } - - // map the provider display name with the LoginSource - sources := make(map[*models.LoginSource]string) - for _, externalAccount := range accountLinks { - if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil { - var providerDisplayName string - if loginSource.IsOAuth2() { - providerTechnicalName := loginSource.OAuth2().Provider - providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName - } else { - providerDisplayName = loginSource.Name - } - sources[loginSource] = providerDisplayName - } - } - ctx.Data["AccountLinks"] = sources - - if ctx.Query("openid.return_to") != "" { - settingsOpenIDVerify(ctx) - return - } - - openid, err := models.GetUserOpenIDs(ctx.User.ID) - if err != nil { - ctx.ServerError("GetUserOpenIDs", err) - return - } - ctx.Data["OpenIDs"] = openid - - ctx.HTML(200, tplSettingsSecurity) -} - -// SettingsDeleteAccountLink delete a single account link -func SettingsDeleteAccountLink(ctx *context.Context) { - if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil { - ctx.Flash.Error("RemoveAccountLink: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success")) - } - - ctx.JSON(200, map[string]interface{}{ - "redirect": setting.AppSubURL + "/user/settings/security", - }) -} - -// SettingsApplications render manage access token page -func SettingsApplications(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsApplications"] = true - - tokens, err := models.ListAccessTokens(ctx.User.ID) - if err != nil { - ctx.ServerError("ListAccessTokens", err) - return - } - ctx.Data["Tokens"] = tokens - - ctx.HTML(200, tplSettingsApplications) -} - -// SettingsApplicationsPost response for add user's access token -func SettingsApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsApplications"] = true - - if ctx.HasError() { - tokens, err := models.ListAccessTokens(ctx.User.ID) - if err != nil { - ctx.ServerError("ListAccessTokens", err) - return - } - ctx.Data["Tokens"] = tokens - ctx.HTML(200, tplSettingsApplications) - return - } - - t := &models.AccessToken{ - UID: ctx.User.ID, - Name: form.Name, - } - if err := models.NewAccessToken(t); err != nil { - ctx.ServerError("NewAccessToken", err) - return - } - - ctx.Flash.Success(ctx.Tr("settings.generate_token_success")) - ctx.Flash.Info(t.Sha1) - - ctx.Redirect(setting.AppSubURL + "/user/settings/applications") -} - -// SettingsDeleteApplication response for delete user access token -func SettingsDeleteApplication(ctx *context.Context) { - if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil { - ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.delete_token_success")) - } - - ctx.JSON(200, map[string]interface{}{ - "redirect": setting.AppSubURL + "/user/settings/applications", - }) -} - -// SettingsKeys render user's SSH/GPG public keys page -func SettingsKeys(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsKeys"] = true - ctx.Data["DisableSSH"] = setting.SSH.Disabled - - keys, err := models.ListPublicKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListPublicKeys", err) - return - } - ctx.Data["Keys"] = keys - - gpgkeys, err := models.ListGPGKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListGPGKeys", err) - return - } - ctx.Data["GPGKeys"] = gpgkeys - - ctx.HTML(200, tplSettingsKeys) -} - -// SettingsKeysPost response for change user's SSH/GPG keys -func SettingsKeysPost(ctx *context.Context, form auth.AddKeyForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsKeys"] = true - - keys, err := models.ListPublicKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListPublicKeys", err) - return - } - ctx.Data["Keys"] = keys - - gpgkeys, err := models.ListGPGKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListGPGKeys", err) - return - } - ctx.Data["GPGKeys"] = gpgkeys - - if ctx.HasError() { - ctx.HTML(200, tplSettingsKeys) - return - } - switch form.Type { - case "gpg": - key, err := models.AddGPGKey(ctx.User.ID, form.Content) - if err != nil { - ctx.Data["HasGPGError"] = true - switch { - case models.IsErrGPGKeyParsing(err): - ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error())) - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - case models.IsErrGPGKeyIDAlreadyUsed(err): - ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) - case models.IsErrGPGNoEmailFound(err): - ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) - default: - ctx.ServerError("AddPublicKey", err) - } - return - } - ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", key.KeyID)) - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - case "ssh": - content, err := models.CheckPublicKeyString(form.Content) - if err != nil { - if models.IsErrSSHDisabled(err) { - ctx.Flash.Info(ctx.Tr("settings.ssh_disabled")) - } else if models.IsErrKeyUnableVerify(err) { - ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) - } else { - ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error())) - } - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - return - } - - if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil { - ctx.Data["HasSSHError"] = true - switch { - case models.IsErrKeyAlreadyExist(err): - ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form) - case models.IsErrKeyNameAlreadyUsed(err): - ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form) - default: - ctx.ServerError("AddPublicKey", err) - } - return - } - ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title)) - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - - default: - ctx.Flash.Warning("Function not implemented") - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - } - -} - -// DeleteKey response for delete user's SSH/GPG key -func DeleteKey(ctx *context.Context) { - - switch ctx.Query("type") { - case "gpg": - if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil { - ctx.Flash.Error("DeleteGPGKey: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success")) - } - case "ssh": - if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil { - ctx.Flash.Error("DeletePublicKey: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success")) - } - default: - ctx.Flash.Warning("Function not implemented") - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - } - ctx.JSON(200, map[string]interface{}{ - "redirect": setting.AppSubURL + "/user/settings/keys", - }) -} - -// SettingsTwoFactorRegenerateScratch regenerates the user's 2FA scratch code. -func SettingsTwoFactorRegenerateScratch(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - t, err := models.GetTwoFactorByUID(ctx.User.ID) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if err = t.GenerateScratchToken(); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if err = models.UpdateTwoFactor(t); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken)) - ctx.Redirect(setting.AppSubURL + "/user/settings/security") -} - -// SettingsTwoFactorDisable deletes the user's 2FA settings. -func SettingsTwoFactorDisable(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - t, err := models.GetTwoFactorByUID(ctx.User.ID) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - ctx.Flash.Success(ctx.Tr("settings.twofa_disabled")) - ctx.Redirect(setting.AppSubURL + "/user/settings/security") -} - -func twofaGenerateSecretAndQr(ctx *context.Context) bool { - var otpKey *otp.Key - var err error - uri := ctx.Session.Get("twofaUri") - if uri != nil { - otpKey, err = otp.NewKeyFromURL(uri.(string)) - } - if otpKey == nil { - err = nil // clear the error, in case the URL was invalid - otpKey, err = totp.Generate(totp.GenerateOpts{ - Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")", - AccountName: ctx.User.Name, - }) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return false - } - } - - ctx.Data["TwofaSecret"] = otpKey.Secret() - img, err := otpKey.Image(320, 240) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return false - } - - var imgBytes bytes.Buffer - if err = png.Encode(&imgBytes, img); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return false - } - - ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes())) - ctx.Session.Set("twofaSecret", otpKey.Secret()) - ctx.Session.Set("twofaUri", otpKey.String()) - return true -} - -// SettingsTwoFactorEnroll shows the page where the user can enroll into 2FA. -func SettingsTwoFactorEnroll(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - t, err := models.GetTwoFactorByUID(ctx.User.ID) - if t != nil { - // already enrolled - ctx.ServerError("SettingsTwoFactor", err) - return - } - if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if !twofaGenerateSecretAndQr(ctx) { - return - } - - ctx.HTML(200, tplSettingsTwofaEnroll) -} - -// SettingsTwoFactorEnrollPost handles enrolling the user into 2FA. -func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - t, err := models.GetTwoFactorByUID(ctx.User.ID) - if t != nil { - // already enrolled - ctx.ServerError("SettingsTwoFactor", err) - return - } - if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if ctx.HasError() { - if !twofaGenerateSecretAndQr(ctx) { - return - } - ctx.HTML(200, tplSettingsTwofaEnroll) - return - } - - secret := ctx.Session.Get("twofaSecret").(string) - if !totp.Validate(form.Passcode, secret) { - if !twofaGenerateSecretAndQr(ctx) { - return - } - ctx.Flash.Error(ctx.Tr("settings.passcode_invalid")) - ctx.HTML(200, tplSettingsTwofaEnroll) - return - } - - t = &models.TwoFactor{ - UID: ctx.User.ID, - } - err = t.SetSecret(secret) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - err = t.GenerateScratchToken() - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if err = models.NewTwoFactor(t); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - ctx.Session.Delete("twofaSecret") - ctx.Session.Delete("twofaUri") - ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken)) - ctx.Redirect(setting.AppSubURL + "/user/settings/security") -} - -// SettingsOrganization render all the organization of the user -func SettingsOrganization(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsOrganization"] = true - orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned) - if err != nil { - ctx.ServerError("GetOrgsByUserID", err) - return - } - ctx.Data["Orgs"] = orgs - ctx.HTML(200, tplSettingsOrganization) -} - -// SettingsRepos display a list of all repositories of the user -func SettingsRepos(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsRepos"] = true - ctxUser := ctx.User - - var err error - if err = ctxUser.GetRepositories(1, setting.UI.User.RepoPagingNum); err != nil { - ctx.ServerError("GetRepositories", err) - return - } - repos := ctxUser.Repos - - for i := range repos { - if repos[i].IsFork { - err := repos[i].GetBaseRepo() - if err != nil { - ctx.ServerError("GetBaseRepo", err) - return - } - err = repos[i].BaseRepo.GetOwner() - if err != nil { - ctx.ServerError("GetOwner", err) - return - } - } - } - - ctx.Data["Owner"] = ctxUser - ctx.Data["Repos"] = repos - - ctx.HTML(200, tplSettingsRepositories) -} diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go new file mode 100644 index 0000000000..966d96aeda --- /dev/null +++ b/routers/user/setting/account.go @@ -0,0 +1,174 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsAccount base.TplName = "user/settings/account" +) + +// Account renders change user's password, user's email and user suicide page +func Account(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccount"] = true + ctx.Data["Email"] = ctx.User.Email + + emails, err := models.GetEmailAddresses(ctx.User.ID) + if err != nil { + ctx.ServerError("GetEmailAddresses", err) + return + } + ctx.Data["Emails"] = emails + + ctx.HTML(200, tplSettingsAccount) +} + +// AccountPost response for change user's password +func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccount"] = true + + if ctx.HasError() { + ctx.HTML(200, tplSettingsAccount) + return + } + + if len(form.Password) < setting.MinPasswordLength { + ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength)) + } else if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) { + ctx.Flash.Error(ctx.Tr("settings.password_incorrect")) + } else if form.Password != form.Retype { + ctx.Flash.Error(ctx.Tr("form.password_not_match")) + } else { + var err error + if ctx.User.Salt, err = models.GetUserSalt(); err != nil { + ctx.ServerError("UpdateUser", err) + return + } + ctx.User.HashPassword(form.Password) + if err := models.UpdateUserCols(ctx.User, "salt", "passwd"); err != nil { + ctx.ServerError("UpdateUser", err) + return + } + log.Trace("User password updated: %s", ctx.User.Name) + ctx.Flash.Success(ctx.Tr("settings.change_password_success")) + } + + ctx.Redirect(setting.AppSubURL + "/user/settings/account") +} + +// EmailPost response for change user's email +func EmailPost(ctx *context.Context, form auth.AddEmailForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccount"] = true + + // Make emailaddress primary. + if ctx.Query("_method") == "PRIMARY" { + if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil { + ctx.ServerError("MakeEmailPrimary", err) + return + } + + log.Trace("Email made primary: %s", ctx.User.Name) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") + return + } + + // Add Email address. + emails, err := models.GetEmailAddresses(ctx.User.ID) + if err != nil { + ctx.ServerError("GetEmailAddresses", err) + return + } + ctx.Data["Emails"] = emails + + if ctx.HasError() { + ctx.HTML(200, tplSettingsAccount) + return + } + + email := &models.EmailAddress{ + UID: ctx.User.ID, + Email: form.Email, + IsActivated: !setting.Service.RegisterEmailConfirm, + } + if err := models.AddEmailAddress(email); err != nil { + if models.IsErrEmailAlreadyUsed(err) { + ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) + return + } + ctx.ServerError("AddEmailAddress", err) + return + } + + // Send confirmation email + if setting.Service.RegisterEmailConfirm { + models.SendActivateEmailMail(ctx.Context, ctx.User, email) + + if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil { + log.Error(4, "Set cache(MailResendLimit) fail: %v", err) + } + ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language()))) + } else { + ctx.Flash.Success(ctx.Tr("settings.add_email_success")) + } + + log.Trace("Email address added: %s", email.Email) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") +} + +// DeleteEmail response for delete user's email +func DeleteEmail(ctx *context.Context) { + if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil { + ctx.ServerError("DeleteEmail", err) + return + } + log.Trace("Email address deleted: %s", ctx.User.Name) + + ctx.Flash.Success(ctx.Tr("settings.email_deletion_success")) + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/account", + }) +} + +// DeleteAccount render user suicide page and response for delete user himself +func DeleteAccount(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccount"] = true + + if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil { + if models.IsErrUserNotExist(err) { + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) + } else { + ctx.ServerError("UserSignIn", err) + } + return + } + + if err := models.DeleteUser(ctx.User); err != nil { + switch { + case models.IsErrUserOwnRepos(err): + ctx.Flash.Error(ctx.Tr("form.still_own_repo")) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") + case models.IsErrUserHasOrgs(err): + ctx.Flash.Error(ctx.Tr("form.still_has_org")) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") + default: + ctx.ServerError("DeleteUser", err) + } + } else { + log.Trace("Account deleted: %s", ctx.User.Name) + ctx.Redirect(setting.AppSubURL + "/") + } +} diff --git a/routers/user/setting_test.go b/routers/user/setting/account_test.go similarity index 91% rename from routers/user/setting_test.go rename to routers/user/setting/account_test.go index 6aa9a07439..59fbda1569 100644 --- a/routers/user/setting_test.go +++ b/routers/user/setting/account_test.go @@ -1,8 +1,8 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package user +package setting import ( "net/http" @@ -56,7 +56,7 @@ func TestChangePassword(t *testing.T) { test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) - SettingsAccountPost(ctx, auth.ChangePasswordForm{ + AccountPost(ctx, auth.ChangePasswordForm{ OldPassword: req.OldPassword, Password: req.NewPassword, Retype: req.Retype, diff --git a/routers/user/setting/applications.go b/routers/user/setting/applications.go new file mode 100644 index 0000000000..f292b65d70 --- /dev/null +++ b/routers/user/setting/applications.go @@ -0,0 +1,77 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsApplications base.TplName = "user/settings/applications" +) + +// Applications render manage access token page +func Applications(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsApplications"] = true + + tokens, err := models.ListAccessTokens(ctx.User.ID) + if err != nil { + ctx.ServerError("ListAccessTokens", err) + return + } + ctx.Data["Tokens"] = tokens + + ctx.HTML(200, tplSettingsApplications) +} + +// ApplicationsPost response for add user's access token +func ApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsApplications"] = true + + if ctx.HasError() { + tokens, err := models.ListAccessTokens(ctx.User.ID) + if err != nil { + ctx.ServerError("ListAccessTokens", err) + return + } + ctx.Data["Tokens"] = tokens + ctx.HTML(200, tplSettingsApplications) + return + } + + t := &models.AccessToken{ + UID: ctx.User.ID, + Name: form.Name, + } + if err := models.NewAccessToken(t); err != nil { + ctx.ServerError("NewAccessToken", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.generate_token_success")) + ctx.Flash.Info(t.Sha1) + + ctx.Redirect(setting.AppSubURL + "/user/settings/applications") +} + +// DeleteApplication response for delete user access token +func DeleteApplication(ctx *context.Context) { + if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil { + ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.delete_token_success")) + } + + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/applications", + }) +} diff --git a/routers/user/setting/keys.go b/routers/user/setting/keys.go new file mode 100644 index 0000000000..5c28fa6e6d --- /dev/null +++ b/routers/user/setting/keys.go @@ -0,0 +1,149 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsKeys base.TplName = "user/settings/keys" +) + +// Keys render user's SSH/GPG public keys page +func Keys(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsKeys"] = true + ctx.Data["DisableSSH"] = setting.SSH.Disabled + + keys, err := models.ListPublicKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListPublicKeys", err) + return + } + ctx.Data["Keys"] = keys + + gpgkeys, err := models.ListGPGKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListGPGKeys", err) + return + } + ctx.Data["GPGKeys"] = gpgkeys + + ctx.HTML(200, tplSettingsKeys) +} + +// KeysPost response for change user's SSH/GPG keys +func KeysPost(ctx *context.Context, form auth.AddKeyForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsKeys"] = true + + keys, err := models.ListPublicKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListPublicKeys", err) + return + } + ctx.Data["Keys"] = keys + + gpgkeys, err := models.ListGPGKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListGPGKeys", err) + return + } + ctx.Data["GPGKeys"] = gpgkeys + + if ctx.HasError() { + ctx.HTML(200, tplSettingsKeys) + return + } + switch form.Type { + case "gpg": + key, err := models.AddGPGKey(ctx.User.ID, form.Content) + if err != nil { + ctx.Data["HasGPGError"] = true + switch { + case models.IsErrGPGKeyParsing(err): + ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error())) + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + case models.IsErrGPGKeyIDAlreadyUsed(err): + ctx.Data["Err_Content"] = true + ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) + case models.IsErrGPGNoEmailFound(err): + ctx.Data["Err_Content"] = true + ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) + default: + ctx.ServerError("AddPublicKey", err) + } + return + } + ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", key.KeyID)) + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + case "ssh": + content, err := models.CheckPublicKeyString(form.Content) + if err != nil { + if models.IsErrSSHDisabled(err) { + ctx.Flash.Info(ctx.Tr("settings.ssh_disabled")) + } else if models.IsErrKeyUnableVerify(err) { + ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) + } else { + ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error())) + } + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + return + } + + if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil { + ctx.Data["HasSSHError"] = true + switch { + case models.IsErrKeyAlreadyExist(err): + ctx.Data["Err_Content"] = true + ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form) + case models.IsErrKeyNameAlreadyUsed(err): + ctx.Data["Err_Title"] = true + ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form) + default: + ctx.ServerError("AddPublicKey", err) + } + return + } + ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title)) + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + + default: + ctx.Flash.Warning("Function not implemented") + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + } + +} + +// DeleteKey response for delete user's SSH/GPG key +func DeleteKey(ctx *context.Context) { + + switch ctx.Query("type") { + case "gpg": + if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil { + ctx.Flash.Error("DeleteGPGKey: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success")) + } + case "ssh": + if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil { + ctx.Flash.Error("DeletePublicKey: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success")) + } + default: + ctx.Flash.Warning("Function not implemented") + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + } + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/keys", + }) +} diff --git a/routers/user/setting/main_test.go b/routers/user/setting/main_test.go new file mode 100644 index 0000000000..d343c02f48 --- /dev/null +++ b/routers/user/setting/main_test.go @@ -0,0 +1,16 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models" +) + +func TestMain(m *testing.M) { + models.MainTest(m, filepath.Join("..", "..", "..")) +} diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go new file mode 100644 index 0000000000..2ca64ad2e5 --- /dev/null +++ b/routers/user/setting/profile.go @@ -0,0 +1,220 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import ( + "errors" + "fmt" + "io/ioutil" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "github.com/Unknwon/com" + "github.com/Unknwon/i18n" +) + +const ( + tplSettingsProfile base.TplName = "user/settings/profile" + tplSettingsOrganization base.TplName = "user/settings/organization" + tplSettingsRepositories base.TplName = "user/settings/repos" +) + +// Profile render user's profile page +func Profile(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsProfile"] = true + ctx.HTML(200, tplSettingsProfile) +} + +func handleUsernameChange(ctx *context.Context, newName string) { + // Non-local users are not allowed to change their username. + if len(newName) == 0 || !ctx.User.IsLocal() { + return + } + + // Check if user name has been changed + if ctx.User.LowerName != strings.ToLower(newName) { + if err := models.ChangeUserName(ctx.User, newName); err != nil { + switch { + case models.IsErrUserAlreadyExist(err): + ctx.Flash.Error(ctx.Tr("form.username_been_taken")) + ctx.Redirect(setting.AppSubURL + "/user/settings") + case models.IsErrEmailAlreadyUsed(err): + ctx.Flash.Error(ctx.Tr("form.email_been_used")) + ctx.Redirect(setting.AppSubURL + "/user/settings") + case models.IsErrNameReserved(err): + ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName)) + ctx.Redirect(setting.AppSubURL + "/user/settings") + case models.IsErrNamePatternNotAllowed(err): + ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) + ctx.Redirect(setting.AppSubURL + "/user/settings") + default: + ctx.ServerError("ChangeUserName", err) + } + return + } + log.Trace("User name changed: %s -> %s", ctx.User.Name, newName) + } + + // In case it's just a case change + ctx.User.Name = newName + ctx.User.LowerName = strings.ToLower(newName) +} + +// ProfilePost response for change user's profile +func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsProfile"] = true + + if ctx.HasError() { + ctx.HTML(200, tplSettingsProfile) + return + } + + handleUsernameChange(ctx, form.Name) + if ctx.Written() { + return + } + + ctx.User.FullName = form.FullName + ctx.User.Email = form.Email + ctx.User.KeepEmailPrivate = form.KeepEmailPrivate + ctx.User.Website = form.Website + ctx.User.Location = form.Location + ctx.User.Language = form.Language + if err := models.UpdateUserSetting(ctx.User); err != nil { + if _, ok := err.(models.ErrEmailAlreadyUsed); ok { + ctx.Flash.Error(ctx.Tr("form.email_been_used")) + ctx.Redirect(setting.AppSubURL + "/user/settings") + return + } + ctx.ServerError("UpdateUser", err) + return + } + + // Update the language to the one we just set + ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL) + + log.Trace("User settings updated: %s", ctx.User.Name) + ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success")) + ctx.Redirect(setting.AppSubURL + "/user/settings") +} + +// UpdateAvatarSetting update user's avatar +// FIXME: limit size. +func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *models.User) error { + ctxUser.UseCustomAvatar = form.Source == auth.AvatarLocal + if len(form.Gravatar) > 0 { + ctxUser.Avatar = base.EncodeMD5(form.Gravatar) + ctxUser.AvatarEmail = form.Gravatar + } + + if form.Avatar != nil { + fr, err := form.Avatar.Open() + if err != nil { + return fmt.Errorf("Avatar.Open: %v", err) + } + defer fr.Close() + + data, err := ioutil.ReadAll(fr) + if err != nil { + return fmt.Errorf("ioutil.ReadAll: %v", err) + } + if !base.IsImageFile(data) { + return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) + } + if err = ctxUser.UploadAvatar(data); err != nil { + return fmt.Errorf("UploadAvatar: %v", err) + } + } else { + // No avatar is uploaded but setting has been changed to enable, + // generate a random one when needed. + if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) { + if err := ctxUser.GenerateRandomAvatar(); err != nil { + log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err) + } + } + } + + if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil { + return fmt.Errorf("UpdateUser: %v", err) + } + + return nil +} + +// AvatarPost response for change user's avatar request +func AvatarPost(ctx *context.Context, form auth.AvatarForm) { + if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil { + ctx.Flash.Error(err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.update_avatar_success")) + } + + ctx.Redirect(setting.AppSubURL + "/user/settings") +} + +// DeleteAvatar render delete avatar page +func DeleteAvatar(ctx *context.Context) { + if err := ctx.User.DeleteAvatar(); err != nil { + ctx.Flash.Error(err.Error()) + } + + ctx.Redirect(setting.AppSubURL + "/user/settings") +} + +// Organization render all the organization of the user +func Organization(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsOrganization"] = true + orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned) + if err != nil { + ctx.ServerError("GetOrgsByUserID", err) + return + } + ctx.Data["Orgs"] = orgs + ctx.HTML(200, tplSettingsOrganization) +} + +// Repos display a list of all repositories of the user +func Repos(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsRepos"] = true + ctxUser := ctx.User + + var err error + if err = ctxUser.GetRepositories(1, setting.UI.User.RepoPagingNum); err != nil { + ctx.ServerError("GetRepositories", err) + return + } + repos := ctxUser.Repos + + for i := range repos { + if repos[i].IsFork { + err := repos[i].GetBaseRepo() + if err != nil { + ctx.ServerError("GetBaseRepo", err) + return + } + err = repos[i].BaseRepo.GetOwner() + if err != nil { + ctx.ServerError("GetOwner", err) + return + } + } + } + + ctx.Data["Owner"] = ctxUser + ctx.Data["Repos"] = repos + + ctx.HTML(200, tplSettingsRepositories) +} diff --git a/routers/user/setting/security.go b/routers/user/setting/security.go new file mode 100644 index 0000000000..5346f349ff --- /dev/null +++ b/routers/user/setting/security.go @@ -0,0 +1,92 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsSecurity base.TplName = "user/settings/security" + tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll" +) + +// Security render change user's password page and 2FA +func Security(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + enrolled := true + _, err := models.GetTwoFactorByUID(ctx.User.ID) + if err != nil { + if models.IsErrTwoFactorNotEnrolled(err) { + enrolled = false + } else { + ctx.ServerError("SettingsTwoFactor", err) + return + } + } + ctx.Data["TwofaEnrolled"] = enrolled + + tokens, err := models.ListAccessTokens(ctx.User.ID) + if err != nil { + ctx.ServerError("ListAccessTokens", err) + return + } + ctx.Data["Tokens"] = tokens + + accountLinks, err := models.ListAccountLinks(ctx.User) + if err != nil { + ctx.ServerError("ListAccountLinks", err) + return + } + + // map the provider display name with the LoginSource + sources := make(map[*models.LoginSource]string) + for _, externalAccount := range accountLinks { + if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil { + var providerDisplayName string + if loginSource.IsOAuth2() { + providerTechnicalName := loginSource.OAuth2().Provider + providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName + } else { + providerDisplayName = loginSource.Name + } + sources[loginSource] = providerDisplayName + } + } + ctx.Data["AccountLinks"] = sources + + if ctx.Query("openid.return_to") != "" { + settingsOpenIDVerify(ctx) + return + } + + openid, err := models.GetUserOpenIDs(ctx.User.ID) + if err != nil { + ctx.ServerError("GetUserOpenIDs", err) + return + } + ctx.Data["OpenIDs"] = openid + + ctx.HTML(200, tplSettingsSecurity) +} + +// DeleteAccountLink delete a single account link +func DeleteAccountLink(ctx *context.Context) { + if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil { + ctx.Flash.Error("RemoveAccountLink: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success")) + } + + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/security", + }) +} diff --git a/routers/user/setting_openid.go b/routers/user/setting/security_openid.go similarity index 94% rename from routers/user/setting_openid.go rename to routers/user/setting/security_openid.go index 7716466120..c98dc2cda9 100644 --- a/routers/user/setting_openid.go +++ b/routers/user/setting/security_openid.go @@ -1,8 +1,8 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package user +package setting import ( "code.gitea.io/gitea/models" @@ -13,8 +13,8 @@ import ( "code.gitea.io/gitea/modules/setting" ) -// SettingsOpenIDPost response for change user's openid -func SettingsOpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { +// OpenIDPost response for change user's openid +func OpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsSecurity"] = true diff --git a/routers/user/setting/security_twofa.go b/routers/user/setting/security_twofa.go new file mode 100644 index 0000000000..55101ed1a4 --- /dev/null +++ b/routers/user/setting/security_twofa.go @@ -0,0 +1,187 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import ( + "bytes" + "encoding/base64" + "html/template" + "image/png" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" +) + +// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code. +func RegenerateScratchTwoFactor(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if err = t.GenerateScratchToken(); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if err = models.UpdateTwoFactor(t); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken)) + ctx.Redirect(setting.AppSubURL + "/user/settings/security") +} + +// DisableTwoFactor deletes the user's 2FA settings. +func DisableTwoFactor(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.twofa_disabled")) + ctx.Redirect(setting.AppSubURL + "/user/settings/security") +} + +func twofaGenerateSecretAndQr(ctx *context.Context) bool { + var otpKey *otp.Key + var err error + uri := ctx.Session.Get("twofaUri") + if uri != nil { + otpKey, err = otp.NewKeyFromURL(uri.(string)) + } + if otpKey == nil { + err = nil // clear the error, in case the URL was invalid + otpKey, err = totp.Generate(totp.GenerateOpts{ + Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")", + AccountName: ctx.User.Name, + }) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return false + } + } + + ctx.Data["TwofaSecret"] = otpKey.Secret() + img, err := otpKey.Image(320, 240) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return false + } + + var imgBytes bytes.Buffer + if err = png.Encode(&imgBytes, img); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return false + } + + ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes())) + ctx.Session.Set("twofaSecret", otpKey.Secret()) + ctx.Session.Set("twofaUri", otpKey.String()) + return true +} + +// EnrollTwoFactor shows the page where the user can enroll into 2FA. +func EnrollTwoFactor(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if t != nil { + // already enrolled + ctx.ServerError("SettingsTwoFactor", err) + return + } + if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if !twofaGenerateSecretAndQr(ctx) { + return + } + + ctx.HTML(200, tplSettingsTwofaEnroll) +} + +// EnrollTwoFactorPost handles enrolling the user into 2FA. +func EnrollTwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if t != nil { + // already enrolled + ctx.ServerError("SettingsTwoFactor", err) + return + } + if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if ctx.HasError() { + if !twofaGenerateSecretAndQr(ctx) { + return + } + ctx.HTML(200, tplSettingsTwofaEnroll) + return + } + + secret := ctx.Session.Get("twofaSecret").(string) + if !totp.Validate(form.Passcode, secret) { + if !twofaGenerateSecretAndQr(ctx) { + return + } + ctx.Flash.Error(ctx.Tr("settings.passcode_invalid")) + ctx.HTML(200, tplSettingsTwofaEnroll) + return + } + + t = &models.TwoFactor{ + UID: ctx.User.ID, + } + err = t.SetSecret(secret) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + err = t.GenerateScratchToken() + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if err = models.NewTwoFactor(t); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + ctx.Session.Delete("twofaSecret") + ctx.Session.Delete("twofaUri") + ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken)) + ctx.Redirect(setting.AppSubURL + "/user/settings/security") +}