Browse Source

Can register, login, logout

Dashie der otter 1 year ago
parent
commit
1c432aa610
Signed by: Dashie <dashie@sigpipe.me> GPG Key ID: C2D57B325840B755

+ 5
- 1
.bra.toml View File

@@ -6,7 +6,11 @@ init_cmds = [
6 6
 watch_all = true
7 7
 watch_dirs = [
8 8
 	"$WORKDIR/cmd",
9
-	"$WORKDIR/routers"
9
+	"$WORKDIR/routers",
10
+    "$WORKDIR/context",
11
+    "$WORKDIR/stuff",
12
+    "$WORKDIR/models",
13
+    "$WORKDIR/conf/locale",
10 14
 ]
11 15
 watch_exts = [".go"]
12 16
 ignore_files = [".+_test.go"]

+ 4
- 4
bindata/bindata.go
File diff suppressed because it is too large
View File


+ 13
- 4
cmd/web.go View File

@@ -8,12 +8,13 @@ import (
8 8
 	"dev.sigpipe.me/dashie/git.txt/routers"
9 9
 	"dev.sigpipe.me/dashie/git.txt/bindata"
10 10
 	"dev.sigpipe.me/dashie/git.txt/stuff/template"
11
+	"dev.sigpipe.me/dashie/git.txt/stuff/form"
11 12
 	"path"
12 13
 	"github.com/go-macaron/session"
13 14
 	"github.com/go-macaron/csrf"
14 15
 	"github.com/go-macaron/cache"
15 16
 	"github.com/go-macaron/i18n"
16
-	//"github.com/go-macaron/binding"
17
+	"github.com/go-macaron/binding"
17 18
 	"strings"
18 19
 	"fmt"
19 20
 	log "gopkg.in/clog.v1"
@@ -120,16 +121,24 @@ func runWeb(ctx *cli.Context) error {
120 121
 
121 122
 	m := newMacaron()
122 123
 
123
-	//bindIgnErr := binding.BindIgnErr
124
+	reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
125
+
126
+	bindIgnErr := binding.BindIgnErr
124 127
 
125 128
 	m.Get("/", routers.Home)
126 129
 
127 130
 	m.Group("/user", func() {
128 131
 		m.Group("/login", func() {
129
-			m.Combo("").Get(user.Login)
132
+			m.Combo("").Get(user.Login).Post(bindIgnErr(form.Login{}), user.LoginPost)
130 133
 		})
134
+		m.Get("/register", user.Register)
135
+		m.Post("/register", bindIgnErr(form.Register{}), user.RegisterPost)
136
+	}, reqSignOut)
137
+
138
+	m.Group("/user", func() {
139
+		m.Get("/logout", user.Logout)
131 140
 	})
132
-	m.Get("/register", user.Register)
141
+
133 142
 
134 143
 	// robots.txt
135 144
 	m.Get("/robots.txt", func(ctx *context.Context) {

+ 9
- 0
conf/app.ini View File

@@ -103,6 +103,15 @@ CSRF_COOKIE_NAME = _csrf
103 103
 [security]
104 104
 ; !!CHANGE THIS TO KEEP YOUR USER DATA SAFE!!
105 105
 SECRET_KEY = !#@FDEWREWR&*(
106
+INSTALL_LOCK = false
107
+; Auto-login remember days
108
+LOGIN_REMEMBER_DAYS = 7
109
+COOKIE_USERNAME = gitxt_celestia
110
+COOKIE_REMEMBER_NAME = gitxt_luna
111
+COOKIE_SECURE = false
112
+; Enable to set cookie to indicate user login status
113
+ENABLE_LOGIN_STATUS_COOKIE = false
114
+LOGIN_STATUS_COOKIE_NAME = login_status
106 115
 
107 116
 [i18n]
108 117
 LANGS = en-US,fr-FR

+ 15
- 3
conf/locale/locale_en-US.ini View File

@@ -1,11 +1,23 @@
1 1
 [login]
2 2
 title = "Sign In"
3 3
 sign_in = "Sign In"
4
+username = "Username"
5
+username_placeholder = "Your username."
4 6
 email = "Email"
5
-email_placeholder = "Email address..."
7
+email_placeholder = "Your email."
6 8
 password = "Password"
7
-password_placeholder = "Password..."
9
+password_placeholder = "Your nice password."
10
+repeat_password_placeholder = "And the same one here."
8 11
 remember_me = "Remember me"
9 12
 
10 13
 [register]
11
-title = "Register"
14
+title = "Register"
15
+register = "Register"
16
+not_allowed = "Registration not allowed"
17
+
18
+[form]
19
+password_not_match = "Passwords doesn't match"
20
+username_password_incorrect = "Invalid username or password"
21
+username_been_taken = "Username already taken, sorry"
22
+username_reserved = "Username reserved, please choose another username"
23
+username_pattern_not_allowed = "Invalid username pattern"

+ 54
- 1
context/context.go View File

@@ -15,6 +15,8 @@ import (
15 15
 	"github.com/go-macaron/i18n"
16 16
 	"html/template"
17 17
 	"dev.sigpipe.me/dashie/git.txt/models"
18
+	"dev.sigpipe.me/dashie/git.txt/stuff/form"
19
+	"dev.sigpipe.me/dashie/git.txt/stuff/auth"
18 20
 )
19 21
 
20 22
 // Context represents context of a request.
@@ -42,6 +44,36 @@ func (ctx *Context) HTML(status int, name string) {
42 44
 	ctx.Context.HTML(status, name)
43 45
 }
44 46
 
47
+// Success responses template with status http.StatusOK.
48
+func (c *Context) Success(name string) {
49
+	c.HTML(http.StatusOK, name)
50
+}
51
+
52
+// JSONSuccess responses JSON with status http.StatusOK.
53
+func (c *Context) JSONSuccess(data interface{}) {
54
+	c.JSON(http.StatusOK, data)
55
+}
56
+
57
+// HasError returns true if error occurs in form validation.
58
+func (ctx *Context) HasError() bool {
59
+	hasErr, ok := ctx.Data["HasError"]
60
+	if !ok {
61
+		return false
62
+	}
63
+	ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string)
64
+	ctx.Data["Flash"] = ctx.Flash
65
+	return hasErr.(bool)
66
+}
67
+
68
+// RenderWithErr used for page has form validation but need to prompt error to users.
69
+func (ctx *Context) RenderWithErr(msg, tpl string, f interface{}) {
70
+	if f != nil {
71
+		form.Assign(f, ctx.Data)
72
+	}
73
+	ctx.Flash.ErrorMsg = msg
74
+	ctx.Data["Flash"] = ctx.Flash
75
+	ctx.HTML(http.StatusOK, tpl)
76
+}
45 77
 
46 78
 // Handle handles and logs error by given status.
47 79
 func (ctx *Context) Handle(status int, title string, err error) {
@@ -55,6 +87,27 @@ func (ctx *Context) Handle(status int, title string, err error) {
55 87
 	ctx.HTML(status, fmt.Sprintf("status/%d", status))
56 88
 }
57 89
 
90
+// NotFound renders the 404 page.
91
+func (ctx *Context) NotFound() {
92
+	ctx.Handle(http.StatusNotFound, "", nil)
93
+}
94
+
95
+// ServerError renders the 500 page.
96
+func (c *Context) ServerError(title string, err error) {
97
+	c.Handle(http.StatusInternalServerError, title, err)
98
+}
99
+
100
+// NotFoundOrServerError use error check function to determine if the error
101
+// is about not found. It responses with 404 status code for not found error,
102
+// or error context description for logging purpose of 500 server error.
103
+func (c *Context) NotFoundOrServerError(title string, errck func(error) bool, err error) {
104
+	if errck(err) {
105
+		c.NotFound()
106
+		return
107
+	}
108
+	c.ServerError(title, err)
109
+}
110
+
58 111
 func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
59 112
 	modtime := time.Now()
60 113
 	for _, p := range params {
@@ -97,7 +150,7 @@ func Contexter() macaron.Handler {
97 150
 		ctx.Data["PageStartTime"] = time.Now()
98 151
 
99 152
 		// Get user from session if logined.
100
-		// ctx.User, ctx.IsBasicAuth = auth.SignedInUser(ctx.Context, ctx.Session)
153
+		ctx.User, ctx.IsBasicAuth = auth.SignedInUser(ctx.Context, ctx.Session)
101 154
 
102 155
 		if ctx.User != nil {
103 156
 			ctx.IsLogged = true

+ 201
- 0
models/error.go View File

@@ -0,0 +1,201 @@
1
+// Copyright 2015 The Gogs Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package models
6
+
7
+import (
8
+	"fmt"
9
+)
10
+
11
+type ErrNameReserved struct {
12
+	Name string
13
+}
14
+
15
+func IsErrNameReserved(err error) bool {
16
+	_, ok := err.(ErrNameReserved)
17
+	return ok
18
+}
19
+
20
+func (err ErrNameReserved) Error() string {
21
+	return fmt.Sprintf("name is reserved [name: %s]", err.Name)
22
+}
23
+
24
+type ErrNamePatternNotAllowed struct {
25
+	Pattern string
26
+}
27
+
28
+func IsErrNamePatternNotAllowed(err error) bool {
29
+	_, ok := err.(ErrNamePatternNotAllowed)
30
+	return ok
31
+}
32
+
33
+func (err ErrNamePatternNotAllowed) Error() string {
34
+	return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern)
35
+}
36
+
37
+//  ____ ___
38
+// |    |   \______ ___________
39
+// |    |   /  ___// __ \_  __ \
40
+// |    |  /\___ \\  ___/|  | \/
41
+// |______//____  >\___  >__|
42
+//              \/     \/
43
+
44
+type ErrUserAlreadyExist struct {
45
+	Name string
46
+}
47
+
48
+func IsErrUserAlreadyExist(err error) bool {
49
+	_, ok := err.(ErrUserAlreadyExist)
50
+	return ok
51
+}
52
+
53
+func (err ErrUserAlreadyExist) Error() string {
54
+	return fmt.Sprintf("user already exists [name: %s]", err.Name)
55
+}
56
+
57
+type ErrEmailAlreadyUsed struct {
58
+	Email string
59
+}
60
+
61
+func IsErrEmailAlreadyUsed(err error) bool {
62
+	_, ok := err.(ErrEmailAlreadyUsed)
63
+	return ok
64
+}
65
+
66
+func (err ErrEmailAlreadyUsed) Error() string {
67
+	return fmt.Sprintf("e-mail has been used [email: %s]", err.Email)
68
+}
69
+
70
+type ErrUserOwnRepos struct {
71
+	UID int64
72
+}
73
+
74
+func IsErrUserOwnRepos(err error) bool {
75
+	_, ok := err.(ErrUserOwnRepos)
76
+	return ok
77
+}
78
+
79
+func (err ErrUserOwnRepos) Error() string {
80
+	return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
81
+}
82
+
83
+// __________     ___.   .__  .__          ____  __.
84
+// \______   \__ _\_ |__ |  | |__| ____   |    |/ _|____ ___.__.
85
+//  |     ___/  |  \ __ \|  | |  |/ ___\  |      <_/ __ <   |  |
86
+//  |    |   |  |  / \_\ \  |_|  \  \___  |    |  \  ___/\___  |
87
+//  |____|   |____/|___  /____/__|\___  > |____|__ \___  > ____|
88
+//                     \/             \/          \/   \/\/
89
+
90
+type ErrKeyUnableVerify struct {
91
+	Result string
92
+}
93
+
94
+func IsErrKeyUnableVerify(err error) bool {
95
+	_, ok := err.(ErrKeyUnableVerify)
96
+	return ok
97
+}
98
+
99
+func (err ErrKeyUnableVerify) Error() string {
100
+	return fmt.Sprintf("Unable to verify key content [result: %s]", err.Result)
101
+}
102
+
103
+type ErrKeyNotExist struct {
104
+	ID int64
105
+}
106
+
107
+func IsErrKeyNotExist(err error) bool {
108
+	_, ok := err.(ErrKeyNotExist)
109
+	return ok
110
+}
111
+
112
+func (err ErrKeyNotExist) Error() string {
113
+	return fmt.Sprintf("public key does not exist [id: %d]", err.ID)
114
+}
115
+
116
+type ErrKeyAlreadyExist struct {
117
+	OwnerID int64
118
+	Content string
119
+}
120
+
121
+func IsErrKeyAlreadyExist(err error) bool {
122
+	_, ok := err.(ErrKeyAlreadyExist)
123
+	return ok
124
+}
125
+
126
+func (err ErrKeyAlreadyExist) Error() string {
127
+	return fmt.Sprintf("public key already exists [owner_id: %d, content: %s]", err.OwnerID, err.Content)
128
+}
129
+
130
+type ErrKeyNameAlreadyUsed struct {
131
+	OwnerID int64
132
+	Name    string
133
+}
134
+
135
+func IsErrKeyNameAlreadyUsed(err error) bool {
136
+	_, ok := err.(ErrKeyNameAlreadyUsed)
137
+	return ok
138
+}
139
+
140
+func (err ErrKeyNameAlreadyUsed) Error() string {
141
+	return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
142
+}
143
+
144
+type ErrKeyAccessDenied struct {
145
+	UserID int64
146
+	KeyID  int64
147
+	Note   string
148
+}
149
+
150
+func IsErrKeyAccessDenied(err error) bool {
151
+	_, ok := err.(ErrKeyAccessDenied)
152
+	return ok
153
+}
154
+
155
+func (err ErrKeyAccessDenied) Error() string {
156
+	return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d, note: %s]",
157
+		err.UserID, err.KeyID, err.Note)
158
+}
159
+
160
+type ErrDeployKeyNotExist struct {
161
+	ID     int64
162
+	KeyID  int64
163
+	RepoID int64
164
+}
165
+
166
+func IsErrDeployKeyNotExist(err error) bool {
167
+	_, ok := err.(ErrDeployKeyNotExist)
168
+	return ok
169
+}
170
+
171
+func (err ErrDeployKeyNotExist) Error() string {
172
+	return fmt.Sprintf("Deploy key does not exist [id: %d, key_id: %d, repo_id: %d]", err.ID, err.KeyID, err.RepoID)
173
+}
174
+
175
+type ErrDeployKeyAlreadyExist struct {
176
+	KeyID  int64
177
+	RepoID int64
178
+}
179
+
180
+func IsErrDeployKeyAlreadyExist(err error) bool {
181
+	_, ok := err.(ErrDeployKeyAlreadyExist)
182
+	return ok
183
+}
184
+
185
+func (err ErrDeployKeyAlreadyExist) Error() string {
186
+	return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
187
+}
188
+
189
+type ErrDeployKeyNameAlreadyUsed struct {
190
+	RepoID int64
191
+	Name   string
192
+}
193
+
194
+func IsErrDeployKeyNameAlreadyUsed(err error) bool {
195
+	_, ok := err.(ErrDeployKeyNameAlreadyUsed)
196
+	return ok
197
+}
198
+
199
+func (err ErrDeployKeyNameAlreadyUsed) Error() string {
200
+	return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
201
+}

+ 12
- 0
models/errors/errors.go View File

@@ -0,0 +1,12 @@
1
+// Copyright 2017 The Gogs Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package errors
6
+
7
+import "errors"
8
+
9
+// New is a wrapper of real errors.New function.
10
+func New(text string) error {
11
+	return errors.New(text)
12
+}

+ 45
- 0
models/errors/user.go View File

@@ -0,0 +1,45 @@
1
+// Copyright 2017 The Gogs Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package errors
6
+
7
+import "fmt"
8
+
9
+type EmptyName struct{}
10
+
11
+func IsEmptyName(err error) bool {
12
+	_, ok := err.(EmptyName)
13
+	return ok
14
+}
15
+
16
+func (err EmptyName) Error() string {
17
+	return "empty name"
18
+}
19
+
20
+type UserNotExist struct {
21
+	UserID int64
22
+	Name   string
23
+}
24
+
25
+func IsUserNotExist(err error) bool {
26
+	_, ok := err.(UserNotExist)
27
+	return ok
28
+}
29
+
30
+func (err UserNotExist) Error() string {
31
+	return fmt.Sprintf("user does not exist [user_id: %d, name: %s]", err.UserID, err.Name)
32
+}
33
+
34
+type UserNotKeyOwner struct {
35
+	KeyID int64
36
+}
37
+
38
+func IsUserNotKeyOwner(err error) bool {
39
+	_, ok := err.(UserNotKeyOwner)
40
+	return ok
41
+}
42
+
43
+func (err UserNotKeyOwner) Error() string {
44
+	return fmt.Sprintf("user is not the owner of public key [key_id: %d]", err.KeyID)
45
+}

+ 186
- 1
models/user.go View File

@@ -2,14 +2,29 @@ package models
2 2
 
3 3
 import (
4 4
 	"time"
5
+	"strings"
6
+	"unicode/utf8"
7
+	"dev.sigpipe.me/dashie/git.txt/models/errors"
8
+	"golang.org/x/crypto/pbkdf2"
9
+	"crypto/sha256"
10
+	"fmt"
11
+	"crypto/subtle"
12
+	"dev.sigpipe.me/dashie/git.txt/stuff/tool"
13
+	"os"
14
+	"path/filepath"
15
+	"dev.sigpipe.me/dashie/git.txt/setting"
5 16
 )
6 17
 
7 18
 type User struct {
8 19
 	ID		int64	`xorm:"pk autoincr"`
9 20
 	UserName	string	`xorm:"UNIQUE NOT NULL"`
10
-	Password	string	`xorm:"NOT NULL"`
21
+	LowerName	string	`xorm:"UNIQUE NOT NULL"`
11 22
 	Email		string	`xorm:"NOT NULL"`
12 23
 
24
+	Password	string	`xorm:"NOT NULL"`
25
+	Rands		string	`xorm:"VARCHAR(10)"`
26
+	Salt		string	`xorm:"VARCHAR(10)"`
27
+
13 28
 	// Permissions
14 29
 	IsAdmin		bool	`xorm:"DEFAULT 0"`
15 30
 	IsActive	bool	`xorm:"DEFAULT 0"`
@@ -41,4 +56,174 @@ type SshKey struct {
41 56
 
42 57
 	// Relations
43 58
 	// 	UserID
59
+}
60
+
61
+func countUsers(e Engine) int64 {
62
+	count, _ := e.Where("type=0").Count(new(User))
63
+	return count
64
+}
65
+
66
+// CountUsers returns number of users.
67
+func CountUsers() int64 {
68
+	return countUsers(x)
69
+}
70
+
71
+func getUserByID(e Engine, id int64) (*User, error) {
72
+	u := new(User)
73
+	has, err := e.Id(id).Get(u)
74
+	if err != nil {
75
+		return nil, err
76
+	} else if !has {
77
+		return nil, errors.UserNotExist{id, ""}
78
+	}
79
+	return u, nil
80
+}
81
+
82
+// GetUserByID returns the user object by given ID if exists.
83
+func GetUserByID(id int64) (*User, error) {
84
+	return getUserByID(x, id)
85
+}
86
+
87
+// IsUserExist checks if given user name exist,
88
+// the user name should be noncased unique.
89
+// If uid is presented, then check will rule out that one,
90
+// it is used when update a user name in settings page.
91
+func IsUserExist(uid int64, name string) (bool, error) {
92
+	if len(name) == 0 {
93
+		return false, nil
94
+	}
95
+	return x.Where("id != ?", uid).Get(&User{LowerName: strings.ToLower(name)})
96
+}
97
+
98
+var (
99
+	reservedUsernames    = []string{"assets", "css", "img", "js", "less", "plugins", "debug", "raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new", ".", ".."}
100
+	reservedUserPatterns = []string{"*.keys"}
101
+)
102
+
103
+// isUsableName checks if name is reserved or pattern of name is not allowed
104
+// based on given reserved names and patterns.
105
+// Names are exact match, patterns can be prefix or suffix match with placeholder '*'.
106
+func isUsableName(names, patterns []string, name string) error {
107
+	name = strings.TrimSpace(strings.ToLower(name))
108
+	if utf8.RuneCountInString(name) == 0 {
109
+		return errors.EmptyName{}
110
+	}
111
+
112
+	for i := range names {
113
+		if name == names[i] {
114
+			return ErrNameReserved{name}
115
+		}
116
+	}
117
+
118
+	for _, pat := range patterns {
119
+		if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) ||
120
+			(pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) {
121
+			return ErrNamePatternNotAllowed{pat}
122
+		}
123
+	}
124
+
125
+	return nil
126
+}
127
+
128
+func IsUsableUsername(name string) error {
129
+	return isUsableName(reservedUsernames, reservedUserPatterns, name)
130
+}
131
+
132
+// EncodePasswd encodes password to safe format.
133
+func (u *User) EncodePasswd() {
134
+	newPasswd := pbkdf2.Key([]byte(u.Password), []byte(u.Salt), 10000, 50, sha256.New)
135
+	u.Password = fmt.Sprintf("%x", newPasswd)
136
+}
137
+
138
+// ValidatePassword checks if given password matches the one belongs to the user.
139
+func (u *User) ValidatePassword(passwd string) bool {
140
+	newUser := &User{Password: passwd, Salt: u.Salt}
141
+	newUser.EncodePasswd()
142
+	return subtle.ConstantTimeCompare([]byte(u.Password), []byte(newUser.Password)) == 1
143
+}
144
+
145
+// GetUserSalt returns a ramdom user salt token.
146
+func GetUserSalt() (string, error) {
147
+	return tool.RandomString(10)
148
+}
149
+
150
+// UserPath returns the path absolute path of user repositories.
151
+func UserPath(userName string) string {
152
+	return filepath.Join(setting.RepositoryRoot, strings.ToLower(userName))
153
+}
154
+
155
+// Create a new user and do some validation
156
+func CreateUser(u *User) (err error) {
157
+	if err = IsUsableUsername(u.UserName); err != nil {
158
+		return err
159
+	}
160
+
161
+	isExist, err := IsUserExist(0, u.UserName)
162
+	if err != nil {
163
+		return err
164
+	} else if isExist {
165
+		return ErrUserAlreadyExist{u.UserName}
166
+	}
167
+
168
+	u.Email = strings.ToLower(u.Email)
169
+	u.LowerName = strings.ToLower(u.UserName)
170
+
171
+	if u.Rands, err = GetUserSalt(); err != nil {
172
+		return err
173
+	}
174
+	if u.Salt, err = GetUserSalt(); err != nil {
175
+		return err
176
+	}
177
+	u.EncodePasswd()
178
+
179
+	sess := x.NewSession()
180
+	defer sessionRelease(sess)
181
+	if err = sess.Begin(); err != nil {
182
+		return err
183
+	}
184
+
185
+	if _, err = sess.Insert(u); err != nil {
186
+		return err
187
+	} else if err = os.MkdirAll(UserPath(u.UserName), os.ModePerm); err != nil {
188
+		return err
189
+	}
190
+
191
+	return sess.Commit()
192
+}
193
+
194
+// Update an user
195
+func updateUser(e Engine, u *User) error {
196
+	u.LowerName = strings.ToLower(u.UserName)
197
+	u.Email = strings.ToLower(u.Email)
198
+	_, err := e.Id(u.ID).AllCols().Update(u)
199
+	return err
200
+}
201
+
202
+func UpdateUser(u *User) error {
203
+	return updateUser(x, u)
204
+}
205
+
206
+// Login validates user name and password.
207
+func UserLogin(username, password string) (*User, error) {
208
+	var user *User
209
+	if strings.Contains(username, "@") {
210
+		user = &User{Email: strings.ToLower(username)}
211
+	} else {
212
+		user = &User{LowerName: strings.ToLower(username)}
213
+	}
214
+
215
+	hasUser, err := x.Get(user)
216
+	if err != nil {
217
+		return nil, err
218
+	}
219
+
220
+	if hasUser {
221
+		if user.ValidatePassword(password) {
222
+			return user, nil
223
+		}
224
+
225
+		return nil, errors.UserNotExist{user.ID, user.UserName}
226
+	}
227
+
228
+	return nil, errors.UserNotExist{user.ID, user.UserName}
44 229
 }

+ 139
- 0
routers/user/auth.go View File

@@ -2,6 +2,12 @@ package user
2 2
 
3 3
 import (
4 4
 	"dev.sigpipe.me/dashie/git.txt/context"
5
+	"dev.sigpipe.me/dashie/git.txt/setting"
6
+	"dev.sigpipe.me/dashie/git.txt/stuff/form"
7
+	"dev.sigpipe.me/dashie/git.txt/models"
8
+	log "gopkg.in/clog.v1"
9
+	"dev.sigpipe.me/dashie/git.txt/models/errors"
10
+	"net/url"
5 11
 )
6 12
 
7 13
 const (
@@ -12,6 +18,14 @@ const (
12 18
 	RESET_PASSWORD = "user/auth/reset_password"
13 19
 )
14 20
 
21
+// isValidRedirect returns false if the URL does not redirect to same site.
22
+// False: //url, http://url
23
+// True: /url
24
+func isValidRedirect(url string) bool {
25
+	return len(url) >= 2 && url[0] == '/' && url[1] != '/'
26
+}
27
+
28
+// Login
15 29
 func Login(ctx *context.Context) {
16 30
 	ctx.Title("login.title")
17 31
 
@@ -20,7 +34,132 @@ func Login(ctx *context.Context) {
20 34
 	ctx.HTML(200, LOGIN)
21 35
 }
22 36
 
37
+func LoginPost(ctx *context.Context, f form.Login) {
38
+	ctx.Title("login.title")
39
+
40
+	if ctx.HasError() {
41
+		ctx.Success(LOGIN)
42
+		return
43
+	}
44
+
45
+	u, err := models.UserLogin(f.UserName, f.Password)
46
+	if err != nil {
47
+		if errors.IsUserNotExist(err) {
48
+			ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), LOGIN, &f)
49
+		} else {
50
+			ctx.ServerError("UserSignIn", err)
51
+		}
52
+		return
53
+	}
54
+
55
+	afterLogin(ctx, u, f.Remember)
56
+	return
57
+}
58
+
59
+// After login func, may be useful with Two-Factor
60
+func afterLogin(ctx *context.Context, u *models.User, remember bool) {
61
+	if remember {
62
+		days := 86400 * setting.LoginRememberDays
63
+		ctx.SetCookie(setting.CookieUserName, u.UserName, days, setting.AppSubURL, "", setting.CookieSecure, true)
64
+		ctx.SetSuperSecureCookie(u.Rands+u.Password, setting.CookieRememberName, u.UserName, days, setting.AppSubURL, "", setting.CookieSecure, true)
65
+	}
66
+
67
+	ctx.Session.Set("uid", u.ID)
68
+	ctx.Session.Set("uname", u.UserName)
69
+
70
+	// Clear CSRF and force regenerate one
71
+	ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
72
+	if setting.EnableLoginStatusCookie {
73
+		ctx.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubURL)
74
+	}
75
+
76
+	redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to"))
77
+	if isValidRedirect(redirectTo) {
78
+		ctx.Redirect(redirectTo)
79
+		return
80
+	}
81
+
82
+	ctx.Redirect(setting.AppSubURL + "/")
83
+}
84
+
85
+// Registration
23 86
 func Register(ctx *context.Context) {
24 87
 	ctx.Title("register.title")
88
+	if ! setting.CanRegister {
89
+		ctx.Flash.Error(ctx.Tr("register.not_allowed"))
90
+		ctx.Redirect(setting.AppSubURL + "/")
91
+		return
92
+	}
93
+
25 94
 	ctx.HTML(200, REGISTER)
95
+}
96
+
97
+func RegisterPost(ctx *context.Context, f form.Register) {
98
+	ctx.Title("register.title")
99
+
100
+	if ! setting.CanRegister {
101
+		ctx.Flash.Error(ctx.Tr("register.not_allowed"))
102
+		ctx.Redirect(setting.AppSubURL + "/")
103
+		return
104
+	}
105
+
106
+	if ctx.HasError() {
107
+		ctx.HTML(200, REGISTER)
108
+		return
109
+	}
110
+
111
+	if f.Password != f.Repeat {
112
+		ctx.Data["Err_Password"] = true
113
+		ctx.Data["Err_Retype"] = true
114
+		ctx.RenderWithErr(ctx.Tr("form.password_not_match"), REGISTER, &f)
115
+		return
116
+	}
117
+
118
+	u := &models.User{
119
+		UserName:	f.UserName,
120
+		Email:		f.Email,
121
+		Password:	f.Password,
122
+		IsActive:	true, // FIXME: implement user activation by email
123
+	}
124
+	if err := models.CreateUser(u); err != nil {
125
+		switch {
126
+		case models.IsErrUserAlreadyExist(err):
127
+			ctx.Data["Err_UserName"] = true
128
+			ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), REGISTER, &f)
129
+		case models.IsErrNameReserved(err):
130
+			ctx.Data["Err_UserName"] = true
131
+			ctx.RenderWithErr(ctx.Tr("form.username_reserved"), REGISTER, &f)
132
+		case models.IsErrNamePatternNotAllowed(err):
133
+			ctx.Data["Err_UserName"] = true
134
+			ctx.RenderWithErr(ctx.Tr("form.username_pattern_not_allowed"), REGISTER, &f)
135
+		default:
136
+			ctx.Handle(500, "CreateUser", err)
137
+		}
138
+		return
139
+	}
140
+	log.Trace("Account created: %s", u.UserName)
141
+
142
+	// Auto set Admin if first user
143
+	if models.CountUsers() == 1 {
144
+		u.IsAdmin = true
145
+		u.IsActive = true // bypass email activation
146
+		if err := models.UpdateUser(u); err != nil {
147
+			ctx.Handle(500, "UpdateUser", err)
148
+			return
149
+		}
150
+	}
151
+
152
+	// TODO: send activation email
153
+
154
+	ctx.Redirect(setting.AppSubURL + "/user/login")
155
+}
156
+
157
+// Logout
158
+func Logout(ctx *context.Context) {
159
+	ctx.Session.Delete("uid")
160
+	ctx.Session.Delete("uname")
161
+	ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL)
162
+	ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL)
163
+	ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
164
+	ctx.Redirect(setting.AppSubURL + "/")
26 165
 }

+ 19
- 1
setting/setting.go View File

@@ -78,7 +78,14 @@ var (
78 78
 	CSRFCookieName string
79 79
 
80 80
 	// Security settings
81
-	SecretKey	string
81
+	InstallLock             bool
82
+	SecretKey               string
83
+	LoginRememberDays       int
84
+	CookieUserName          string
85
+	CookieRememberName      string
86
+	CookieSecure            bool
87
+	EnableLoginStatusCookie bool
88
+	LoginStatusCookieName string
82 89
 
83 90
 	// Cache settings
84 91
 	CacheAdapter  string
@@ -225,6 +232,17 @@ func InitConfig() {
225 232
 	Names = Cfg.Section("i18n").Key("NAMES").Strings(",")
226 233
 	dateLangs = Cfg.Section("i18n.datelang").KeysHash()
227 234
 
235
+	sec = Cfg.Section("security")
236
+	InstallLock = sec.Key("INSTALL_LOCK").MustBool()
237
+	SecretKey = sec.Key("SECRET_KEY").String()
238
+	LoginRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt()
239
+	CookieUserName = sec.Key("COOKIE_USERNAME").String()
240
+	CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").String()
241
+	CookieSecure = sec.Key("COOKIE_SECURE").MustBool(false)
242
+	EnableLoginStatusCookie = sec.Key("ENABLE_LOGIN_STATUS_COOKIE").MustBool(false)
243
+	LoginStatusCookieName = sec.Key("LOGIN_STATUS_COOKIE_NAME").MustString("login_status")
244
+
245
+
228 246
 	initLogging()
229 247
 	initSession()
230 248
 	initCache()

+ 17
- 3
static/css/custom.css View File

@@ -45,12 +45,26 @@ footer .right {
45 45
 .form-signin .form-control:focus {
46 46
     z-index: 2;
47 47
 }
48
-.form-signin input[type="email"] {
49
-    margin-bottom: -1px;
48
+.form-signin input[id="user_name"] {
49
+    margin-bottom: -15px;
50 50
     border-bottom-right-radius: 0;
51 51
     border-bottom-left-radius: 0;
52 52
 }
53
-.form-signin input[type="password"] {
53
+.form-signin input[id="email"] {
54
+    margin-bottom: -15px;
55
+    border-top-left-radius: 0;
56
+    border-top-right-radius: 0;
57
+    border-bottom-right-radius: 0;
58
+    border-bottom-left-radius: 0;
59
+}
60
+.form-signin input[id="password"] {
61
+    margin-bottom: -15px;
62
+    border-top-left-radius: 0;
63
+    border-top-right-radius: 0;
64
+    border-bottom-left-radius: 0;
65
+    border-bottom-right-radius: 0;
66
+}
67
+.form-signin input[id="repeat"] {
54 68
     margin-bottom: 10px;
55 69
     border-top-left-radius: 0;
56 70
     border-top-right-radius: 0;

+ 62
- 0
stuff/auth/auth.go View File

@@ -0,0 +1,62 @@
1
+// Copyright 2014 The Gogs Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package auth
6
+
7
+import (
8
+	"strings"
9
+	"dev.sigpipe.me/dashie/git.txt/models"
10
+	"dev.sigpipe.me/dashie/git.txt/models/errors"
11
+
12
+	"github.com/go-macaron/session"
13
+	log "gopkg.in/clog.v1"
14
+	"gopkg.in/macaron.v1"
15
+)
16
+
17
+func IsAPIPath(url string) bool {
18
+	return strings.HasPrefix(url, "/api/")
19
+}
20
+
21
+// SignedInID returns the id of signed in user.
22
+func SignedInID(ctx *macaron.Context, sess session.Store) int64 {
23
+	if !models.HasEngine {
24
+		return 0
25
+	}
26
+
27
+	uid := sess.Get("uid")
28
+	if uid == nil {
29
+		return 0
30
+	}
31
+	if id, ok := uid.(int64); ok {
32
+		if _, err := models.GetUserByID(id); err != nil {
33
+			if !errors.IsUserNotExist(err) {
34
+				log.Error(2, "GetUserByID: %v", err)
35
+			}
36
+			return 0
37
+		}
38
+		return id
39
+	}
40
+	return 0
41
+}
42
+
43
+// SignedInUser returns the user object of signed user.
44
+// It returns a bool value to indicate whether user uses basic auth or not.
45
+func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) {
46
+	if !models.HasEngine {
47
+		return nil, false
48
+	}
49
+
50
+	uid := SignedInID(ctx, sess)
51
+
52
+	if uid <= 0 {
53
+		return nil, false
54
+	}
55
+
56
+	u, err := models.GetUserByID(uid)
57
+	if err != nil {
58
+		log.Error(4, "GetUserById: %v", err)
59
+		return nil, false
60
+	}
61
+	return u, false
62
+}

+ 29
- 0
stuff/form/auth.go View File

@@ -0,0 +1,29 @@
1
+package form
2
+
3
+import (
4
+	"github.com/go-macaron/binding"
5
+	"gopkg.in/macaron.v1"
6
+)
7
+
8
+// Register
9
+type Register struct {
10
+	UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"`
11
+	Email    string `binding:"Required;Email;MaxSize(254)"`
12
+	Password string `binding:"Required;MaxSize(255)"`
13
+	Repeat string
14
+}
15
+
16
+func (f *Register) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
17
+	return validate(errs, ctx.Data, f, ctx.Locale)
18
+}
19
+
20
+// Login
21
+type Login struct {
22
+	UserName string `binding:"Required;MaxSize(254)"`
23
+	Password string `binding:"Required;MaxSize(255)"`
24
+	Remember bool
25
+}
26
+
27
+func (f *Login) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
28
+	return validate(errs, ctx.Data, f, ctx.Locale)
29
+}

+ 153
- 0
stuff/form/form.go View File

@@ -0,0 +1,153 @@
1
+package form
2
+
3
+import (
4
+	"reflect"
5
+	"github.com/go-macaron/binding"
6
+	"gopkg.in/macaron.v1"
7
+	"github.com/Unknwon/com"
8
+	"strings"
9
+	"regexp"
10
+	"fmt"
11
+	log "gopkg.in/clog.v1"
12
+)
13
+
14
+const ERR_ALPHA_DASH_DOT_SLASH = "AlphaDashDotSlashError"
15
+
16
+var AlphaDashDotSlashPattern = regexp.MustCompile("[^\\d\\w-_\\./]")
17
+
18
+func init() {
19
+	binding.SetNameMapper(com.ToSnakeCase)
20
+	binding.AddRule(&binding.Rule{
21
+		IsMatch: func(rule string) bool {
22
+			return rule == "AlphaDashDotSlash"
23
+		},
24
+		IsValid: func(errs binding.Errors, name string, v interface{}) (bool, binding.Errors) {
25
+			if AlphaDashDotSlashPattern.MatchString(fmt.Sprintf("%v", v)) {
26
+				errs.Add([]string{name}, ERR_ALPHA_DASH_DOT_SLASH, "AlphaDashDotSlash")
27
+				return false, errs
28
+			}
29
+			return true, errs
30
+		},
31
+	})
32
+}
33
+
34
+type Form interface {
35
+	binding.Validator
36
+}
37
+
38
+// Assign assign form values back to the template data.
39
+func Assign(form interface{}, data map[string]interface{}) {
40
+	typ := reflect.TypeOf(form)
41
+	val := reflect.ValueOf(form)
42
+
43
+	if typ.Kind() == reflect.Ptr {
44
+		typ = typ.Elem()
45
+		val = val.Elem()
46
+	}
47
+
48
+	for i := 0; i < typ.NumField(); i++ {
49
+		field := typ.Field(i)
50
+
51
+		fieldName := field.Tag.Get("form")
52
+		// Allow ignored fields in the struct
53
+		if fieldName == "-" {
54
+			continue
55
+		} else if len(fieldName) == 0 {
56
+			fieldName = com.ToSnakeCase(field.Name)
57
+		}
58
+
59
+		data[fieldName] = val.Field(i).Interface()
60
+	}
61
+}
62
+
63
+func getRuleBody(field reflect.StructField, prefix string) string {
64
+	for _, rule := range strings.Split(field.Tag.Get("binding"), ";") {
65
+		if strings.HasPrefix(rule, prefix) {
66
+			return rule[len(prefix) : len(rule)-1]
67
+		}
68
+	}
69
+	return ""
70
+}
71
+
72
+func getSize(field reflect.StructField) string {
73
+	return getRuleBody(field, "Size(")
74
+}
75
+
76
+func getMinSize(field reflect.StructField) string {
77
+	return getRuleBody(field, "MinSize(")
78
+}
79
+
80
+func getMaxSize(field reflect.StructField) string {
81
+	return getRuleBody(field, "MaxSize(")
82
+}
83
+
84
+func getInclude(field reflect.StructField) string {
85
+	return getRuleBody(field, "Include(")
86
+}
87
+
88
+func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaron.Locale) binding.Errors {
89
+	log.Trace("Validating form")
90
+
91
+	if errs.Len() == 0 {
92
+		return errs
93
+	}
94
+
95
+	data["HasError"] = true
96
+	Assign(f, data)
97
+
98
+	typ := reflect.TypeOf(f)
99
+	val := reflect.ValueOf(f)
100
+
101
+	if typ.Kind() == reflect.Ptr {
102
+		typ = typ.Elem()
103
+		val = val.Elem()
104
+	}
105
+
106
+	for i := 0; i < typ.NumField(); i++ {
107
+		field := typ.Field(i)
108
+
109
+		fieldName := field.Tag.Get("form")
110
+		// Allow ignored fields in the struct
111
+		if fieldName == "-" {
112
+			continue
113
+		}
114
+
115
+		if errs[0].FieldNames[0] == field.Name {
116
+			data["Err_"+field.Name] = true
117
+
118
+			trName := field.Tag.Get("locale")
119
+			if len(trName) == 0 {
120
+				trName = l.Tr("form." + field.Name)
121
+			} else {
122
+				trName = l.Tr(trName)
123
+			}
124
+
125
+			switch errs[0].Classification {
126
+			case binding.ERR_REQUIRED:
127
+				data["ErrorMsg"] = trName + l.Tr("form.require_error")
128
+			case binding.ERR_ALPHA_DASH:
129
+				data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
130
+			case binding.ERR_ALPHA_DASH_DOT:
131
+				data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error")
132
+			case ERR_ALPHA_DASH_DOT_SLASH:
133
+				data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_slash_error")
134
+			case binding.ERR_SIZE:
135
+				data["ErrorMsg"] = trName + l.Tr("form.size_error", getSize(field))
136
+			case binding.ERR_MIN_SIZE:
137
+				data["ErrorMsg"] = trName + l.Tr("form.min_size_error", getMinSize(field))
138
+			case binding.ERR_MAX_SIZE:
139
+				data["ErrorMsg"] = trName + l.Tr("form.max_size_error", getMaxSize(field))
140
+			case binding.ERR_EMAIL:
141
+				data["ErrorMsg"] = trName + l.Tr("form.email_error")
142
+			case binding.ERR_URL:
143
+				data["ErrorMsg"] = trName + l.Tr("form.url_error")
144
+			case binding.ERR_INCLUDE:
145
+				data["ErrorMsg"] = trName + l.Tr("form.include_error", getInclude(field))
146
+			default:
147
+				data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification
148
+			}
149
+			return errs
150
+		}
151
+	}
152
+	return errs
153
+}

+ 31
- 0
stuff/tool/tool.go View File

@@ -4,8 +4,12 @@ import (
4 4
 	"encoding/hex"
5 5
 	"crypto/sha1"
6 6
 	"crypto/md5"
7
+	"math/big"
8
+	"crypto/rand"
7 9
 )
8 10
 
11
+const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
12
+
9 13
 // MD5Bytes encodes string to MD5 bytes.
10 14
 func MD5Bytes(str string) []byte {
11 15
 	m := md5.New()
@@ -31,4 +35,31 @@ func ShortSHA1(sha1 string) string {
31 35
 		return sha1[:10]
32 36
 	}
33 37
 	return sha1
38
+}
39
+
40
+// RandomString returns generated random string in given length of characters.
41
+// It also returns possible error during generation.
42
+func RandomString(n int) (string, error) {
43
+	buffer := make([]byte, n)
44
+	max := big.NewInt(int64(len(alphanum)))
45
+
46
+	for i := 0; i < n; i++ {
47
+		index, err := randomInt(max)
48
+		if err != nil {
49
+			return "", err
50
+		}
51
+
52
+		buffer[i] = alphanum[index]
53
+	}
54
+
55
+	return string(buffer), nil
56
+}
57
+
58
+func randomInt(max *big.Int) (int, error) {
59
+	rand, err := rand.Int(rand.Reader, max)
60
+	if err != nil {
61
+		return 0, err
62
+	}
63
+
64
+	return int(rand.Int64()), nil
34 65
 }

+ 8
- 4
templates/base/alert.tmpl View File

@@ -1,20 +1,24 @@
1 1
 {{if .Flash.ErrorMsg}}
2
-<div class="ui negative message">
2
+<div class="alert alert-danger alert-dismissible">
3
+    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
3 4
     <p>{{.Flash.ErrorMsg | Str2html}}</p>
4 5
 </div>
5 6
 {{end}}
6 7
 {{if .Flash.WarningMsg}}
7
-<div class="ui warning message">
8
+<div class="alert alert-warning alert-dismissible">
9
+    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
8 10
     <p>{{.Flash.WarningMsg | Str2html}}</p>
9 11
 </div>
10 12
 {{end}}
11 13
 {{if .Flash.SuccessMsg}}
12
-<div class="ui positive message">
14
+<div class="alert alert-success alert-dismissible">
15
+    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
13 16
     <p>{{.Flash.SuccessMsg | Str2html}}</p>
14 17
 </div>
15 18
 {{end}}
16 19
 {{if .Flash.InfoMsg}}
17
-<div class="ui info message">
20
+<div class="alert alert-info alert-dismissible">
21
+    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
18 22
     <p>{{.Flash.InfoMsg | Str2html}}</p>
19 23
 </div>
20 24
 {{end}}

+ 3
- 2
templates/base/head.tmpl View File

@@ -45,10 +45,10 @@
45 45
                     <li><a href="{{AppSubURL}}/new">New git.txt</a></li>
46 46
                 {{end}}
47 47
                 {{if .IsLogged}}
48
-                    <li><a href="{{AppSubURL}}/logout">Logout</a></li>
48
+                    <li><a href="{{AppSubURL}}/user/logout">Logout</a></li>
49 49
                 {{else}}
50 50
                     {{if CanRegister}}
51
-                        <li><a href="{{AppSubURL}}/register">Register</a></li>
51
+                        <li><a href="{{AppSubURL}}/user/register">Register</a></li>
52 52
                     {{end}}
53 53
                     <li><a href="{{AppSubURL}}/user/login">Login</a></li>
54 54
                 {{end}}
@@ -58,6 +58,7 @@
58 58
 </nav>
59 59
 
60 60
 <div class="container">
61
+    {{template "base/alert" .}}
61 62
 {{/*
62 63
 </div>
63 64
 </body>

+ 11
- 6
templates/user/auth/login.tmpl View File

@@ -1,14 +1,19 @@
1 1
 {{template "base/head" .}}
2 2
 
3
-
4 3
 <form class="form-signin" action="{{.Link}}" method="post">
5 4
     {{.CSRFTokenHTML}}
6 5
     <h2 class="form-signin-heading">{{.i18n.Tr "login.sign_in"}}</h2>
7
-    {{template "base/alert" .}}
8
-    <label for="inputEmail" class="sr-only">{{.i18n.Tr "login.email"}}</label>
9
-    <input type="email" id="inputEmail" class="form-control {{if .Err_UserName}}error{{end}}" placeholder='{{.i18n.Tr "login.email_placeholder"}}' required autofocus>
10
-    <label for="inputPassword" class="sr-only">{{.i18n.Tr "login.password"}}</label>
11
-    <input type="password" id="inputPassword" class="form-control {{if .Err_UserName}}error{{end}}" placeholder='{{.i18n.Tr "login.password_placeholder"}}' required>
6
+
7
+    <div class="user_name form-group {{if .Err_UserName}}has-error{{end}}">
8
+    <label for="user_name" class="sr-only">{{.i18n.Tr "login.username"}}</label>
9
+    <input type="string" name="user_name" id="user_name" class="form-control" placeholder='{{.i18n.Tr "login.username_placeholder"}}' required autofocus>
10
+    </div>
11
+
12
+    <div class="password form-group {{if .Err_Password}}has-error{{end}}">
13
+    <label for="password" class="sr-only">{{.i18n.Tr "login.password"}}</label>
14
+    <input type="password" name="password" id="password" class="form-control" placeholder='{{.i18n.Tr "login.password_placeholder"}}' required>
15
+    </div>
16
+
12 17
     <div class="checkbox">
13 18
         <label>
14 19
             <input type="checkbox" value="remember-me"> {{.i18n.Tr "login.remember_me"}}

+ 30
- 0
templates/user/auth/register.tmpl View File

@@ -0,0 +1,30 @@
1
+{{template "base/head" .}}
2
+
3
+<form class="form-signin" action="{{.Link}}" method="post">
4
+    {{.CSRFTokenHTML}}
5
+    <h2 class="form-signin-heading">{{.i18n.Tr "register.register"}}</h2>
6
+
7
+    <div class="user_name form-group {{if .Err_UserName}}has-error{{end}}">
8
+    <label for="user_name" class="sr-only">{{.i18n.Tr "login.username"}}</label>
9
+    <input type="text" name="user_name" id="user_name" class="form-control" value="{{.user_name}}" placeholder='{{.i18n.Tr "login.username_placeholder"}}' required autofocus>
10
+    </div>
11
+
12
+    <div class="email form-group {{if .Err_Email}}has-error{{end}}">
13
+    <label for="email" class="sr-only">{{.i18n.Tr "login.email"}}</label>
14
+    <input type="email" name="email" id="email" class="form-control" value="{{.email}}" placeholder='{{.i18n.Tr "login.email_placeholder"}}' required autofocus>
15
+    </div>
16
+
17
+    <div class="password form-group {{if .Err_Password}}has-error{{end}}">
18
+    <label for="password" class="sr-only">{{.i18n.Tr "login.password"}}</label>
19
+    <input type="password" name="password" id="password" class="form-control" value="{{.password}}" placeholder='{{.i18n.Tr "login.password_placeholder"}}' required>
20
+    </div>
21
+
22
+    <div class="repeat form-group {{if .Err_Repeat}}has-error{{end}}">
23
+    <label for="repeat" class="sr-only">{{.i18n.Tr "login.password"}}</label>
24
+    <input type="password" name="repeat" id="repeat" class="form-control" value="{{.repeat}}" placeholder='{{.i18n.Tr "login.repeat_password_placeholder"}}' required>
25
+    </div>
26
+
27
+    <button class="btn btn-lg btn-primary btn-block" type="submit">{{.i18n.Tr "login.sign_in"}}</button>
28
+</form>
29
+
30
+{{template "base/footer" .}}

Loading…
Cancel
Save