mirror of https://github.com/gogs/gogs.git
2fa: initial support (#945)
parent
624474386a
commit
a617d52374
3
Makefile
3
Makefile
|
@ -25,6 +25,9 @@ check: test
|
|||
|
||||
dist: release
|
||||
|
||||
web: build
|
||||
./gogs web
|
||||
|
||||
govet:
|
||||
$(GOVET) gogs.go
|
||||
$(GOVET) models pkg routers
|
||||
|
|
17
cmd/web.go
17
cmd/web.go
|
@ -190,8 +190,13 @@ func runWeb(ctx *cli.Context) error {
|
|||
|
||||
// ***** START: User *****
|
||||
m.Group("/user", func() {
|
||||
m.Get("/login", user.SignIn)
|
||||
m.Post("/login", bindIgnErr(form.SignIn{}), user.SignInPost)
|
||||
m.Group("/login", func() {
|
||||
m.Combo("").Get(user.Login).
|
||||
Post(bindIgnErr(form.SignIn{}), user.LoginPost)
|
||||
m.Combo("/two_factor").Get(user.LoginTwoFactor).Post(user.LoginTwoFactorPost)
|
||||
m.Combo("/two_factor_recovery_code").Get(user.LoginTwoFactorRecoveryCode).Post(user.LoginTwoFactorRecoveryCodePost)
|
||||
})
|
||||
|
||||
m.Get("/sign_up", user.SignUp)
|
||||
m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost)
|
||||
m.Get("/reset_password", user.ResetPasswd)
|
||||
|
@ -212,6 +217,14 @@ func runWeb(ctx *cli.Context) error {
|
|||
m.Combo("/ssh").Get(user.SettingsSSHKeys).
|
||||
Post(bindIgnErr(form.AddSSHKey{}), user.SettingsSSHKeysPost)
|
||||
m.Post("/ssh/delete", user.DeleteSSHKey)
|
||||
m.Group("/security", func() {
|
||||
m.Get("", user.SettingsSecurity)
|
||||
m.Combo("/two_factor_enable").Get(user.SettingsTwoFactorEnable).
|
||||
Post(user.SettingsTwoFactorEnablePost)
|
||||
m.Combo("/two_factor_recovery_codes").Get(user.SettingsTwoFactorRecoveryCodes).
|
||||
Post(user.SettingsTwoFactorRecoveryCodesPost)
|
||||
m.Post("/two_factor_disable", user.SettingsTwoFactorDisable)
|
||||
})
|
||||
m.Group("/repositories", func() {
|
||||
m.Get("", user.SettingsRepos)
|
||||
m.Post("/leave", user.SettingsLeaveRepo)
|
||||
|
|
|
@ -168,6 +168,14 @@ reset_password_helper = Click here to reset your password
|
|||
password_too_short = Password length cannot be less then 6.
|
||||
non_local_account = Non-local accounts cannot change passwords through Gogs.
|
||||
|
||||
login_two_factor = Two-factor Authentication
|
||||
login_two_factor_passcode = Authentication Passcode
|
||||
login_two_factor_enter_recovery_code = Enter a two-factor recovery code
|
||||
login_two_factor_recovery = Two-factor Recovery
|
||||
login_two_factor_recovery_code = Recovery Code
|
||||
login_two_factor_enter_passcode = Enter a two-factor passcode
|
||||
login_two_factor_invalid_recovery_code = Recovery code has been used or does not valid.
|
||||
|
||||
[mail]
|
||||
activate_account = Please activate your account
|
||||
activate_email = Verify your email address
|
||||
|
@ -255,6 +263,7 @@ profile = Profile
|
|||
password = Password
|
||||
avatar = Avatar
|
||||
ssh_keys = SSH Keys
|
||||
security = Security
|
||||
repos = Repositories
|
||||
orgs = Organizations
|
||||
applications = Applications
|
||||
|
@ -324,10 +333,29 @@ no_activity = No recent activity
|
|||
key_state_desc = This key is used in last 7 days
|
||||
token_state_desc = This token is used in last 7 days
|
||||
|
||||
manage_social = Manage Associated Social Accounts
|
||||
social_desc = This is a list of associated social accounts. Remove any binding that you do not recognize.
|
||||
unbind = Unbind
|
||||
unbind_success = Social account has been unbound.
|
||||
two_factor = Two-factor Authentication
|
||||
two_factor_status = Status:
|
||||
two_factor_on = On
|
||||
two_factor_off = Off
|
||||
two_factor_enable = Enable
|
||||
two_factor_disable = Disable
|
||||
two_factor_view_recovery_codes = View and save <a href="%s%s">your recovery codes</a> in a safe place. You can use them as passcode if you lose access to your authentication application.
|
||||
two_factor_enable_title = Enable Two-factor Authentication
|
||||
two_factor_scan_qr = Please use your authentication application to scan the image:
|
||||
two_factor_or_enter_secret = Or enter the secret:
|
||||
two_factor_then_enter_passcode = Then enter passcode:
|
||||
two_factor_verify = Verify
|
||||
two_factor_invalid_passcode = The passcode you entered is not valid, please try again!
|
||||
two_factor_enable_error = Enable Two-factor authentication failed: %v
|
||||
two_factor_enable_success = Two-factor authentication has enabled for your account successfully!
|
||||
two_factor_recovery_codes_title = Two-factor Authentication Recovery Codes
|
||||
two_factor_recovery_codes_desc = Recovery codes are used when you temporarily lose access to your authentication application. Each recovery code can only be used once, <b>please keep these codes in a safe place</b>.
|
||||
two_factor_regenerate_recovery_codes = Regenerate Recovery Codes
|
||||
two_factor_regenerate_recovery_codes_error = Regenerate recovery codes failed: %v
|
||||
two_factor_regenerate_recovery_codes_success = New recovery codes has been generated successfully!
|
||||
two_factor_disable_title = Disable Two-factor Authentication
|
||||
two_factor_disable_desc = Your account security level will decrease after disabled two-factor authentication. Do you want to continue?
|
||||
two_factor_disable_success = Two-factor authentication has disabled successfully!
|
||||
|
||||
manage_access_token = Manage Personal Access Tokens
|
||||
generate_new_token = Generate New Token
|
||||
|
|
2
gogs.go
2
gogs.go
|
@ -16,7 +16,7 @@ import (
|
|||
"github.com/gogits/gogs/pkg/setting"
|
||||
)
|
||||
|
||||
const APP_VER = "0.11.4.0405"
|
||||
const APP_VER = "0.11.5.0406"
|
||||
|
||||
func init() {
|
||||
setting.AppVer = APP_VER
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2017 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 errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
type TwoFactorNotFound struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
func IsTwoFactorNotFound(err error) bool {
|
||||
_, ok := err.(TwoFactorNotFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err TwoFactorNotFound) Error() string {
|
||||
return fmt.Sprintf("two-factor authentication does not found [user_id: %d]", err.UserID)
|
||||
}
|
||||
|
||||
type TwoFactorRecoveryCodeNotFound struct {
|
||||
Code string
|
||||
}
|
||||
|
||||
func IsTwoFactorRecoveryCodeNotFound(err error) bool {
|
||||
_, ok := err.(TwoFactorRecoveryCodeNotFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err TwoFactorRecoveryCodeNotFound) Error() string {
|
||||
return fmt.Sprintf("two-factor recovery code does not found [code: %s]", err.Code)
|
||||
}
|
|
@ -27,7 +27,7 @@ import (
|
|||
"github.com/gogits/gogs/pkg/setting"
|
||||
)
|
||||
|
||||
// Engine represents a xorm engine or session.
|
||||
// Engine represents a XORM engine or session.
|
||||
type Engine interface {
|
||||
Delete(interface{}) (int64, error)
|
||||
Exec(string, ...interface{}) (sql.Result, error)
|
||||
|
@ -64,7 +64,7 @@ var (
|
|||
|
||||
func init() {
|
||||
tables = append(tables,
|
||||
new(User), new(PublicKey), new(AccessToken),
|
||||
new(User), new(PublicKey), new(AccessToken), new(TwoFactor), new(TwoFactorRecoveryCode),
|
||||
new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Upload),
|
||||
new(Watch), new(Star), new(Follow), new(Action),
|
||||
new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
// Copyright 2017 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 models
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/pquerna/otp/totp"
|
||||
log "gopkg.in/clog.v1"
|
||||
|
||||
"github.com/gogits/gogs/models/errors"
|
||||
"github.com/gogits/gogs/pkg/setting"
|
||||
"github.com/gogits/gogs/pkg/tool"
|
||||
)
|
||||
|
||||
// TwoFactor represents a two-factor authentication token.
|
||||
type TwoFactor struct {
|
||||
ID int64
|
||||
UserID int64 `xorm:"UNIQUE"`
|
||||
Secret string
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
}
|
||||
|
||||
func (t *TwoFactor) BeforeInsert() {
|
||||
t.CreatedUnix = time.Now().Unix()
|
||||
}
|
||||
|
||||
func (t *TwoFactor) AfterSet(colName string, _ xorm.Cell) {
|
||||
switch colName {
|
||||
case "created_unix":
|
||||
t.Created = time.Unix(t.CreatedUnix, 0).Local()
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateTOTP returns true if given passcode is valid for two-factor authentication token.
|
||||
// It also returns possible validation error.
|
||||
func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
|
||||
secret, err := base64.StdEncoding.DecodeString(t.Secret)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("DecodeString: %v", err)
|
||||
}
|
||||
decryptSecret, err := com.AESGCMDecrypt(tool.MD5Bytes(setting.SecretKey), secret)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("AESGCMDecrypt: %v", err)
|
||||
}
|
||||
return totp.Validate(passcode, string(decryptSecret)), nil
|
||||
}
|
||||
|
||||
// IsUserEnabledTwoFactor returns true if user has enabled two-factor authentication.
|
||||
func IsUserEnabledTwoFactor(userID int64) bool {
|
||||
has, err := x.Where("user_id = ?", userID).Get(new(TwoFactor))
|
||||
if err != nil {
|
||||
log.Error(2, "IsUserEnabledTwoFactor [user_id: %d]: %v", userID, err)
|
||||
}
|
||||
return has
|
||||
}
|
||||
|
||||
func generateRecoveryCodes(userID int64) ([]*TwoFactorRecoveryCode, error) {
|
||||
recoveryCodes := make([]*TwoFactorRecoveryCode, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
code, err := tool.GetRandomString(10)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetRandomString: %v", err)
|
||||
}
|
||||
recoveryCodes[i] = &TwoFactorRecoveryCode{
|
||||
UserID: userID,
|
||||
Code: strings.ToLower(code[:5] + "-" + code[5:]),
|
||||
}
|
||||
}
|
||||
return recoveryCodes, nil
|
||||
}
|
||||
|
||||
// NewTwoFactor creates a new two-factor authentication token and recovery codes for given user.
|
||||
func NewTwoFactor(userID int64, secret string) error {
|
||||
t := &TwoFactor{
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
// Encrypt secret
|
||||
encryptSecret, err := com.AESGCMEncrypt(tool.MD5Bytes(setting.SecretKey), []byte(secret))
|
||||
if err != nil {
|
||||
return fmt.Errorf("AESGCMEncrypt: %v", err)
|
||||
}
|
||||
t.Secret = base64.StdEncoding.EncodeToString(encryptSecret)
|
||||
|
||||
recoveryCodes, err := generateRecoveryCodes(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generateRecoveryCodes: %v", err)
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = sess.Insert(t); err != nil {
|
||||
return fmt.Errorf("insert two-factor: %v", err)
|
||||
} else if _, err = sess.Insert(recoveryCodes); err != nil {
|
||||
return fmt.Errorf("insert recovery codes: %v", err)
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// GetTwoFactorByUserID returns two-factor authentication token of given user.
|
||||
func GetTwoFactorByUserID(userID int64) (*TwoFactor, error) {
|
||||
t := new(TwoFactor)
|
||||
has, err := x.Where("user_id = ?", userID).Get(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, errors.TwoFactorNotFound{userID}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// DeleteTwoFactor removes two-factor authentication token and recovery codes of given user.
|
||||
func DeleteTwoFactor(userID int64) (err error) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = sess.Where("user_id = ?", userID).Delete(new(TwoFactor)); err != nil {
|
||||
return fmt.Errorf("delete two-factor: %v", err)
|
||||
} else if err = deleteRecoveryCodesByUserID(sess, userID); err != nil {
|
||||
return fmt.Errorf("deleteRecoveryCodesByUserID: %v", err)
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// TwoFactorRecoveryCode represents a two-factor authentication recovery code.
|
||||
type TwoFactorRecoveryCode struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
Code string `xorm:"VARCHAR(11)"`
|
||||
IsUsed bool
|
||||
}
|
||||
|
||||
// GetRecoveryCodesByUserID returns all recovery codes of given user.
|
||||
func GetRecoveryCodesByUserID(userID int64) ([]*TwoFactorRecoveryCode, error) {
|
||||
recoveryCodes := make([]*TwoFactorRecoveryCode, 0, 10)
|
||||
return recoveryCodes, x.Where("user_id = ?", userID).Find(&recoveryCodes)
|
||||
}
|
||||
|
||||
func deleteRecoveryCodesByUserID(e Engine, userID int64) error {
|
||||
_, err := e.Where("user_id = ?", userID).Delete(new(TwoFactorRecoveryCode))
|
||||
return err
|
||||
}
|
||||
|
||||
// RegenerateRecoveryCodes regenerates new set of recovery codes for given user.
|
||||
func RegenerateRecoveryCodes(userID int64) error {
|
||||
recoveryCodes, err := generateRecoveryCodes(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generateRecoveryCodes: %v", err)
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = deleteRecoveryCodesByUserID(sess, userID); err != nil {
|
||||
return fmt.Errorf("deleteRecoveryCodesByUserID: %v", err)
|
||||
} else if _, err = sess.Insert(recoveryCodes); err != nil {
|
||||
return fmt.Errorf("insert new recovery codes: %v", err)
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// UseRecoveryCode validates recovery code of given user and marks it is used if valid.
|
||||
func UseRecoveryCode(userID int64, code string) error {
|
||||
recoveryCode := new(TwoFactorRecoveryCode)
|
||||
has, err := x.Where("code = ?", code).And("is_used = ?", false).Get(recoveryCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get unused code: %v", err)
|
||||
} else if !has {
|
||||
return errors.TwoFactorRecoveryCodeNotFound{code}
|
||||
}
|
||||
|
||||
recoveryCode.IsUsed = true
|
||||
if _, err = x.Id(recoveryCode.ID).Cols("is_used").Update(recoveryCode); err != nil {
|
||||
return fmt.Errorf("mark code as used: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -31,8 +31,8 @@ import (
|
|||
|
||||
"github.com/gogits/gogs/models/errors"
|
||||
"github.com/gogits/gogs/pkg/avatar"
|
||||
"github.com/gogits/gogs/pkg/tool"
|
||||
"github.com/gogits/gogs/pkg/setting"
|
||||
"github.com/gogits/gogs/pkg/tool"
|
||||
)
|
||||
|
||||
type UserType int
|
||||
|
@ -404,6 +404,11 @@ func (u *User) IsPublicMember(orgId int64) bool {
|
|||
return IsPublicMembership(orgId, u.ID)
|
||||
}
|
||||
|
||||
// IsEnabledTwoFactor returns true if user has enabled two-factor authentication.
|
||||
func (u *User) IsEnabledTwoFactor() bool {
|
||||
return IsUserEnabledTwoFactor(u.ID)
|
||||
}
|
||||
|
||||
func (u *User) getOrganizationCount(e Engine) (int64, error) {
|
||||
return e.Where("uid=?", u.ID).Count(new(OrgUser))
|
||||
}
|
||||
|
@ -479,7 +484,7 @@ func IsUserExist(uid int64, name string) (bool, error) {
|
|||
if len(name) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
return x.Where("id!=?", uid).Get(&User{LowerName: strings.ToLower(name)})
|
||||
return x.Where("id != ?", uid).Get(&User{LowerName: strings.ToLower(name)})
|
||||
}
|
||||
|
||||
// GetUserSalt returns a ramdom user salt token.
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -89,6 +89,11 @@ func (c *Context) Success(name string) {
|
|||
c.HTML(http.StatusOK, name)
|
||||
}
|
||||
|
||||
// JSONSuccess responses JSON with status http.StatusOK.
|
||||
func (c *Context) JSONSuccess(data interface{}) {
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// RenderWithErr used for page has form validation but need to prompt error to users.
|
||||
func (ctx *Context) RenderWithErr(msg, tpl string, f interface{}) {
|
||||
if f != nil {
|
||||
|
|
|
@ -27,11 +27,16 @@ import (
|
|||
"github.com/gogits/gogs/pkg/setting"
|
||||
)
|
||||
|
||||
// EncodeMD5 encodes string to md5 hex value.
|
||||
func EncodeMD5(str string) string {
|
||||
// MD5Bytes encodes string to MD5 bytes.
|
||||
func MD5Bytes(str string) []byte {
|
||||
m := md5.New()
|
||||
m.Write([]byte(str))
|
||||
return hex.EncodeToString(m.Sum(nil))
|
||||
return m.Sum(nil)
|
||||
}
|
||||
|
||||
// EncodeMD5 encodes string to MD5 hex value.
|
||||
func EncodeMD5(str string) string {
|
||||
return hex.EncodeToString(MD5Bytes(str))
|
||||
}
|
||||
|
||||
// Encode string to sha1 hex value.
|
||||
|
|
|
@ -960,7 +960,7 @@ footer .ui.language .menu {
|
|||
}
|
||||
#create-page-form form input,
|
||||
#create-page-form form textarea {
|
||||
width: 50%!important;
|
||||
width: 50% !important;
|
||||
}
|
||||
.user.activate form,
|
||||
.user.forgot.password form,
|
||||
|
@ -1017,14 +1017,14 @@ footer .ui.language .menu {
|
|||
.user.reset.password form textarea,
|
||||
.user.signin form textarea,
|
||||
.user.signup form textarea {
|
||||
width: 50%!important;
|
||||
width: 50% !important;
|
||||
}
|
||||
.user.activate form,
|
||||
.user.forgot.password form,
|
||||
.user.reset.password form,
|
||||
.user.signin form,
|
||||
.user.signup form {
|
||||
width: 700px!important;
|
||||
width: 700px !important;
|
||||
}
|
||||
.user.activate form .header,
|
||||
.user.forgot.password form .header,
|
||||
|
@ -1040,6 +1040,12 @@ footer .ui.language .menu {
|
|||
.user.signup form .inline.field > label {
|
||||
width: 200px !important;
|
||||
}
|
||||
.user.signin.two-factor form {
|
||||
width: 300px !important;
|
||||
}
|
||||
.user.signin.two-factor form .header {
|
||||
padding-left: inherit !important;
|
||||
}
|
||||
.repository.new.repo form,
|
||||
.repository.new.migrate form,
|
||||
.repository.new.fork form {
|
||||
|
@ -1079,7 +1085,7 @@ footer .ui.language .menu {
|
|||
.repository.new.repo form textarea,
|
||||
.repository.new.migrate form textarea,
|
||||
.repository.new.fork form textarea {
|
||||
width: 50%!important;
|
||||
width: 50% !important;
|
||||
}
|
||||
.repository.new.repo form .dropdown .dropdown.icon,
|
||||
.repository.new.migrate form .dropdown .dropdown.icon,
|
||||
|
@ -2752,7 +2758,7 @@ footer .ui.language .menu {
|
|||
}
|
||||
.organization.new.org form input,
|
||||
.organization.new.org form textarea {
|
||||
width: 50%!important;
|
||||
width: 50% !important;
|
||||
}
|
||||
.organization.options input {
|
||||
min-width: 300px;
|
||||
|
@ -2856,15 +2862,8 @@ footer .ui.language .menu {
|
|||
.user.settings .email.list .item:not(:first-child) .button {
|
||||
margin-top: -10px;
|
||||
}
|
||||
.user.settings.organizations .orgs.non-empty {
|
||||
padding: 0;
|
||||
}
|
||||
.user.settings.organizations .orgs .item {
|
||||
padding: 10px;
|
||||
}
|
||||
.user.settings.organizations .orgs .item .button {
|
||||
margin-top: 5px;
|
||||
margin-right: 8px;
|
||||
.user.settings.security .two-factor .toggle.button {
|
||||
margin-top: -5px;
|
||||
}
|
||||
.user.settings.repositories .repos {
|
||||
padding: 0;
|
||||
|
@ -2876,6 +2875,16 @@ footer .ui.language .menu {
|
|||
.user.settings.repositories .repos .item .button {
|
||||
margin-top: -5px;
|
||||
}
|
||||
.user.settings.organizations .orgs.non-empty {
|
||||
padding: 0;
|
||||
}
|
||||
.user.settings.organizations .orgs .item {
|
||||
padding: 10px;
|
||||
}
|
||||
.user.settings.organizations .orgs .item .button {
|
||||
margin-top: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.user.profile .ui.card .header {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
}
|
||||
input,
|
||||
textarea {
|
||||
width: 50%!important;
|
||||
width: 50% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,10 +52,10 @@
|
|||
.user.reset.password,
|
||||
.user.signin,
|
||||
.user.signup {
|
||||
@input-padding: 200px!important;
|
||||
@input-padding: 200px !important;
|
||||
#create-page-form;
|
||||
form {
|
||||
width: 700px!important;
|
||||
width: 700px !important;
|
||||
.header {
|
||||
padding-left: @input-padding+30px;
|
||||
}
|
||||
|
@ -65,6 +65,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.user.signin.two-factor {
|
||||
form {
|
||||
width: 300px !important;
|
||||
.header {
|
||||
padding-left: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.repository {
|
||||
&.new.repo,
|
||||
&.new.migrate,
|
||||
|
|
|
@ -19,16 +19,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&.organizations .orgs {
|
||||
&.non-empty {
|
||||
padding: 0;
|
||||
}
|
||||
.item {
|
||||
padding: 10px;
|
||||
.button {
|
||||
margin-top: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
&.security {
|
||||
.two-factor .toggle.button {
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
&.repositories .repos {
|
||||
|
@ -41,6 +34,18 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&.organizations .orgs {
|
||||
&.non-empty {
|
||||
padding: 0;
|
||||
}
|
||||
.item {
|
||||
padding: 10px;
|
||||
.button {
|
||||
margin-top: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.profile {
|
||||
|
|
|
@ -23,9 +23,9 @@ import (
|
|||
|
||||
"github.com/gogits/gogs/models"
|
||||
"github.com/gogits/gogs/models/errors"
|
||||
"github.com/gogits/gogs/pkg/tool"
|
||||
"github.com/gogits/gogs/pkg/context"
|
||||
"github.com/gogits/gogs/pkg/setting"
|
||||
"github.com/gogits/gogs/pkg/tool"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -114,7 +114,6 @@ func HTTPContexter() macaron.Handler {
|
|||
|
||||
authUser, err := models.UserSignIn(authUsername, authPassword)
|
||||
if err != nil && !errors.IsUserNotExist(err) {
|
||||
|
||||
c.Handle(http.StatusInternalServerError, "UserSignIn", err)
|
||||
return
|
||||
}
|
||||
|
@ -139,6 +138,10 @@ func HTTPContexter() macaron.Handler {
|
|||
c.Handle(http.StatusInternalServerError, "GetUserByID", err)
|
||||
return
|
||||
}
|
||||
} else if authUser.IsEnabledTwoFactor() {
|
||||
askCredentials(c, http.StatusUnauthorized, `User with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password
|
||||
Please create and use personal access token on user settings page`)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("HTTPGit - Authenticated user: %s", authUser.Name)
|
||||
|
@ -152,7 +155,7 @@ func HTTPContexter() macaron.Handler {
|
|||
c.Handle(http.StatusInternalServerError, "HasAccess", err)
|
||||
return
|
||||
} else if !has {
|
||||
askCredentials(c, http.StatusUnauthorized, "User permission denied")
|
||||
askCredentials(c, http.StatusForbidden, "User permission denied")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -20,20 +20,22 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
SIGNIN = "user/auth/signin"
|
||||
SIGNUP = "user/auth/signup"
|
||||
ACTIVATE = "user/auth/activate"
|
||||
FORGOT_PASSWORD = "user/auth/forgot_passwd"
|
||||
RESET_PASSWORD = "user/auth/reset_passwd"
|
||||
LOGIN = "user/auth/login"
|
||||
TWO_FACTOR = "user/auth/two_factor"
|
||||
TWO_FACTOR_RECOVERY_CODE = "user/auth/two_factor_recovery_code"
|
||||
SIGNUP = "user/auth/signup"
|
||||
ACTIVATE = "user/auth/activate"
|
||||
FORGOT_PASSWORD = "user/auth/forgot_passwd"
|
||||
RESET_PASSWORD = "user/auth/reset_passwd"
|
||||
)
|
||||
|
||||
// AutoSignIn reads cookie and try to auto-login.
|
||||
func AutoSignIn(ctx *context.Context) (bool, error) {
|
||||
// AutoLogin reads cookie and try to auto-login.
|
||||
func AutoLogin(c *context.Context) (bool, error) {
|
||||
if !models.HasEngine {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
uname := ctx.GetCookie(setting.CookieUserName)
|
||||
uname := c.GetCookie(setting.CookieUserName)
|
||||
if len(uname) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
@ -42,9 +44,9 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
|
|||
defer func() {
|
||||
if !isSucceed {
|
||||
log.Trace("auto-login cookie cleared: %s", uname)
|
||||
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl)
|
||||
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl)
|
||||
ctx.SetCookie(setting.LoginStatusCookieName, "", -1, setting.AppSubUrl)
|
||||
c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl)
|
||||
c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl)
|
||||
c.SetCookie(setting.LoginStatusCookieName, "", -1, setting.AppSubUrl)
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -56,16 +58,16 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
if val, ok := ctx.GetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName); !ok || val != u.Name {
|
||||
if val, ok := c.GetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName); !ok || val != u.Name {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
isSucceed = true
|
||||
ctx.Session.Set("uid", u.ID)
|
||||
ctx.Session.Set("uname", u.Name)
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubUrl)
|
||||
c.Session.Set("uid", u.ID)
|
||||
c.Session.Set("uname", u.Name)
|
||||
c.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubUrl)
|
||||
if setting.EnableLoginStatusCookie {
|
||||
ctx.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubUrl)
|
||||
c.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubUrl)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
@ -77,77 +79,165 @@ func isValidRedirect(url string) bool {
|
|||
return len(url) >= 2 && url[0] == '/' && url[1] != '/'
|
||||
}
|
||||
|
||||
func SignIn(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||
func Login(c *context.Context) {
|
||||
c.Data["Title"] = c.Tr("sign_in")
|
||||
|
||||
// Check auto-login.
|
||||
isSucceed, err := AutoSignIn(ctx)
|
||||
isSucceed, err := AutoLogin(c)
|
||||
if err != nil {
|
||||
ctx.Handle(500, "AutoSignIn", err)
|
||||
c.Handle(500, "AutoLogin", err)
|
||||
return
|
||||
}
|
||||
|
||||
redirectTo := ctx.Query("redirect_to")
|
||||
redirectTo := c.Query("redirect_to")
|
||||
if len(redirectTo) > 0 {
|
||||
ctx.SetCookie("redirect_to", redirectTo, 0, setting.AppSubUrl)
|
||||
c.SetCookie("redirect_to", redirectTo, 0, setting.AppSubUrl)
|
||||
} else {
|
||||
redirectTo, _ = url.QueryUnescape(ctx.GetCookie("redirect_to"))
|
||||
redirectTo, _ = url.QueryUnescape(c.GetCookie("redirect_to"))
|
||||
}
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl)
|
||||
c.SetCookie("redirect_to", "", -1, setting.AppSubUrl)
|
||||
|
||||
if isSucceed {
|
||||
if isValidRedirect(redirectTo) {
|
||||
ctx.Redirect(redirectTo)
|
||||
c.Redirect(redirectTo)
|
||||
} else {
|
||||
ctx.Redirect(setting.AppSubUrl + "/")
|
||||
c.Redirect(setting.AppSubUrl + "/")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(200, SIGNIN)
|
||||
c.HTML(200, LOGIN)
|
||||
}
|
||||
|
||||
func SignInPost(ctx *context.Context, f form.SignIn) {
|
||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||
func afterLogin(c *context.Context, u *models.User, remember bool) {
|
||||
if remember {
|
||||
days := 86400 * setting.LoginRememberDays
|
||||
c.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubUrl, "", setting.CookieSecure, true)
|
||||
c.SetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName, u.Name, days, setting.AppSubUrl, "", setting.CookieSecure, true)
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(200, SIGNIN)
|
||||
c.Session.Set("uid", u.ID)
|
||||
c.Session.Set("uname", u.Name)
|
||||
c.Session.Delete("twoFactorRemember")
|
||||
c.Session.Delete("twoFactorUserID")
|
||||
|
||||
// Clear whatever CSRF has right now, force to generate a new one
|
||||
c.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubUrl)
|
||||
if setting.EnableLoginStatusCookie {
|
||||
c.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubUrl)
|
||||
}
|
||||
|
||||
redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to"))
|
||||
c.SetCookie("redirect_to", "", -1, setting.AppSubUrl)
|
||||
if isValidRedirect(redirectTo) {
|
||||
c.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(setting.AppSubUrl + "/")
|
||||
}
|
||||
|
||||
func LoginPost(c *context.Context, f form.SignIn) {
|
||||
c.Data["Title"] = c.Tr("sign_in")
|
||||
|
||||
if c.HasError() {
|
||||
c.Success(LOGIN)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := models.UserSignIn(f.UserName, f.Password)
|
||||
if err != nil {
|
||||
if errors.IsUserNotExist(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), SIGNIN, &f)
|
||||
c.RenderWithErr(c.Tr("form.username_password_incorrect"), LOGIN, &f)
|
||||
} else {
|
||||
ctx.Handle(500, "UserSignIn", err)
|
||||
c.ServerError("UserSignIn", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if f.Remember {
|
||||
days := 86400 * setting.LoginRememberDays
|
||||
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubUrl, "", setting.CookieSecure, true)
|
||||
ctx.SetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName, u.Name, days, setting.AppSubUrl, "", setting.CookieSecure, true)
|
||||
}
|
||||
|
||||
ctx.Session.Set("uid", u.ID)
|
||||
ctx.Session.Set("uname", u.Name)
|
||||
|
||||
// Clear whatever CSRF has right now, force to generate a new one
|
||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubUrl)
|
||||
if setting.EnableLoginStatusCookie {
|
||||
ctx.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubUrl)
|
||||
}
|
||||
|
||||
redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to"))
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl)
|
||||
if isValidRedirect(redirectTo) {
|
||||
ctx.Redirect(redirectTo)
|
||||
if !u.IsEnabledTwoFactor() {
|
||||
afterLogin(c, u, f.Remember)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubUrl + "/")
|
||||
c.Session.Set("twoFactorRemember", f.Remember)
|
||||
c.Session.Set("twoFactorUserID", u.ID)
|
||||
c.Redirect(setting.AppSubUrl + "/user/login/two_factor")
|
||||
}
|
||||
|
||||
func LoginTwoFactor(c *context.Context) {
|
||||
_, ok := c.Session.Get("twoFactorUserID").(int64)
|
||||
if !ok {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
c.Success(TWO_FACTOR)
|
||||
}
|
||||
|
||||
func LoginTwoFactorPost(c *context.Context) {
|
||||
userID, ok := c.Session.Get("twoFactorUserID").(int64)
|
||||
if !ok {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
t, err := models.GetTwoFactorByUserID(userID)
|
||||
if err != nil {
|
||||
c.ServerError("GetTwoFactorByUserID", err)
|
||||
return
|
||||
}
|
||||
valid, err := t.ValidateTOTP(c.Query("passcode"))
|
||||
if err != nil {
|
||||
c.ServerError("ValidateTOTP", err)
|
||||
return
|
||||
} else if !valid {
|
||||
c.Flash.Error(c.Tr("settings.two_factor_invalid_passcode"))
|
||||
c.Redirect(setting.AppSubUrl + "/user/login/two_factor")
|
||||
return
|
||||
}
|
||||
|
||||
u, err := models.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
afterLogin(c, u, c.Session.Get("twoFactorRemember").(bool))
|
||||
}
|
||||
|
||||
func LoginTwoFactorRecoveryCode(c *context.Context) {
|
||||
_, ok := c.Session.Get("twoFactorUserID").(int64)
|
||||
if !ok {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
c.Success(TWO_FACTOR_RECOVERY_CODE)
|
||||
}
|
||||
|
||||
func LoginTwoFactorRecoveryCodePost(c *context.Context) {
|
||||
userID, ok := c.Session.Get("twoFactorUserID").(int64)
|
||||
if !ok {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.UseRecoveryCode(userID, c.Query("recovery_code")); err != nil {
|
||||
if errors.IsTwoFactorRecoveryCodeNotFound(err) {
|
||||
c.Flash.Error(c.Tr("auth.login_two_factor_invalid_recovery_code"))
|
||||
c.Redirect(setting.AppSubUrl + "/user/login/two_factor_recovery_code")
|
||||
} else {
|
||||
c.ServerError("UseRecoveryCode", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
u, err := models.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
afterLogin(c, u, c.Session.Get("twoFactorRemember").(bool))
|
||||
}
|
||||
|
||||
func SignOut(ctx *context.Context) {
|
||||
|
|
|
@ -5,11 +5,17 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
log "gopkg.in/clog.v1"
|
||||
|
||||
"github.com/gogits/gogs/models"
|
||||
|
@ -22,17 +28,19 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
SETTINGS_PROFILE = "user/settings/profile"
|
||||
SETTINGS_AVATAR = "user/settings/avatar"
|
||||
SETTINGS_PASSWORD = "user/settings/password"
|
||||
SETTINGS_EMAILS = "user/settings/email"
|
||||
SETTINGS_SSH_KEYS = "user/settings/sshkeys"
|
||||
SETTINGS_SECURITY = "user/settings/security"
|
||||
SETTINGS_REPOSITORIES = "user/settings/repositories"
|
||||
SETTINGS_ORGANIZATIONS = "user/settings/organizations"
|
||||
SETTINGS_APPLICATIONS = "user/settings/applications"
|
||||
SETTINGS_DELETE = "user/settings/delete"
|
||||
NOTIFICATION = "user/notification"
|
||||
SETTINGS_PROFILE = "user/settings/profile"
|
||||
SETTINGS_AVATAR = "user/settings/avatar"
|
||||
SETTINGS_PASSWORD = "user/settings/password"
|
||||
SETTINGS_EMAILS = "user/settings/email"
|
||||
SETTINGS_SSH_KEYS = "user/settings/sshkeys"
|
||||
SETTINGS_SECURITY = "user/settings/security"
|
||||
SETTINGS_TWO_FACTOR_ENABLE = "user/settings/two_factor_enable"
|
||||
SETTINGS_TWO_FACTOR_RECOVERY_CODES = "user/settings/two_factor_recovery_codes"
|
||||
SETTINGS_REPOSITORIES = "user/settings/repositories"
|
||||
SETTINGS_ORGANIZATIONS = "user/settings/organizations"
|
||||
SETTINGS_APPLICATIONS = "user/settings/applications"
|
||||
SETTINGS_DELETE = "user/settings/delete"
|
||||
NOTIFICATION = "user/notification"
|
||||
)
|
||||
|
||||
func Settings(c *context.Context) {
|
||||
|
@ -376,6 +384,141 @@ func DeleteSSHKey(ctx *context.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
func SettingsSecurity(c *context.Context) {
|
||||
c.Data["Title"] = c.Tr("settings")
|
||||
c.Data["PageIsSettingsSecurity"] = true
|
||||
|
||||
t, err := models.GetTwoFactorByUserID(c.UserID())
|
||||
if err != nil && !errors.IsTwoFactorNotFound(err) {
|
||||
c.ServerError("GetTwoFactorByUserID", err)
|
||||
return
|
||||
}
|
||||
c.Data["TwoFactor"] = t
|
||||
|
||||
c.Success(SETTINGS_SECURITY)
|
||||
}
|
||||
|
||||
func SettingsTwoFactorEnable(c *context.Context) {
|
||||
if c.User.IsEnabledTwoFactor() {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["Title"] = c.Tr("settings")
|
||||
c.Data["PageIsSettingsSecurity"] = true
|
||||
|
||||
var key *otp.Key
|
||||
var err error
|
||||
keyURL := c.Session.Get("twoFactorURL")
|
||||
if keyURL != nil {
|
||||
key, _ = otp.NewKeyFromURL(keyURL.(string))
|
||||
}
|
||||
if key == nil {
|
||||
key, err = totp.Generate(totp.GenerateOpts{
|
||||
Issuer: setting.AppName,
|
||||
AccountName: c.User.Email,
|
||||
})
|
||||
if err != nil {
|
||||
c.ServerError("Generate", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Data["TwoFactorSecret"] = key.Secret()
|
||||
|
||||
img, err := key.Image(240, 240)
|
||||
if err != nil {
|
||||
c.ServerError("Image", err)
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err = png.Encode(&buf, img); err != nil {
|
||||
c.ServerError("Encode", err)
|
||||
return
|
||||
}
|
||||
c.Data["QRCode"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||
|
||||
c.Session.Set("twoFactorSecret", c.Data["TwoFactorSecret"])
|
||||
c.Session.Set("twoFactorURL", key.String())
|
||||
c.Success(SETTINGS_TWO_FACTOR_ENABLE)
|
||||
}
|
||||
|
||||
func SettingsTwoFactorEnablePost(c *context.Context) {
|
||||
secret, ok := c.Session.Get("twoFactorSecret").(string)
|
||||
if !ok {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !totp.Validate(c.Query("passcode"), secret) {
|
||||
c.Flash.Error(c.Tr("settings.two_factor_invalid_passcode"))
|
||||
c.Redirect(setting.AppSubUrl + "/user/settings/security/two_factor_enable")
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.NewTwoFactor(c.UserID(), secret); err != nil {
|
||||
c.Flash.Error(c.Tr("settings.two_factor_enable_error", err))
|
||||
c.Redirect(setting.AppSubUrl + "/user/settings/security/two_factor_enable")
|
||||
return
|
||||
}
|
||||
|
||||
c.Session.Delete("twoFactorSecret")
|
||||
c.Session.Delete("twoFactorURL")
|
||||
c.Flash.Success(c.Tr("settings.two_factor_enable_success"))
|
||||
c.Redirect(setting.AppSubUrl + "/user/settings/security/two_factor_recovery_codes")
|
||||
}
|
||||
|
||||
func SettingsTwoFactorRecoveryCodes(c *context.Context) {
|
||||
if !c.User.IsEnabledTwoFactor() {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["Title"] = c.Tr("settings")
|
||||
c.Data["PageIsSettingsSecurity"] = true
|
||||
|
||||
recoveryCodes, err := models.GetRecoveryCodesByUserID(c.UserID())
|
||||
if err != nil {
|
||||
c.ServerError("GetRecoveryCodesByUserID", err)
|
||||
return
|
||||
}
|
||||
c.Data["RecoveryCodes"] = recoveryCodes
|
||||
|
||||
c.Success(SETTINGS_TWO_FACTOR_RECOVERY_CODES)
|
||||
}
|
||||
|
||||
func SettingsTwoFactorRecoveryCodesPost(c *context.Context) {
|
||||
if !c.User.IsEnabledTwoFactor() {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.RegenerateRecoveryCodes(c.UserID()); err != nil {
|
||||
c.Flash.Error(c.Tr("settings.two_factor_regenerate_recovery_codes_error", err))
|
||||
} else {
|
||||
c.Flash.Success(c.Tr("settings.two_factor_regenerate_recovery_codes_success"))
|
||||
}
|
||||
|
||||
c.Redirect(setting.AppSubUrl + "/user/settings/security/two_factor_recovery_codes")
|
||||
}
|
||||
|
||||
func SettingsTwoFactorDisable(c *context.Context) {
|
||||
if !c.User.IsEnabledTwoFactor() {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.DeleteTwoFactor(c.UserID()); err != nil {
|
||||
c.ServerError("DeleteTwoFactor", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Flash.Success(c.Tr("settings.two_factor_disable_success"))
|
||||
c.JSONSuccess(map[string]interface{}{
|
||||
"redirect": setting.AppSubUrl + "/user/settings/security",
|
||||
})
|
||||
}
|
||||
|
||||
func SettingsApplications(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.11.4.0405
|
||||
0.11.5.0406
|
|
@ -0,0 +1,28 @@
|
|||
{{template "base/head" .}}
|
||||
<div class="user signin two-factor">
|
||||
<div class="ui middle very relaxed page grid">
|
||||
<div class="column">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<h3 class="ui top attached center header">
|
||||
{{.i18n.Tr "auth.login_two_factor"}}
|
||||
</h3>
|
||||
<div class="ui attached segment">
|
||||
{{template "base/alert" .}}
|
||||
<div class="required field">
|
||||
<label for="passcode">{{.i18n.Tr "auth.login_two_factor_passcode"}}</label>
|
||||
<div class="ui fluid input">
|
||||
<input id="passcode" name="passcode" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="ui fluid green button">{{.i18n.Tr "settings.two_factor_verify"}}</button>
|
||||
</div>
|
||||
<p>
|
||||
<a href="{{AppSubUrl}}/user/login/two_factor_recovery_code">{{.i18n.Tr "auth.login_two_factor_enter_recovery_code"}}</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
|
@ -0,0 +1,28 @@
|
|||
{{template "base/head" .}}
|
||||
<div class="user signin two-factor">
|
||||
<div class="ui middle very relaxed page grid">
|
||||
<div class="column">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<h3 class="ui top attached center header">
|
||||
{{.i18n.Tr "auth.login_two_factor_recovery"}}
|
||||
</h3>
|
||||
<div class="ui attached segment">
|
||||
{{template "base/alert" .}}
|
||||
<div class="required field">
|
||||
<label for="recovery_code">{{.i18n.Tr "auth.login_two_factor_recovery_code"}}</label>
|
||||
<div class="ui fluid input">
|
||||
<input id="recovery_code" name="recovery_code" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="ui fluid green button">{{.i18n.Tr "settings.two_factor_verify"}}</button>
|
||||
</div>
|
||||
<p>
|
||||
<a href="{{AppSubUrl}}/user/login/two_factor">{{.i18n.Tr "auth.login_two_factor_enter_passcode"}}</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
|
@ -16,6 +16,9 @@
|
|||
<a class="{{if .PageIsSettingsSSHKeys}}active{{end}} item" href="{{AppSubUrl}}/user/settings/ssh">
|
||||
{{.i18n.Tr "settings.ssh_keys"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsSecurity}}active{{end}} item" href="{{AppSubUrl}}/user/settings/security">
|
||||
{{.i18n.Tr "settings.security"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsRepositories}}active{{end}} item" href="{{AppSubUrl}}/user/settings/repositories">
|
||||
{{.i18n.Tr "settings.repos"}}
|
||||
</a>
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
{{template "base/head" .}}
|
||||
<div class="user settings security">
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
{{template "user/settings/navbar" .}}
|
||||
<div class="twelve wide column content">
|
||||
{{template "base/alert" .}}
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.two_factor"}}
|
||||
</h4>
|
||||
<div class="ui attached segment two-factor">
|
||||
<p class="text bold">
|
||||
{{.i18n.Tr "settings.two_factor_status"}}
|
||||
{{if .TwoFactor}}
|
||||
<span class="text green">{{.i18n.Tr "settings.two_factor_on"}} <i class="octicon octicon-check"></i></span>
|
||||
<button class="ui right mini red toggle button delete-button" data-url="{{$.Link}}/two_factor_disable">{{.i18n.Tr "settings.two_factor_disable"}}</button>
|
||||
{{else}}
|
||||
<span class="text red">{{.i18n.Tr "settings.two_factor_off"}} <i class="octicon octicon-x"></i></span>
|
||||
<a class="ui right mini green toggle button" href="{{AppSubUrl}}/user/settings/security/two_factor_enable">{{.i18n.Tr "settings.two_factor_enable"}}</a>
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
{{if .TwoFactor}}
|
||||
<br>
|
||||
<p>{{.i18n.Tr "settings.two_factor_view_recovery_codes" AppSubUrl "/user/settings/security/two_factor_recovery_codes" | Safe}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui small basic delete modal">
|
||||
<div class="ui icon header">
|
||||
<i class="trash icon"></i>
|
||||
{{.i18n.Tr "settings.two_factor_disable_title"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{.i18n.Tr "settings.two_factor_disable_desc"}}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui red basic inverted cancel button">
|
||||
<i class="remove icon"></i>
|
||||
{{.i18n.Tr "modal.no"}}
|
||||
</div>
|
||||
<div class="ui green basic inverted ok button">
|
||||
<i class="checkmark icon"></i>
|
||||
{{.i18n.Tr "modal.yes"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
|
@ -0,0 +1,28 @@
|
|||
{{template "base/head" .}}
|
||||
<div class="user settings security two-factor">
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
{{template "user/settings/navbar" .}}
|
||||
<div class="twelve wide column content">
|
||||
{{template "base/alert" .}}
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.two_factor_enable_title"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<div>{{.i18n.Tr "settings.two_factor_scan_qr"}}</div>
|
||||
<img src="{{.QRCode}}" alt="{{.TwoFactorSecret}}">
|
||||
<p>{{.i18n.Tr "settings.two_factor_or_enter_secret"}} <b>{{.TwoFactorSecret}}</b></p>
|
||||
<form class="ui form" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="required inline field">
|
||||
<span>{{.i18n.Tr "settings.two_factor_then_enter_passcode"}}</span>
|
||||
<input class="ui input" name="passcode" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<button class="ui green button">{{.i18n.Tr "settings.two_factor_verify"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
|
@ -0,0 +1,36 @@
|
|||
{{template "base/head" .}}
|
||||
<div class="user settings security two-factor">
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
{{template "user/settings/navbar" .}}
|
||||
<div class="twelve wide column content">
|
||||
{{template "base/alert" .}}
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "settings.two_factor_recovery_codes_title"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>{{.i18n.Tr "settings.two_factor_recovery_codes_desc" | Safe}}</p>
|
||||
<ul class="ui list">
|
||||
{{range .RecoveryCodes}}
|
||||
<li class="item">
|
||||
<code>
|
||||
{{if .IsUsed}}
|
||||
<del>{{.Code}}</del>
|
||||
{{else}}
|
||||
{{.Code}}
|
||||
{{end}}
|
||||
</code>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<form class="ui form" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button class="ui blue button">{{.i18n.Tr "settings.two_factor_regenerate_recovery_codes"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
Loading…
Reference in New Issue