Browse Source

Can register, login, logout

tags/v0.2
Dashie der otter 2 years 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 = [
watch_all = true
watch_dirs = [
"$WORKDIR/cmd",
"$WORKDIR/routers"
"$WORKDIR/routers",
"$WORKDIR/context",
"$WORKDIR/stuff",
"$WORKDIR/models",
"$WORKDIR/conf/locale",
]
watch_exts = [".go"]
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 (
"dev.sigpipe.me/dashie/git.txt/routers"
"dev.sigpipe.me/dashie/git.txt/bindata"
"dev.sigpipe.me/dashie/git.txt/stuff/template"
"dev.sigpipe.me/dashie/git.txt/stuff/form"
"path"
"github.com/go-macaron/session"
"github.com/go-macaron/csrf"
"github.com/go-macaron/cache"
"github.com/go-macaron/i18n"
//"github.com/go-macaron/binding"
"github.com/go-macaron/binding"
"strings"
"fmt"
log "gopkg.in/clog.v1"
@@ -120,16 +121,24 @@ func runWeb(ctx *cli.Context) error {

m := newMacaron()

//bindIgnErr := binding.BindIgnErr
reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})

bindIgnErr := binding.BindIgnErr

m.Get("/", routers.Home)

m.Group("/user", func() {
m.Group("/login", func() {
m.Combo("").Get(user.Login)
m.Combo("").Get(user.Login).Post(bindIgnErr(form.Login{}), user.LoginPost)
})
m.Get("/register", user.Register)
m.Post("/register", bindIgnErr(form.Register{}), user.RegisterPost)
}, reqSignOut)

m.Group("/user", func() {
m.Get("/logout", user.Logout)
})
m.Get("/register", user.Register)

// robots.txt
m.Get("/robots.txt", func(ctx *context.Context) {

+ 9
- 0
conf/app.ini View File

@@ -103,6 +103,15 @@ CSRF_COOKIE_NAME = _csrf
[security]
; !!CHANGE THIS TO KEEP YOUR USER DATA SAFE!!
SECRET_KEY = !#@FDEWREWR&*(
INSTALL_LOCK = false
; Auto-login remember days
LOGIN_REMEMBER_DAYS = 7
COOKIE_USERNAME = gitxt_celestia
COOKIE_REMEMBER_NAME = gitxt_luna
COOKIE_SECURE = false
; Enable to set cookie to indicate user login status
ENABLE_LOGIN_STATUS_COOKIE = false
LOGIN_STATUS_COOKIE_NAME = login_status

[i18n]
LANGS = en-US,fr-FR

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

@@ -1,11 +1,23 @@
[login]
title = "Sign In"
sign_in = "Sign In"
username = "Username"
username_placeholder = "Your username."
email = "Email"
email_placeholder = "Email address..."
email_placeholder = "Your email."
password = "Password"
password_placeholder = "Password..."
password_placeholder = "Your nice password."
repeat_password_placeholder = "And the same one here."
remember_me = "Remember me"

[register]
title = "Register"
title = "Register"
register = "Register"
not_allowed = "Registration not allowed"

[form]
password_not_match = "Passwords doesn't match"
username_password_incorrect = "Invalid username or password"
username_been_taken = "Username already taken, sorry"
username_reserved = "Username reserved, please choose another username"
username_pattern_not_allowed = "Invalid username pattern"

+ 54
- 1
context/context.go View File

@@ -15,6 +15,8 @@ import (
"github.com/go-macaron/i18n"
"html/template"
"dev.sigpipe.me/dashie/git.txt/models"
"dev.sigpipe.me/dashie/git.txt/stuff/form"
"dev.sigpipe.me/dashie/git.txt/stuff/auth"
)

// Context represents context of a request.
@@ -42,6 +44,36 @@ func (ctx *Context) HTML(status int, name string) {
ctx.Context.HTML(status, name)
}

// Success responses template with status http.StatusOK.
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)
}

// HasError returns true if error occurs in form validation.
func (ctx *Context) HasError() bool {
hasErr, ok := ctx.Data["HasError"]
if !ok {
return false
}
ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string)
ctx.Data["Flash"] = ctx.Flash
return hasErr.(bool)
}

// 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 {
form.Assign(f, ctx.Data)
}
ctx.Flash.ErrorMsg = msg
ctx.Data["Flash"] = ctx.Flash
ctx.HTML(http.StatusOK, tpl)
}

// Handle handles and logs error by given status.
func (ctx *Context) Handle(status int, title string, err error) {
@@ -55,6 +87,27 @@ func (ctx *Context) Handle(status int, title string, err error) {
ctx.HTML(status, fmt.Sprintf("status/%d", status))
}

// NotFound renders the 404 page.
func (ctx *Context) NotFound() {
ctx.Handle(http.StatusNotFound, "", nil)
}

// ServerError renders the 500 page.
func (c *Context) ServerError(title string, err error) {
c.Handle(http.StatusInternalServerError, title, err)
}

// NotFoundOrServerError use error check function to determine if the error
// is about not found. It responses with 404 status code for not found error,
// or error context description for logging purpose of 500 server error.
func (c *Context) NotFoundOrServerError(title string, errck func(error) bool, err error) {
if errck(err) {
c.NotFound()
return
}
c.ServerError(title, err)
}

func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
modtime := time.Now()
for _, p := range params {
@@ -97,7 +150,7 @@ func Contexter() macaron.Handler {
ctx.Data["PageStartTime"] = time.Now()

// Get user from session if logined.
// ctx.User, ctx.IsBasicAuth = auth.SignedInUser(ctx.Context, ctx.Session)
ctx.User, ctx.IsBasicAuth = auth.SignedInUser(ctx.Context, ctx.Session)

if ctx.User != nil {
ctx.IsLogged = true

+ 201
- 0
models/error.go View File

@@ -0,0 +1,201 @@
// Copyright 2015 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 (
"fmt"
)

type ErrNameReserved struct {
Name string
}

func IsErrNameReserved(err error) bool {
_, ok := err.(ErrNameReserved)
return ok
}

func (err ErrNameReserved) Error() string {
return fmt.Sprintf("name is reserved [name: %s]", err.Name)
}

type ErrNamePatternNotAllowed struct {
Pattern string
}

func IsErrNamePatternNotAllowed(err error) bool {
_, ok := err.(ErrNamePatternNotAllowed)
return ok
}

func (err ErrNamePatternNotAllowed) Error() string {
return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern)
}

// ____ ___
// | | \______ ___________
// | | / ___// __ \_ __ \
// | | /\___ \\ ___/| | \/
// |______//____ >\___ >__|
// \/ \/

type ErrUserAlreadyExist struct {
Name string
}

func IsErrUserAlreadyExist(err error) bool {
_, ok := err.(ErrUserAlreadyExist)
return ok
}

func (err ErrUserAlreadyExist) Error() string {
return fmt.Sprintf("user already exists [name: %s]", err.Name)
}

type ErrEmailAlreadyUsed struct {
Email string
}

func IsErrEmailAlreadyUsed(err error) bool {
_, ok := err.(ErrEmailAlreadyUsed)
return ok
}

func (err ErrEmailAlreadyUsed) Error() string {
return fmt.Sprintf("e-mail has been used [email: %s]", err.Email)
}

type ErrUserOwnRepos struct {
UID int64
}

func IsErrUserOwnRepos(err error) bool {
_, ok := err.(ErrUserOwnRepos)
return ok
}

func (err ErrUserOwnRepos) Error() string {
return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
}

// __________ ___. .__ .__ ____ __.
// \______ \__ _\_ |__ | | |__| ____ | |/ _|____ ___.__.
// | ___/ | \ __ \| | | |/ ___\ | <_/ __ < | |
// | | | | / \_\ \ |_| \ \___ | | \ ___/\___ |
// |____| |____/|___ /____/__|\___ > |____|__ \___ > ____|
// \/ \/ \/ \/\/

type ErrKeyUnableVerify struct {
Result string
}

func IsErrKeyUnableVerify(err error) bool {
_, ok := err.(ErrKeyUnableVerify)
return ok
}

func (err ErrKeyUnableVerify) Error() string {
return fmt.Sprintf("Unable to verify key content [result: %s]", err.Result)
}

type ErrKeyNotExist struct {
ID int64
}

func IsErrKeyNotExist(err error) bool {
_, ok := err.(ErrKeyNotExist)
return ok
}

func (err ErrKeyNotExist) Error() string {
return fmt.Sprintf("public key does not exist [id: %d]", err.ID)
}

type ErrKeyAlreadyExist struct {
OwnerID int64
Content string
}

func IsErrKeyAlreadyExist(err error) bool {
_, ok := err.(ErrKeyAlreadyExist)
return ok
}

func (err ErrKeyAlreadyExist) Error() string {
return fmt.Sprintf("public key already exists [owner_id: %d, content: %s]", err.OwnerID, err.Content)
}

type ErrKeyNameAlreadyUsed struct {
OwnerID int64
Name string
}

func IsErrKeyNameAlreadyUsed(err error) bool {
_, ok := err.(ErrKeyNameAlreadyUsed)
return ok
}

func (err ErrKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
}

type ErrKeyAccessDenied struct {
UserID int64
KeyID int64
Note string
}

func IsErrKeyAccessDenied(err error) bool {
_, ok := err.(ErrKeyAccessDenied)
return ok
}

func (err ErrKeyAccessDenied) Error() string {
return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d, note: %s]",
err.UserID, err.KeyID, err.Note)
}

type ErrDeployKeyNotExist struct {
ID int64
KeyID int64
RepoID int64
}

func IsErrDeployKeyNotExist(err error) bool {
_, ok := err.(ErrDeployKeyNotExist)
return ok
}

func (err ErrDeployKeyNotExist) Error() string {
return fmt.Sprintf("Deploy key does not exist [id: %d, key_id: %d, repo_id: %d]", err.ID, err.KeyID, err.RepoID)
}

type ErrDeployKeyAlreadyExist struct {
KeyID int64
RepoID int64
}

func IsErrDeployKeyAlreadyExist(err error) bool {
_, ok := err.(ErrDeployKeyAlreadyExist)
return ok
}

func (err ErrDeployKeyAlreadyExist) Error() string {
return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
}

type ErrDeployKeyNameAlreadyUsed struct {
RepoID int64
Name string
}

func IsErrDeployKeyNameAlreadyUsed(err error) bool {
_, ok := err.(ErrDeployKeyNameAlreadyUsed)
return ok
}

func (err ErrDeployKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
}

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

@@ -0,0 +1,12 @@
// 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 "errors"

// New is a wrapper of real errors.New function.
func New(text string) error {
return errors.New(text)
}

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

@@ -0,0 +1,45 @@
// 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 EmptyName struct{}

func IsEmptyName(err error) bool {
_, ok := err.(EmptyName)
return ok
}

func (err EmptyName) Error() string {
return "empty name"
}

type UserNotExist struct {
UserID int64
Name string
}

func IsUserNotExist(err error) bool {
_, ok := err.(UserNotExist)
return ok
}

func (err UserNotExist) Error() string {
return fmt.Sprintf("user does not exist [user_id: %d, name: %s]", err.UserID, err.Name)
}

type UserNotKeyOwner struct {
KeyID int64
}

func IsUserNotKeyOwner(err error) bool {
_, ok := err.(UserNotKeyOwner)
return ok
}

func (err UserNotKeyOwner) Error() string {
return fmt.Sprintf("user is not the owner of public key [key_id: %d]", err.KeyID)
}

+ 186
- 1
models/user.go View File

@@ -2,14 +2,29 @@ package models

import (
"time"
"strings"
"unicode/utf8"
"dev.sigpipe.me/dashie/git.txt/models/errors"
"golang.org/x/crypto/pbkdf2"
"crypto/sha256"
"fmt"
"crypto/subtle"
"dev.sigpipe.me/dashie/git.txt/stuff/tool"
"os"
"path/filepath"
"dev.sigpipe.me/dashie/git.txt/setting"
)

type User struct {
ID int64 `xorm:"pk autoincr"`
UserName string `xorm:"UNIQUE NOT NULL"`
Password string `xorm:"NOT NULL"`
LowerName string `xorm:"UNIQUE NOT NULL"`
Email string `xorm:"NOT NULL"`

Password string `xorm:"NOT NULL"`
Rands string `xorm:"VARCHAR(10)"`
Salt string `xorm:"VARCHAR(10)"`

// Permissions
IsAdmin bool `xorm:"DEFAULT 0"`
IsActive bool `xorm:"DEFAULT 0"`
@@ -41,4 +56,174 @@ type SshKey struct {

// Relations
// UserID
}

func countUsers(e Engine) int64 {
count, _ := e.Where("type=0").Count(new(User))
return count
}

// CountUsers returns number of users.
func CountUsers() int64 {
return countUsers(x)
}

func getUserByID(e Engine, id int64) (*User, error) {
u := new(User)
has, err := e.Id(id).Get(u)
if err != nil {
return nil, err
} else if !has {
return nil, errors.UserNotExist{id, ""}
}
return u, nil
}

// GetUserByID returns the user object by given ID if exists.
func GetUserByID(id int64) (*User, error) {
return getUserByID(x, id)
}

// IsUserExist checks if given user name exist,
// the user name should be noncased unique.
// If uid is presented, then check will rule out that one,
// it is used when update a user name in settings page.
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)})
}

var (
reservedUsernames = []string{"assets", "css", "img", "js", "less", "plugins", "debug", "raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new", ".", ".."}
reservedUserPatterns = []string{"*.keys"}
)

// isUsableName checks if name is reserved or pattern of name is not allowed
// based on given reserved names and patterns.
// Names are exact match, patterns can be prefix or suffix match with placeholder '*'.
func isUsableName(names, patterns []string, name string) error {
name = strings.TrimSpace(strings.ToLower(name))
if utf8.RuneCountInString(name) == 0 {
return errors.EmptyName{}
}

for i := range names {
if name == names[i] {
return ErrNameReserved{name}
}
}

for _, pat := range patterns {
if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) ||
(pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) {
return ErrNamePatternNotAllowed{pat}
}
}

return nil
}

func IsUsableUsername(name string) error {
return isUsableName(reservedUsernames, reservedUserPatterns, name)
}

// EncodePasswd encodes password to safe format.
func (u *User) EncodePasswd() {
newPasswd := pbkdf2.Key([]byte(u.Password), []byte(u.Salt), 10000, 50, sha256.New)
u.Password = fmt.Sprintf("%x", newPasswd)
}

// ValidatePassword checks if given password matches the one belongs to the user.
func (u *User) ValidatePassword(passwd string) bool {
newUser := &User{Password: passwd, Salt: u.Salt}
newUser.EncodePasswd()
return subtle.ConstantTimeCompare([]byte(u.Password), []byte(newUser.Password)) == 1
}

// GetUserSalt returns a ramdom user salt token.
func GetUserSalt() (string, error) {
return tool.RandomString(10)
}

// UserPath returns the path absolute path of user repositories.
func UserPath(userName string) string {
return filepath.Join(setting.RepositoryRoot, strings.ToLower(userName))
}

// Create a new user and do some validation
func CreateUser(u *User) (err error) {
if err = IsUsableUsername(u.UserName); err != nil {
return err
}

isExist, err := IsUserExist(0, u.UserName)
if err != nil {
return err
} else if isExist {
return ErrUserAlreadyExist{u.UserName}
}

u.Email = strings.ToLower(u.Email)
u.LowerName = strings.ToLower(u.UserName)

if u.Rands, err = GetUserSalt(); err != nil {
return err
}
if u.Salt, err = GetUserSalt(); err != nil {
return err
}
u.EncodePasswd()

sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}

if _, err = sess.Insert(u); err != nil {
return err
} else if err = os.MkdirAll(UserPath(u.UserName), os.ModePerm); err != nil {
return err
}

return sess.Commit()
}

// Update an user
func updateUser(e Engine, u *User) error {
u.LowerName = strings.ToLower(u.UserName)
u.Email = strings.ToLower(u.Email)
_, err := e.Id(u.ID).AllCols().Update(u)
return err
}

func UpdateUser(u *User) error {
return updateUser(x, u)
}

// Login validates user name and password.
func UserLogin(username, password string) (*User, error) {
var user *User
if strings.Contains(username, "@") {
user = &User{Email: strings.ToLower(username)}
} else {
user = &User{LowerName: strings.ToLower(username)}
}

hasUser, err := x.Get(user)
if err != nil {
return nil, err
}

if hasUser {
if user.ValidatePassword(password) {
return user, nil
}

return nil, errors.UserNotExist{user.ID, user.UserName}
}

return nil, errors.UserNotExist{user.ID, user.UserName}
}

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

@@ -2,6 +2,12 @@ package user

import (
"dev.sigpipe.me/dashie/git.txt/context"
"dev.sigpipe.me/dashie/git.txt/setting"
"dev.sigpipe.me/dashie/git.txt/stuff/form"
"dev.sigpipe.me/dashie/git.txt/models"
log "gopkg.in/clog.v1"
"dev.sigpipe.me/dashie/git.txt/models/errors"
"net/url"
)

const (
@@ -12,6 +18,14 @@ const (
RESET_PASSWORD = "user/auth/reset_password"
)

// isValidRedirect returns false if the URL does not redirect to same site.
// False: //url, http://url
// True: /url
func isValidRedirect(url string) bool {
return len(url) >= 2 && url[0] == '/' && url[1] != '/'
}

// Login
func Login(ctx *context.Context) {
ctx.Title("login.title")

@@ -20,7 +34,132 @@ func Login(ctx *context.Context) {
ctx.HTML(200, LOGIN)
}

func LoginPost(ctx *context.Context, f form.Login) {
ctx.Title("login.title")

if ctx.HasError() {
ctx.Success(LOGIN)
return
}

u, err := models.UserLogin(f.UserName, f.Password)
if err != nil {
if errors.IsUserNotExist(err) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), LOGIN, &f)
} else {
ctx.ServerError("UserSignIn", err)
}
return
}

afterLogin(ctx, u, f.Remember)
return
}

// After login func, may be useful with Two-Factor
func afterLogin(ctx *context.Context, u *models.User, remember bool) {
if remember {
days := 86400 * setting.LoginRememberDays
ctx.SetCookie(setting.CookieUserName, u.UserName, days, setting.AppSubURL, "", setting.CookieSecure, true)
ctx.SetSuperSecureCookie(u.Rands+u.Password, setting.CookieRememberName, u.UserName, days, setting.AppSubURL, "", setting.CookieSecure, true)
}

ctx.Session.Set("uid", u.ID)
ctx.Session.Set("uname", u.UserName)

// Clear CSRF and force regenerate 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"))
if isValidRedirect(redirectTo) {
ctx.Redirect(redirectTo)
return
}

ctx.Redirect(setting.AppSubURL + "/")
}

// Registration
func Register(ctx *context.Context) {
ctx.Title("register.title")
if ! setting.CanRegister {
ctx.Flash.Error(ctx.Tr("register.not_allowed"))
ctx.Redirect(setting.AppSubURL + "/")
return
}

ctx.HTML(200, REGISTER)
}

func RegisterPost(ctx *context.Context, f form.Register) {
ctx.Title("register.title")

if ! setting.CanRegister {
ctx.Flash.Error(ctx.Tr("register.not_allowed"))
ctx.Redirect(setting.AppSubURL + "/")
return
}

if ctx.HasError() {
ctx.HTML(200, REGISTER)
return
}

if f.Password != f.Repeat {
ctx.Data["Err_Password"] = true
ctx.Data["Err_Retype"] = true
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), REGISTER, &f)
return
}

u := &models.User{
UserName: f.UserName,
Email: f.Email,
Password: f.Password,
IsActive: true, // FIXME: implement user activation by email
}
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), REGISTER, &f)
case models.IsErrNameReserved(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_reserved"), REGISTER, &f)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_pattern_not_allowed"), REGISTER, &f)
default:
ctx.Handle(500, "CreateUser", err)
}
return
}
log.Trace("Account created: %s", u.UserName)

// Auto set Admin if first user
if models.CountUsers() == 1 {
u.IsAdmin = true
u.IsActive = true // bypass email activation
if err := models.UpdateUser(u); err != nil {
ctx.Handle(500, "UpdateUser", err)
return
}
}

// TODO: send activation email

ctx.Redirect(setting.AppSubURL + "/user/login")
}

// Logout
func Logout(ctx *context.Context) {
ctx.Session.Delete("uid")
ctx.Session.Delete("uname")
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL)
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL)
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
ctx.Redirect(setting.AppSubURL + "/")
}

+ 19
- 1
setting/setting.go View File

@@ -78,7 +78,14 @@ var (
CSRFCookieName string

// Security settings
SecretKey string
InstallLock bool
SecretKey string
LoginRememberDays int
CookieUserName string
CookieRememberName string
CookieSecure bool
EnableLoginStatusCookie bool
LoginStatusCookieName string

// Cache settings
CacheAdapter string
@@ -225,6 +232,17 @@ func InitConfig() {
Names = Cfg.Section("i18n").Key("NAMES").Strings(",")
dateLangs = Cfg.Section("i18n.datelang").KeysHash()

sec = Cfg.Section("security")
InstallLock = sec.Key("INSTALL_LOCK").MustBool()
SecretKey = sec.Key("SECRET_KEY").String()
LoginRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt()
CookieUserName = sec.Key("COOKIE_USERNAME").String()
CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").String()
CookieSecure = sec.Key("COOKIE_SECURE").MustBool(false)
EnableLoginStatusCookie = sec.Key("ENABLE_LOGIN_STATUS_COOKIE").MustBool(false)
LoginStatusCookieName = sec.Key("LOGIN_STATUS_COOKIE_NAME").MustString("login_status")


initLogging()
initSession()
initCache()

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

@@ -45,12 +45,26 @@ footer .right {
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
.form-signin input[id="user_name"] {
margin-bottom: -15px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
.form-signin input[id="email"] {
margin-bottom: -15px;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[id="password"] {
margin-bottom: -15px;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.form-signin input[id="repeat"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;

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

@@ -0,0 +1,62 @@
// 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 auth

import (
"strings"
"dev.sigpipe.me/dashie/git.txt/models"
"dev.sigpipe.me/dashie/git.txt/models/errors"

"github.com/go-macaron/session"
log "gopkg.in/clog.v1"
"gopkg.in/macaron.v1"
)

func IsAPIPath(url string) bool {
return strings.HasPrefix(url, "/api/")
}

// SignedInID returns the id of signed in user.
func SignedInID(ctx *macaron.Context, sess session.Store) int64 {
if !models.HasEngine {
return 0
}

uid := sess.Get("uid")
if uid == nil {
return 0
}
if id, ok := uid.(int64); ok {
if _, err := models.GetUserByID(id); err != nil {
if !errors.IsUserNotExist(err) {
log.Error(2, "GetUserByID: %v", err)
}
return 0
}
return id
}
return 0
}

// SignedInUser returns the user object of signed user.
// It returns a bool value to indicate whether user uses basic auth or not.
func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) {
if !models.HasEngine {
return nil, false
}

uid := SignedInID(ctx, sess)

if uid <= 0 {
return nil, false
}

u, err := models.GetUserByID(uid)
if err != nil {
log.Error(4, "GetUserById: %v", err)
return nil, false
}
return u, false
}

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

@@ -0,0 +1,29 @@
package form

import (
"github.com/go-macaron/binding"
"gopkg.in/macaron.v1"
)

// Register
type Register struct {
UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"`
Email string `binding:"Required;Email;MaxSize(254)"`
Password string `binding:"Required;MaxSize(255)"`
Repeat string
}

func (f *Register) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}

// Login
type Login struct {
UserName string `binding:"Required;MaxSize(254)"`
Password string `binding:"Required;MaxSize(255)"`
Remember bool
}

func (f *Login) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}

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

@@ -0,0 +1,153 @@
package form

import (
"reflect"
"github.com/go-macaron/binding"
"gopkg.in/macaron.v1"
"github.com/Unknwon/com"
"strings"
"regexp"
"fmt"
log "gopkg.in/clog.v1"
)

const ERR_ALPHA_DASH_DOT_SLASH = "AlphaDashDotSlashError"

var AlphaDashDotSlashPattern = regexp.MustCompile("[^\\d\\w-_\\./]")

func init() {
binding.SetNameMapper(com.ToSnakeCase)
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return rule == "AlphaDashDotSlash"
},
IsValid: func(errs binding.Errors, name string, v interface{}) (bool, binding.Errors) {
if AlphaDashDotSlashPattern.MatchString(fmt.Sprintf("%v", v)) {
errs.Add([]string{name}, ERR_ALPHA_DASH_DOT_SLASH, "AlphaDashDotSlash")
return false, errs
}
return true, errs
},
})
}

type Form interface {
binding.Validator
}

// Assign assign form values back to the template data.
func Assign(form interface{}, data map[string]interface{}) {
typ := reflect.TypeOf(form)
val := reflect.ValueOf(form)

if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
val = val.Elem()
}

for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)

fieldName := field.Tag.Get("form")
// Allow ignored fields in the struct
if fieldName == "-" {
continue
} else if len(fieldName) == 0 {
fieldName = com.ToSnakeCase(field.Name)
}

data[fieldName] = val.Field(i).Interface()
}
}

func getRuleBody(field reflect.StructField, prefix string) string {
for _, rule := range strings.Split(field.Tag.Get("binding"), ";") {
if strings.HasPrefix(rule, prefix) {
return rule[len(prefix) : len(rule)-1]
}
}
return ""
}

func getSize(field reflect.StructField) string {
return getRuleBody(field, "Size(")
}

func getMinSize(field reflect.StructField) string {
return getRuleBody(field, "MinSize(")
}

func getMaxSize(field reflect.StructField) string {
return getRuleBody(field, "MaxSize(")
}

func getInclude(field reflect.StructField) string {
return getRuleBody(field, "Include(")
}

func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaron.Locale) binding.Errors {
log.Trace("Validating form")

if errs.Len() == 0 {
return errs
}

data["HasError"] = true
Assign(f, data)

typ := reflect.TypeOf(f)
val := reflect.ValueOf(f)

if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
val = val.Elem()
}

for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)

fieldName := field.Tag.Get("form")
// Allow ignored fields in the struct
if fieldName == "-" {
continue
}

if errs[0].FieldNames[0] == field.Name {
data["Err_"+field.Name] = true

trName := field.Tag.Get("locale")
if len(trName) == 0 {
trName = l.Tr("form." + field.Name)
} else {
trName = l.Tr(trName)
}

switch errs[0].Classification {
case binding.ERR_REQUIRED:
data["ErrorMsg"] = trName + l.Tr("form.require_error")
case binding.ERR_ALPHA_DASH:
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
case binding.ERR_ALPHA_DASH_DOT:
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error")
case ERR_ALPHA_DASH_DOT_SLASH:
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_slash_error")
case binding.ERR_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.size_error", getSize(field))
case binding.ERR_MIN_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.min_size_error", getMinSize(field))
case binding.ERR_MAX_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.max_size_error", getMaxSize(field))
case binding.ERR_EMAIL:
data["ErrorMsg"] = trName + l.Tr("form.email_error")
case binding.ERR_URL:
data["ErrorMsg"] = trName + l.Tr("form.url_error")
case binding.ERR_INCLUDE:
data["ErrorMsg"] = trName + l.Tr("form.include_error", getInclude(field))
default:
data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification
}
return errs
}
}
return errs
}

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

@@ -4,8 +4,12 @@ import (
"encoding/hex"
"crypto/sha1"
"crypto/md5"
"math/big"
"crypto/rand"
)

const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

// MD5Bytes encodes string to MD5 bytes.
func MD5Bytes(str string) []byte {
m := md5.New()
@@ -31,4 +35,31 @@ func ShortSHA1(sha1 string) string {
return sha1[:10]
}
return sha1
}

// RandomString returns generated random string in given length of characters.
// It also returns possible error during generation.
func RandomString(n int) (string, error) {
buffer := make([]byte, n)
max := big.NewInt(int64(len(alphanum)))

for i := 0; i < n; i++ {
index, err := randomInt(max)
if err != nil {
return "", err
}

buffer[i] = alphanum[index]
}

return string(buffer), nil
}

func randomInt(max *big.Int) (int, error) {
rand, err := rand.Int(rand.Reader, max)
if err != nil {
return 0, err
}

return int(rand.Int64()), nil
}

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

@@ -1,20 +1,24 @@
{{if .Flash.ErrorMsg}}
<div class="ui negative message">
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>{{.Flash.ErrorMsg | Str2html}}</p>
</div>
{{end}}
{{if .Flash.WarningMsg}}
<div class="ui warning message">
<div class="alert alert-warning alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>{{.Flash.WarningMsg | Str2html}}</p>
</div>
{{end}}
{{if .Flash.SuccessMsg}}
<div class="ui positive message">
<div class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>{{.Flash.SuccessMsg | Str2html}}</p>
</div>
{{end}}
{{if .Flash.InfoMsg}}
<div class="ui info message">
<div class="alert alert-info alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>{{.Flash.InfoMsg | Str2html}}</p>
</div>
{{end}}

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

@@ -45,10 +45,10 @@
<li><a href="{{AppSubURL}}/new">New git.txt</a></li>
{{end}}
{{if .IsLogged}}
<li><a href="{{AppSubURL}}/logout">Logout</a></li>
<li><a href="{{AppSubURL}}/user/logout">Logout</a></li>
{{else}}
{{if CanRegister}}
<li><a href="{{AppSubURL}}/register">Register</a></li>
<li><a href="{{AppSubURL}}/user/register">Register</a></li>
{{end}}
<li><a href="{{AppSubURL}}/user/login">Login</a></li>
{{end}}
@@ -58,6 +58,7 @@
</nav>

<div class="container">
{{template "base/alert" .}}
{{/*
</div>
</body>

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

@@ -1,14 +1,19 @@
{{template "base/head" .}}


<form class="form-signin" action="{{.Link}}" method="post">
{{.CSRFTokenHTML}}
<h2 class="form-signin-heading">{{.i18n.Tr "login.sign_in"}}</h2>
{{template "base/alert" .}}
<label for="inputEmail" class="sr-only">{{.i18n.Tr "login.email"}}</label>
<input type="email" id="inputEmail" class="form-control {{if .Err_UserName}}error{{end}}" placeholder='{{.i18n.Tr "login.email_placeholder"}}' required autofocus>
<label for="inputPassword" class="sr-only">{{.i18n.Tr "login.password"}}</label>
<input type="password" id="inputPassword" class="form-control {{if .Err_UserName}}error{{end}}" placeholder='{{.i18n.Tr "login.password_placeholder"}}' required>

<div class="user_name form-group {{if .Err_UserName}}has-error{{end}}">
<label for="user_name" class="sr-only">{{.i18n.Tr "login.username"}}</label>
<input type="string" name="user_name" id="user_name" class="form-control" placeholder='{{.i18n.Tr "login.username_placeholder"}}' required autofocus>
</div>

<div class="password form-group {{if .Err_Password}}has-error{{end}}">
<label for="password" class="sr-only">{{.i18n.Tr "login.password"}}</label>
<input type="password" name="password" id="password" class="form-control" placeholder='{{.i18n.Tr "login.password_placeholder"}}' required>
</div>

<div class="checkbox">
<label>
<input type="checkbox" value="remember-me"> {{.i18n.Tr "login.remember_me"}}

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

@@ -0,0 +1,30 @@
{{template "base/head" .}}

<form class="form-signin" action="{{.Link}}" method="post">
{{.CSRFTokenHTML}}
<h2 class="form-signin-heading">{{.i18n.Tr "register.register"}}</h2>

<div class="user_name form-group {{if .Err_UserName}}has-error{{end}}">
<label for="user_name" class="sr-only">{{.i18n.Tr "login.username"}}</label>
<input type="text" name="user_name" id="user_name" class="form-control" value="{{.user_name}}" placeholder='{{.i18n.Tr "login.username_placeholder"}}' required autofocus>
</div>

<div class="email form-group {{if .Err_Email}}has-error{{end}}">
<label for="email" class="sr-only">{{.i18n.Tr "login.email"}}</label>
<input type="email" name="email" id="email" class="form-control" value="{{.email}}" placeholder='{{.i18n.Tr "login.email_placeholder"}}' required autofocus>
</div>

<div class="password form-group {{if .Err_Password}}has-error{{end}}">
<label for="password" class="sr-only">{{.i18n.Tr "login.password"}}</label>
<input type="password" name="password" id="password" class="form-control" value="{{.password}}" placeholder='{{.i18n.Tr "login.password_placeholder"}}' required>
</div>

<div class="repeat form-group {{if .Err_Repeat}}has-error{{end}}">
<label for="repeat" class="sr-only">{{.i18n.Tr "login.password"}}</label>
<input type="password" name="repeat" id="repeat" class="form-control" value="{{.repeat}}" placeholder='{{.i18n.Tr "login.repeat_password_placeholder"}}' required>
</div>

<button class="btn btn-lg btn-primary btn-block" type="submit">{{.i18n.Tr "login.sign_in"}}</button>
</form>

{{template "base/footer" .}}

Loading…
Cancel
Save