diff --git a/.gitignore b/.gitignore index ad27cc8be8..d201223ef9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ gogs *.db *.log custom/ +data/ .vendor/ .idea/ *.iml \ No newline at end of file diff --git a/.gopmfile b/.gopmfile index 5b690a06a7..6e6b59c620 100644 --- a/.gopmfile +++ b/.gopmfile @@ -4,7 +4,6 @@ path=github.com/gogits/gogs [deps] github.com/codegangsta/cli= github.com/codegangsta/martini= -github.com/martini-contrib/sessions= github.com/Unknwon/com= github.com/Unknwon/cae= github.com/Unknwon/goconfig= diff --git a/README.md b/README.md index cbd1f588df..504c21975b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ -Gogs - Go Git Service [![wercker status](https://app.wercker.com/status/ad0bdb0bc450ac6f09bc56b9640a50aa/s/ "wercker status")](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [![Go Walker](http://gowalker.org/api/v1/badge)](https://gowalker.org/github.com/gogits/gogs) +Gogs - Go Git Service [![wercker status](https://app.wercker.com/status/ad0bdb0bc450ac6f09bc56b9640a50aa/s/ "wercker status")](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [![Build Status](https://drone.io/github.com/gogits/gogs/status.png)](https://drone.io/github.com/gogits/gogs/latest) ===================== -Gogs(Go Git Service) is a GitHub-like clone in the Go Programming Language. +Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language. -Since we choose to use pure Go implementation of Git manipulation, Gogs certainly supports **ALL platforms** that Go supports, including Linux, Max OS X, and Windows with **ZERO** dependency. +![Demo](http://gowalker.org/public/gogs_demo.gif) -##### Current version: 0.1.5 Alpha +##### Current version: 0.1.6 Alpha + +[简体中文](README_ZH.md) ## Purpose -There are some very good products in this category such as [gitlab](http://gitlab.com), but the environment setup steps often make us crazy. So our goal of Gogs is to build a GitHub-like clone with very easy setup steps, which take advantages of the Go Programming Language. +Since we choose to use pure Go implementation of Git manipulation, Gogs certainly supports **ALL platforms** that Go supports, including Linux, Mac OS X, and Windows with **ZERO** dependency. + +More importantly, Gogs only needs one binary to setup your own project hosting on the fly! ## Overview @@ -23,7 +27,7 @@ There are some very good products in this category such as [gitlab](http://gitla - Activity timeline - SSH protocol support. - Register/delete account. -- Create/delete public repository. +- Create/delete/watch public repository. - User profile page. - Repository viewer. - Gravatar support. @@ -42,8 +46,9 @@ There are two ways to install Gogs: ## Acknowledgments -- Mail service is based on [WeTalk](https://github.com/beego/wetalk). - Logo is inspired by [martini](https://github.com/martini-contrib). +- Mail Service, modules design is inspired by [WeTalk](https://github.com/beego/wetalk). +- System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog). ## Contributors diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000000..0ab8dfdd07 --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,53 @@ +Gogs - Go Git Service [![wercker status](https://app.wercker.com/status/ad0bdb0bc450ac6f09bc56b9640a50aa/s/ "wercker status")](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [![Build Status](https://drone.io/github.com/gogits/gogs/status.png)](https://drone.io/github.com/gogits/gogs/latest) +===================== + +Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。 + +![Demo](http://gowalker.org/public/gogs_demo.gif) + +##### 当前版本:0.1.6 Alpha + +## 开发目的 + +Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依赖,并且支持 Go 语言所支持的 **所有平台**,包括 Linux、Mac OS X 以及 Windows。 + +更重要的是,您只需要一个可执行文件就能借助 Gogs 快速搭建属于您自己的代码托管服务! + +## 项目概览 + +- 有关项目设计、开发说明、变更日志和路线图,请通过 [Wiki](https://github.com/gogits/gogs/wiki) 查看。 +- 您可以到 [Trello Broad](https://trello.com/b/uxAoeLUl/gogs-go-git-service) 跟随开发团队的脚步。 +- 想要先睹为快?通过 [在线体验](http://try.gogits.org/Unknown/gogs) 或查看 **安装部署 -> 二进制安装** 小节。 +- 使用过程中遇到问题?尝试从 [故障排查](https://github.com/gogits/gogs/wiki/Troubleshooting) 页面获取帮助。 + +## 功能特性 + +- 活动时间线 +- SSH 协议支持 +- 注册/删除用户 +- 创建/删除/关注公开仓库 +- 用户个人信息页面 +- 仓库浏览器 +- Gravatar 支持 +- 邮件服务(注册) +- 管理员面板 +- 支持 MySQL、PostgreSQL 以及 SQLite3(仅限二进制版本) + +## 安装部署 + +在安装 Gogs 之前,您需要先安装 [基本环境](https://github.com/gogits/gogs/wiki/Prerequirements)。 + +然后,您可以通过以下两种方式来安装 Gogs: + +- [二进制安装](https://github.com/gogits/gogs/wiki/Install-from-binary): **强烈推荐** 适合体验者和实际部署 +- [源码安装](https://github.com/gogits/gogs/wiki/Install-from-source) + +## 特别鸣谢 + +- Logo 基于 [martini](https://github.com/martini-contrib) 修改而来。 +- 邮件服务、模块设计基于 [WeTalk](https://github.com/beego/wetalk) 修改而来。 +- 系统监视状态基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改而来。 + +## 贡献成员 + +本项目最初由 [Unknown](https://github.com/Unknwon) 和 [lunny](https://github.com/lunny) 发起,随后 [fuxiaohei](https://github.com/fuxiaohei) 与 [slene](https://github.com/slene) 加入到开发团队。您可以通过查看 [贡献者页面](https://github.com/gogits/gogs/graphs/contributors) 获取完整的贡献者列表。 \ No newline at end of file diff --git a/conf/app.ini b/conf/app.ini index ecb0d2511f..b051557f41 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -34,6 +34,10 @@ PATH = data/gogs.db [security] ; !!CHANGE THIS TO KEEP YOUR USER DATA SAFE!! SECRET_KEY = !#@FDEWREWR&*( +; Auto-login remember days +LOGIN_REMEMBER_DAYS = 7 +COOKIE_USERNAME = gogs_awesome +COOKIE_REMEMBER_NAME = gogs_incredible [service] ACTIVE_CODE_LIVE_MINUTES = 180 @@ -44,6 +48,8 @@ REGISTER_EMAIL_CONFIRM = false DISENABLE_REGISTERATION = false ; User must sign in to view anything. REQUIRE_SIGNIN_VIEW = false +; Cache avatar as picture +ENABLE_CACHE_AVATAR = false [mailer] ENABLED = false @@ -70,8 +76,38 @@ INTERVAL = 60 ; memcache: "127.0.0.1:11211" HOST = +[session] +; Either "memory", "file", "redis" or "mysql", default is "memory" +PROVIDER = file +; Provider config options +; memory: not have any config yet +; file: session file path, e.g. data/sessions +; redis: config like redis server addr, poolSize, password, e.g. 127.0.0.1:6379,100,astaxie +; mysql: go-sql-driver/mysql dsn config string, e.g. root:password@/session_table +PROVIDER_CONFIG = data/sessions +; Session cookie name +COOKIE_NAME = i_like_gogits +; If you use session in https only, default is false +COOKIE_SECURE = false +; Enable set cookie, default is true +ENABLE_SET_COOKIE = true +; Session GC time interval, default is 86400 +GC_INTERVAL_TIME = 86400 +; Session life time, default is 86400 +SESSION_LIFE_TIME = 86400 +; session id hash func, Either "sha1", "sha256" or "md5" default is sha1 +SESSION_ID_HASHFUNC = sha1 +; Session hash key, default is use random string +SESSION_ID_HASHKEY = + +[picture] +; The place to picture data, either "server" or "qiniu", default is "server" +SERVICE = server +; For "server" only, root path of picture data, default is "data/pictures" +PATH = data/pictures + [log] -; Either "console", "file", "conn" or "smtp", default is "console" +; Either "console", "file", "conn", "smtp" or "database", default is "console" MODE = console ; Buffer length of channel, keep it as it is if you don't know what it is. BUFFER_LEN = 10000 @@ -120,4 +156,10 @@ HOST = USER = PASSWD = ; Receivers, can be one or more, e.g. ["1@example.com","2@example.com"] -RECEIVERS = \ No newline at end of file +RECEIVERS = + +; For "database" mode only +[log.database] +LEVEL = +Driver = +CONN = \ No newline at end of file diff --git a/gogs.go b/gogs.go index 41df79280a..0bdbbc0697 100644 --- a/gogs.go +++ b/gogs.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -// gogs(Go Git Service) is a Go clone of Github. +// Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language. package main import ( @@ -20,7 +20,7 @@ import ( // Test that go1.2 tag above is included in builds. main.go refers to this definition. const go12tag = true -const APP_VER = "0.1.5.0321" +const APP_VER = "0.1.6.0323.1" func init() { base.AppVer = APP_VER diff --git a/models/action.go b/models/action.go index d338da24ba..cfb124363c 100644 --- a/models/action.go +++ b/models/action.go @@ -7,6 +7,8 @@ package models import ( "encoding/json" "time" + + "github.com/gogits/gogs/modules/log" ) // Operation types of user action. @@ -28,7 +30,7 @@ type Action struct { ActUserName string // Action user name. RepoId int64 RepoName string - Content string `xorm:"varchar(1000)"` + Content string `xorm:"TEXT"` Created time.Time `xorm:"created"` } @@ -79,6 +81,18 @@ func CommitRepoAction(userId int64, userName string, }) return err } + + // Update repository last update time. + repo, err := GetRepositoryByName(userId, repoName) + if err != nil { + return err + } + repo.IsBare = false + if err = UpdateRepository(repo); err != nil { + return err + } + + log.Trace("action.CommitRepoAction: %d/%s", userId, repo.LowerName) return nil } @@ -92,6 +106,8 @@ func NewRepoAction(user *User, repo *Repository) error { RepoId: repo.Id, RepoName: repo.Name, }) + + log.Trace("action.NewRepoAction: %s/%s", user.LowerName, repo.LowerName) return err } diff --git a/models/issue.go b/models/issue.go index c669d201f6..f78c240cbc 100644 --- a/models/issue.go +++ b/models/issue.go @@ -4,16 +4,155 @@ package models +import ( + "errors" + "strings" + "time" + + "github.com/gogits/gogs/modules/base" +) + +var ( + ErrIssueNotExist = errors.New("Issue does not exist") +) + +// Issue represents an issue or pull request of repository. type Issue struct { - Id int64 - RepoId int64 `xorm:"index"` - PosterId int64 + Id int64 + Index int64 // Index in one repository. + Name string + RepoId int64 `xorm:"index"` + PosterId int64 + MilestoneId int64 + AssigneeId int64 + IsPull bool // Indicates whether is a pull request or not. + IsClosed bool + Labels string `xorm:"TEXT"` + Mentions string `xorm:"TEXT"` + Content string `xorm:"TEXT"` + NumComments int + Created time.Time `xorm:"created"` + Updated time.Time `xorm:"updated"` } -type PullRequest struct { - Id int64 +// CreateIssue creates new issue for repository. +func CreateIssue(userId, repoId, milestoneId, assigneeId int64, name, labels, content string, isPull bool) (*Issue, error) { + count, err := GetIssueCount(repoId) + if err != nil { + return nil, err + } + + // TODO: find out mentions + mentions := "" + + issue := &Issue{ + Index: count + 1, + Name: name, + RepoId: repoId, + PosterId: userId, + MilestoneId: milestoneId, + AssigneeId: assigneeId, + IsPull: isPull, + Labels: labels, + Mentions: mentions, + Content: content, + } + _, err = orm.Insert(issue) + return issue, err } +// GetIssueCount returns count of issues in the repository. +func GetIssueCount(repoId int64) (int64, error) { + return orm.Count(&Issue{RepoId: repoId}) +} + +// GetIssueById returns issue object by given id. +func GetIssueById(id int64) (*Issue, error) { + issue := new(Issue) + has, err := orm.Id(id).Get(issue) + if err != nil { + return nil, err + } else if !has { + return nil, ErrIssueNotExist + } + return issue, nil +} + +// GetIssues returns a list of issues by given conditions. +func GetIssues(userId, repoId, posterId, milestoneId int64, page int, isClosed, isMention bool, labels, sortType string) ([]Issue, error) { + sess := orm.Limit(20, (page-1)*20) + + if repoId > 0 { + sess = sess.Where("repo_id=?", repoId).And("is_closed=?", isClosed) + } else { + sess = sess.Where("is_closed=?", isClosed) + } + + if userId > 0 { + sess = sess.And("assignee_id=?", userId) + } else if posterId > 0 { + sess = sess.And("poster_id=?", posterId) + } else if isMention { + sess = sess.And("mentions like '%$" + base.ToStr(userId) + "|%'") + } + + if milestoneId > 0 { + sess = sess.And("milestone_id=?", milestoneId) + } + + if len(labels) > 0 { + for _, label := range strings.Split(labels, ",") { + sess = sess.And("mentions like '%$" + label + "|%'") + } + } + + switch sortType { + case "oldest": + sess = sess.Asc("created") + case "recentupdate": + sess = sess.Desc("updated") + case "leastupdate": + sess = sess.Asc("updated") + case "mostcomment": + sess = sess.Desc("num_comments") + case "leastcomment": + sess = sess.Asc("num_comments") + default: + sess = sess.Desc("created") + } + + var issues []Issue + err := sess.Find(&issues) + return issues, err +} + +// Label represents a list of labels of repository for issues. +type Label struct { + Id int64 + RepoId int64 `xorm:"index"` + Names string + Colors string +} + +// Milestone represents a milestone of repository. +type Milestone struct { + Id int64 + Name string + RepoId int64 `xorm:"index"` + IsClosed bool + Content string + NumIssues int + DueDate time.Time + Created time.Time `xorm:"created"` +} + +// Comment represents a comment in commit and issue page. type Comment struct { - Id int64 + Id int64 + PosterId int64 + IssueId int64 + CommitId int64 + Line int + Content string + Created time.Time `xorm:"created"` } diff --git a/models/models.go b/models/models.go index 7f5f626f5e..ad19a929fb 100644 --- a/models/models.go +++ b/models/models.go @@ -72,7 +72,7 @@ func setEngine() { func NewEngine() { setEngine() if err := orm.Sync(new(User), new(PublicKey), new(Repository), new(Watch), - new(Action), new(Access)); err != nil { + new(Action), new(Access), new(Issue)); err != nil { fmt.Printf("sync database struct error: %v\n", err) os.Exit(2) } diff --git a/models/publickey.go b/models/publickey.go index c69bca681d..3f2fcabd3b 100644 --- a/models/publickey.go +++ b/models/publickey.go @@ -19,6 +19,8 @@ import ( "time" "github.com/Unknwon/com" + + "github.com/gogits/gogs/modules/log" ) const ( @@ -78,7 +80,7 @@ type PublicKey struct { OwnerId int64 `xorm:"index"` Name string `xorm:"unique not null"` Fingerprint string - Content string `xorm:"text not null"` + Content string `xorm:"TEXT not null"` Created time.Time `xorm:"created"` Updated time.Time `xorm:"updated"` } @@ -99,8 +101,8 @@ func AddPublicKey(key *PublicKey) (err error) { } // Calculate fingerprint. - tmpPath := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()), - "id_rsa.pub") + tmpPath := strings.Replace(filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()), + "id_rsa.pub"), "\\", "/", -1) os.MkdirAll(path.Dir(tmpPath), os.ModePerm) if err = ioutil.WriteFile(tmpPath, []byte(key.Content), os.ModePerm); err != nil { return err @@ -127,25 +129,11 @@ func AddPublicKey(key *PublicKey) (err error) { return nil } -// DeletePublicKey deletes SSH key information both in database and authorized_keys file. -func DeletePublicKey(key *PublicKey) (err error) { - // Delete SSH key in database. - has, err := orm.Id(key.Id).Get(key) - if err != nil { - return err - } else if !has { - return errors.New("Public key does not exist") - } - if _, err = orm.Delete(key); err != nil { - return err - } - +func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { // Delete SSH key in SSH key file. sshOpLocker.Lock() defer sshOpLocker.Unlock() - p := filepath.Join(sshPath, "authorized_keys") - tmpP := filepath.Join(sshPath, "authorized_keys.tmp") fr, err := os.Open(p) if err != nil { return err @@ -188,8 +176,29 @@ func DeletePublicKey(key *PublicKey) (err error) { break } } + return nil +} - if err = os.Remove(p); err != nil { +// DeletePublicKey deletes SSH key information both in database and authorized_keys file. +func DeletePublicKey(key *PublicKey) (err error) { + // Delete SSH key in database. + has, err := orm.Id(key.Id).Get(key) + if err != nil { + return err + } else if !has { + return errors.New("Public key does not exist") + } + if _, err = orm.Delete(key); err != nil { + return err + } + + p := filepath.Join(sshPath, "authorized_keys") + tmpP := filepath.Join(sshPath, "authorized_keys.tmp") + log.Trace("ssh.DeletePublicKey(authorized_keys): %s", p) + + if err = rewriteAuthorizedKeys(key, p, tmpP); err != nil { + return err + } else if err = os.Remove(p); err != nil { return err } return os.Rename(tmpP, p) diff --git a/models/repo.go b/models/repo.go index 4972661cb3..a37923c8b1 100644 --- a/models/repo.go +++ b/models/repo.go @@ -83,10 +83,11 @@ type Repository struct { Name string `xorm:"index not null"` Description string Website string - Private bool NumWatches int NumStars int NumForks int + IsPrivate bool + IsBare bool Created time.Time `xorm:"created"` Updated time.Time `xorm:"updated"` } @@ -139,7 +140,8 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv Name: repoName, LowerName: strings.ToLower(repoName), Description: desc, - Private: private, + IsPrivate: private, + IsBare: repoLang == "" && license == "" && !initReadme, } repoPath := RepoPath(user.Name, repoName) @@ -369,6 +371,18 @@ func RepoPath(userName, repoName string) string { return filepath.Join(UserPath(userName), repoName+".git") } +func UpdateRepository(repo *Repository) error { + if len(repo.Description) > 255 { + repo.Description = repo.Description[:255] + } + if len(repo.Website) > 255 { + repo.Website = repo.Website[:255] + } + + _, err := orm.Id(repo.Id).UseBool().Cols("description", "website").Update(repo) + return err +} + // DeleteRepository deletes a repository for a user or orgnaztion. func DeleteRepository(userId, repoId int64, userName string) (err error) { repo := &Repository{Id: repoId, OwnerId: userId} @@ -413,9 +427,9 @@ func DeleteRepository(userId, repoId int64, userName string) (err error) { } // GetRepositoryByName returns the repository by given name under user if exists. -func GetRepositoryByName(user *User, repoName string) (*Repository, error) { +func GetRepositoryByName(userId int64, repoName string) (*Repository, error) { repo := &Repository{ - OwnerId: user.Id, + OwnerId: userId, LowerName: strings.ToLower(repoName), } has, err := orm.Get(repo) diff --git a/models/user.go b/models/user.go index 3c11091285..9333d1ee67 100644 --- a/models/user.go +++ b/models/user.go @@ -201,7 +201,14 @@ func VerifyUserActiveCode(code string) (user *User) { // UpdateUser updates user's information. func UpdateUser(user *User) (err error) { - _, err = orm.Id(user.Id).UseBool().Update(user) + if len(user.Location) > 255 { + user.Location = user.Location[:255] + } + if len(user.Website) > 255 { + user.Website = user.Website[:255] + } + + _, err = orm.Id(user.Id).UseBool().Cols("website", "location").Update(user) return err } @@ -279,9 +286,7 @@ func GetUserByName(name string) (*User, error) { if len(name) == 0 { return nil, ErrUserNotExist } - user := &User{ - LowerName: strings.ToLower(name), - } + user := &User{LowerName: strings.ToLower(name)} has, err := orm.Get(user) if err != nil { return nil, err diff --git a/modules/auth/auth.go b/modules/auth/auth.go index 0e87168891..2e0555f6df 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -61,6 +61,7 @@ func (f *RegisterForm) Validate(errors *binding.Errors, req *http.Request, conte type LogInForm struct { UserName string `form:"username" binding:"Required;AlphaDash;MaxSize(30)"` Password string `form:"passwd" binding:"Required;MinSize(6);MaxSize(30)"` + Remember string `form:"remember"` } func (f *LogInForm) Name(field string) string { diff --git a/modules/auth/issue.go b/modules/auth/issue.go new file mode 100644 index 0000000000..e2b1f9f2a7 --- /dev/null +++ b/modules/auth/issue.go @@ -0,0 +1,54 @@ +// 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 ( + "net/http" + "reflect" + + "github.com/codegangsta/martini" + + "github.com/gogits/binding" + + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" +) + +type CreateIssueForm struct { + IssueName string `form:"name" binding:"Required;MaxSize(50)"` + RepoId int64 `form:"repoid" binding:"Required"` + MilestoneId int64 `form:"milestoneid" binding:"Required"` + AssigneeId int64 `form:"assigneeid"` + Labels string `form:"labels"` + Content string `form:"content"` +} + +func (f *CreateIssueForm) Name(field string) string { + names := map[string]string{ + "IssueName": "Issue name", + "RepoId": "Repository ID", + "MilestoneId": "Milestone ID", + } + return names[field] +} + +func (f *CreateIssueForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { + if req.Method == "GET" || errors.Count() == 0 { + return + } + + data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) + data["HasError"] = true + AssignForm(f, data) + + if len(errors.Overall) > 0 { + for _, err := range errors.Overall { + log.Error("CreateIssueForm.Validate: %v", err) + } + return + } + + validate(errors, data, f) +} diff --git a/modules/auth/user.go b/modules/auth/user.go index f8d8f66149..cb8db1b29a 100644 --- a/modules/auth/user.go +++ b/modules/auth/user.go @@ -9,7 +9,8 @@ import ( "reflect" "github.com/codegangsta/martini" - "github.com/martini-contrib/sessions" + + "github.com/gogits/session" "github.com/gogits/binding" @@ -19,7 +20,7 @@ import ( ) // SignedInId returns the id of signed in user. -func SignedInId(session sessions.Session) int64 { +func SignedInId(session session.SessionStore) int64 { userId := session.Get("userId") if userId == nil { return 0 @@ -34,7 +35,7 @@ func SignedInId(session sessions.Session) int64 { } // SignedInName returns the name of signed in user. -func SignedInName(session sessions.Session) string { +func SignedInName(session session.SessionStore) string { userName := session.Get("userName") if userName == nil { return "" @@ -46,7 +47,7 @@ func SignedInName(session sessions.Session) string { } // SignedInUser returns the user object of signed user. -func SignedInUser(session sessions.Session) *models.User { +func SignedInUser(session session.SessionStore) *models.User { id := SignedInId(session) if id <= 0 { return nil @@ -61,7 +62,7 @@ func SignedInUser(session sessions.Session) *models.User { } // IsSignedIn check if any user has signed in. -func IsSignedIn(session sessions.Session) bool { +func IsSignedIn(session session.SessionStore) bool { return SignedInId(session) > 0 } diff --git a/modules/base/conf.go b/modules/base/conf.go index 863daca644..19f587077b 100644 --- a/modules/base/conf.go +++ b/modules/base/conf.go @@ -16,6 +16,7 @@ import ( "github.com/Unknwon/goconfig" "github.com/gogits/cache" + "github.com/gogits/session" "github.com/gogits/gogs/modules/log" ) @@ -37,21 +38,33 @@ var ( RunUser string RepoRootPath string + LogInRememberDays int + CookieUserName string + CookieRememberName string + Cfg *goconfig.ConfigFile MailService *Mailer + LogMode string + LogConfig string + Cache cache.Cache CacheAdapter string CacheConfig string - LogMode string - LogConfig string + SessionProvider string + SessionConfig *session.Config + SessionManager *session.Manager + + PictureService string + PictureRootPath string ) var Service struct { RegisterEmailConfirm bool DisenableRegisteration bool RequireSignInView bool + EnableCacheAvatar bool ActiveCodeLives int ResetPwdCodeLives int } @@ -82,6 +95,7 @@ func newService() { Service.ResetPwdCodeLives = Cfg.MustInt("service", "RESET_PASSWD_CODE_LIVE_MINUTES", 180) Service.DisenableRegisteration = Cfg.MustBool("service", "DISENABLE_REGISTERATION", false) Service.RequireSignInView = Cfg.MustBool("service", "REQUIRE_SIGNIN_VIEW", false) + Service.EnableCacheAvatar = Cfg.MustBool("service", "ENABLE_CACHE_AVATAR", false) } func newLogService() { @@ -129,6 +143,10 @@ func newLogService() { Cfg.MustValue(modeSec, "HOST", "127.0.0.1:25"), Cfg.MustValue(modeSec, "RECEIVERS", "[]"), Cfg.MustValue(modeSec, "SUBJECT", "Diagnostic message from serve")) + case "database": + LogConfig = fmt.Sprintf(`{"level":%s,"driver":%s,"conn":%s}`, level, + Cfg.MustValue(modeSec, "Driver"), + Cfg.MustValue(modeSec, "CONN")) } log.NewLogger(Cfg.MustInt64("log", "BUFFER_LEN", 10000), LogMode, LogConfig) @@ -159,6 +177,34 @@ func newCacheService() { log.Info("Cache Service Enabled") } +func newSessionService() { + SessionProvider = Cfg.MustValue("session", "PROVIDER", "memory") + + SessionConfig = new(session.Config) + SessionConfig.ProviderConfig = Cfg.MustValue("session", "PROVIDER_CONFIG") + SessionConfig.CookieName = Cfg.MustValue("session", "COOKIE_NAME", "i_like_gogits") + SessionConfig.CookieSecure = Cfg.MustBool("session", "COOKIE_SECURE") + SessionConfig.EnableSetCookie = Cfg.MustBool("session", "ENABLE_SET_COOKIE", true) + SessionConfig.GcIntervalTime = Cfg.MustInt64("session", "GC_INTERVAL_TIME", 86400) + SessionConfig.SessionLifeTime = Cfg.MustInt64("session", "SESSION_LIFE_TIME", 86400) + SessionConfig.SessionIDHashFunc = Cfg.MustValue("session", "SESSION_ID_HASHFUNC", "sha1") + SessionConfig.SessionIDHashKey = Cfg.MustValue("session", "SESSION_ID_HASHKEY") + + if SessionProvider == "file" { + os.MkdirAll(path.Dir(SessionConfig.ProviderConfig), os.ModePerm) + } + + var err error + SessionManager, err = session.NewManager(SessionProvider, *SessionConfig) + if err != nil { + fmt.Printf("Init session system failed, provider: %s, %v\n", + SessionProvider, err) + os.Exit(2) + } + + log.Info("Session Service Enabled") +} + func newMailService() { // Check mailer setting. if Cfg.MustBool("mailer", "ENABLED") { @@ -214,6 +260,13 @@ func NewConfigContext() { SecretKey = Cfg.MustValue("security", "SECRET_KEY") RunUser = Cfg.MustValue("", "RUN_USER") + LogInRememberDays = Cfg.MustInt("security", "LOGIN_REMEMBER_DAYS") + CookieUserName = Cfg.MustValue("security", "COOKIE_USERNAME") + CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME") + + PictureService = Cfg.MustValue("picture", "SERVICE") + PictureRootPath = Cfg.MustValue("picture", "PATH") + // Determine and create root git reposiroty path. RepoRootPath = Cfg.MustValue("repository", "ROOT") if err = os.MkdirAll(RepoRootPath, os.ModePerm); err != nil { @@ -226,6 +279,7 @@ func NewServices() { newService() newLogService() newCacheService() + newSessionService() newMailService() newRegisterMailService() } diff --git a/modules/base/tool.go b/modules/base/tool.go index 8fabb8c531..c7ee2ee857 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -25,13 +25,17 @@ func EncodeMd5(str string) string { return hex.EncodeToString(m.Sum(nil)) } -// Random generate string -func GetRandomString(n int) string { +// GetRandomString generate random string by specify chars. +func GetRandomString(n int, alphabets ...byte) string { const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" var bytes = make([]byte, n) rand.Read(bytes) for i, b := range bytes { - bytes[i] = alphanum[b%byte(len(alphanum))] + if len(alphabets) == 0 { + bytes[i] = alphanum[b%byte(len(alphanum))] + } else { + bytes[i] = alphabets[b%byte(len(alphabets))] + } } return string(bytes) } @@ -111,6 +115,85 @@ const ( Year = 12 * Month ) +func computeTimeDiff(diff int64) (int64, string) { + diffStr := "" + switch { + case diff <= 0: + diff = 0 + diffStr = "now" + case diff < 2: + diff = 0 + diffStr = "1 second" + case diff < 1*Minute: + diffStr = fmt.Sprintf("%d seconds", diff) + diff = 0 + + case diff < 2*Minute: + diff -= 1 * Minute + diffStr = "1 minute" + case diff < 1*Hour: + diffStr = fmt.Sprintf("%d minutes", diff/Minute) + diff -= diff / Minute * Minute + + case diff < 2*Hour: + diff -= 1 * Hour + diffStr = "1 hour" + case diff < 1*Day: + diffStr = fmt.Sprintf("%d hours", diff/Hour) + diff -= diff / Hour * Hour + + case diff < 2*Day: + diff -= 1 * Day + diffStr = "1 day" + case diff < 1*Week: + diffStr = fmt.Sprintf("%d days", diff/Day) + diff -= diff / Day * Day + + case diff < 2*Week: + diff -= 1 * Week + diffStr = "1 week" + case diff < 1*Month: + diffStr = fmt.Sprintf("%d weeks", diff/Week) + diff -= diff / Week * Week + + case diff < 2*Month: + diff -= 1 * Month + diffStr = "1 month" + case diff < 1*Year: + diffStr = fmt.Sprintf("%d months", diff/Month) + diff -= diff / Month * Month + + case diff < 2*Year: + diff -= 1 * Year + diffStr = "1 year" + default: + diffStr = fmt.Sprintf("%d years", diff/Year) + diff = 0 + } + return diff, diffStr +} + +// TimeSincePro calculates the time interval and generate full user-friendly string. +func TimeSincePro(then time.Time) string { + now := time.Now() + diff := now.Unix() - then.Unix() + + if then.After(now) { + return "future" + } + + var timeStr, diffStr string + for { + if diff == 0 { + break + } + + diff, diffStr = computeTimeDiff(diff) + timeStr += ", " + diffStr + } + return strings.TrimPrefix(timeStr, ", ") +} + // TimeSince calculates the time interval and generate user-friendly string. func TimeSince(then time.Time) string { now := time.Now() @@ -123,7 +206,6 @@ func TimeSince(then time.Time) string { } switch { - case diff <= 0: return "now" case diff <= 2: @@ -156,8 +238,10 @@ func TimeSince(then time.Time) string { case diff < 1*Year: return fmt.Sprintf("%d months %s", diff/Month, lbl) - case diff < 18*Month: + case diff < 2*Year: return fmt.Sprintf("1 year %s", lbl) + default: + return fmt.Sprintf("%d years %s", diff/Year, lbl) } return then.String() } diff --git a/modules/middleware/auth.go b/modules/middleware/auth.go index f211de32b9..82c3367c48 100644 --- a/modules/middleware/auth.go +++ b/modules/middleware/auth.go @@ -5,44 +5,54 @@ package middleware import ( + "net/url" + "github.com/codegangsta/martini" "github.com/gogits/gogs/modules/base" ) -// SignInRequire requires user to sign in. -func SignInRequire(redirect bool) martini.Handler { - return func(ctx *Context) { - if !ctx.IsSigned { - if redirect { - ctx.Redirect("/user/login") - } - return - } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { - ctx.Data["Title"] = "Activate Your Account" - ctx.HTML(200, "user/active") - return - } - } +type ToggleOptions struct { + SignInRequire bool + SignOutRequire bool + AdminRequire bool + DisableCsrf bool } -// SignOutRequire requires user to sign out. -func SignOutRequire() martini.Handler { +func Toggle(options *ToggleOptions) martini.Handler { return func(ctx *Context) { - if ctx.IsSigned { + if options.SignOutRequire && ctx.IsSigned { ctx.Redirect("/") return } - } -} -// AdminRequire requires user signed in as administor. -func AdminRequire() martini.Handler { - return func(ctx *Context) { - if !ctx.User.IsAdmin { - ctx.Error(403) - return + if !options.DisableCsrf { + if ctx.Req.Method == "POST" { + if !ctx.CsrfTokenValid() { + ctx.Error(403, "CSRF token does not match") + return + } + } + } + + if options.SignInRequire { + if !ctx.IsSigned { + ctx.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI)) + ctx.Redirect("/user/login") + return + } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { + ctx.Data["Title"] = "Activate Your Account" + ctx.HTML(200, "user/active") + return + } + } + + if options.AdminRequire { + if !ctx.User.IsAdmin { + ctx.Error(403) + return + } + ctx.Data["PageIsAdmin"] = true } - ctx.Data["PageIsAdmin"] = true } } diff --git a/modules/middleware/context.go b/modules/middleware/context.go index a25a3dbbeb..d81ab999bf 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -5,14 +5,20 @@ package middleware import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" "fmt" + "html/template" "net/http" + "strconv" + "strings" "time" "github.com/codegangsta/martini" - "github.com/martini-contrib/sessions" "github.com/gogits/cache" + "github.com/gogits/session" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" @@ -27,11 +33,13 @@ type Context struct { p martini.Params Req *http.Request Res http.ResponseWriter - Session sessions.Session + Session session.SessionStore Cache cache.Cache User *models.User IsSigned bool + csrfToken string + Repo struct { IsValid bool IsOwner bool @@ -90,23 +98,157 @@ func (ctx *Context) Handle(status int, title string, err error) { ctx.HTML(status, fmt.Sprintf("status/%d", status)) } +func (ctx *Context) GetCookie(name string) string { + cookie, err := ctx.Req.Cookie(name) + if err != nil { + return "" + } + return cookie.Value +} + +func (ctx *Context) SetCookie(name string, value string, others ...interface{}) { + cookie := http.Cookie{} + cookie.Name = name + cookie.Value = value + + if len(others) > 0 { + switch v := others[0].(type) { + case int: + cookie.MaxAge = v + case int64: + cookie.MaxAge = int(v) + case int32: + cookie.MaxAge = int(v) + } + } + + // default "/" + if len(others) > 1 { + if v, ok := others[1].(string); ok && len(v) > 0 { + cookie.Path = v + } + } else { + cookie.Path = "/" + } + + // default empty + if len(others) > 2 { + if v, ok := others[2].(string); ok && len(v) > 0 { + cookie.Domain = v + } + } + + // default empty + if len(others) > 3 { + switch v := others[3].(type) { + case bool: + cookie.Secure = v + default: + if others[3] != nil { + cookie.Secure = true + } + } + } + + // default false. for session cookie default true + if len(others) > 4 { + if v, ok := others[4].(bool); ok && v { + cookie.HttpOnly = true + } + } + + ctx.Res.Header().Add("Set-Cookie", cookie.String()) +} + +// Get secure cookie from request by a given key. +func (ctx *Context) GetSecureCookie(Secret, key string) (string, bool) { + val := ctx.GetCookie(key) + if val == "" { + return "", false + } + + parts := strings.SplitN(val, "|", 3) + + if len(parts) != 3 { + return "", false + } + + vs := parts[0] + timestamp := parts[1] + sig := parts[2] + + h := hmac.New(sha1.New, []byte(Secret)) + fmt.Fprintf(h, "%s%s", vs, timestamp) + + if fmt.Sprintf("%02x", h.Sum(nil)) != sig { + return "", false + } + res, _ := base64.URLEncoding.DecodeString(vs) + return string(res), true +} + +// Set Secure cookie for response. +func (ctx *Context) SetSecureCookie(Secret, name, value string, others ...interface{}) { + vs := base64.URLEncoding.EncodeToString([]byte(value)) + timestamp := strconv.FormatInt(time.Now().UnixNano(), 10) + h := hmac.New(sha1.New, []byte(Secret)) + fmt.Fprintf(h, "%s%s", vs, timestamp) + sig := fmt.Sprintf("%02x", h.Sum(nil)) + cookie := strings.Join([]string{vs, timestamp, sig}, "|") + ctx.SetCookie(name, cookie, others...) +} + +func (ctx *Context) CsrfToken() string { + if len(ctx.csrfToken) > 0 { + return ctx.csrfToken + } + + token := ctx.GetCookie("_csrf") + if len(token) == 0 { + token = base.GetRandomString(30) + ctx.SetCookie("_csrf", token) + } + ctx.csrfToken = token + return token +} + +func (ctx *Context) CsrfTokenValid() bool { + token := ctx.Query("_csrf") + if token == "" { + token = ctx.Req.Header.Get("X-Csrf-Token") + } + if token == "" { + return false + } else if ctx.csrfToken != token { + return false + } + return true +} + // InitContext initializes a classic context for a request. func InitContext() martini.Handler { - return func(res http.ResponseWriter, r *http.Request, c martini.Context, - session sessions.Session, rd *Render) { + return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) { ctx := &Context{ c: c, // p: p, - Req: r, - Res: res, - Session: session, - Cache: base.Cache, - Render: rd, + Req: r, + Res: res, + Cache: base.Cache, + Render: rd, } + ctx.Data["PageStartTime"] = time.Now() + + // start session + ctx.Session = base.SessionManager.SessionStart(res, r) + rw := res.(martini.ResponseWriter) + rw.Before(func(martini.ResponseWriter) { + ctx.Session.SessionRelease(res) + }) + // Get user from session if logined. - user := auth.SignedInUser(session) + user := auth.SignedInUser(ctx.Session) ctx.User = user ctx.IsSigned = user != nil @@ -119,7 +261,9 @@ func InitContext() martini.Handler { ctx.Data["IsAdmin"] = ctx.User.IsAdmin } - ctx.Data["PageStartTime"] = time.Now() + // get or create csrf token + ctx.Data["CsrfToken"] = ctx.CsrfToken() + ctx.Data["CsrfTokenHtml"] = template.HTML(``) c.Map(ctx) diff --git a/modules/middleware/render.go b/modules/middleware/render.go index 8a54183135..869ef9abaa 100644 --- a/modules/middleware/render.go +++ b/modules/middleware/render.go @@ -242,8 +242,11 @@ func (r *Render) HTMLString(name string, binding interface{}, htmlOpt ...HTMLOpt } } -func (r *Render) Error(status int) { +func (r *Render) Error(status int, message ...string) { r.WriteHeader(status) + if len(message) > 0 { + r.Write([]byte(message[0])) + } } func (r *Render) Redirect(location string, status ...int) { diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index a9a90e3ff5..3864caaf80 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -54,7 +54,7 @@ func RepoAssignment(redirect bool) martini.Handler { ctx.Repo.Owner = user // get repository - repo, err := models.GetRepositoryByName(user, params["reponame"]) + repo, err := models.GetRepositoryByName(user.Id, params["reponame"]) if err != nil { if redirect { ctx.Redirect("/") diff --git a/public/css/gogs.css b/public/css/gogs.css index 78040bee51..65a6c03d71 100755 --- a/public/css/gogs.css +++ b/public/css/gogs.css @@ -346,6 +346,10 @@ html, body { border-left: 4px solid #DD4B39; } +#gogs-repo-setting-container .form-horizontal label { + line-height: 30px; +} + /* gogits user ssh keys */ #gogs-ssh-keys .list-group-item { @@ -575,12 +579,12 @@ html, body { min-width: 200px; } -#gogs-repo-clone .dropdown-menu{ +#gogs-repo-clone .dropdown-menu { width: 400px; padding: 20px; } -#gogs-repo-clone .input-group{ +#gogs-repo-clone .input-group { margin-bottom: 15px; } diff --git a/public/js/app.js b/public/js/app.js index f179342f4b..df755727b5 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -2,6 +2,39 @@ var Gogits = { "PageIsSignup": false }; +(function($){ + // extend jQuery ajax, set csrf token value + var ajax = $.ajax; + $.extend({ + ajax: function(url, options) { + if (typeof url === 'object') { + options = url; + url = undefined; + } + options = options || {}; + url = options.url; + var csrftoken = $('meta[name=_csrf]').attr('content'); + var headers = options.headers || {}; + var domain = document.domain.replace(/\./ig, '\\.'); + if (!/^(http:|https:).*/.test(url) || eval('/^(http:|https:)\\/\\/(.+\\.)*' + domain + '.*/').test(url)) { + headers = $.extend(headers, {'X-Csrf-Token':csrftoken}); + } + options.headers = headers; + var callback = options.success; + options.success = function(data){ + if(data.once){ + // change all _once value if ajax data.once exist + $('[name=_once]').val(data.once); + } + if(callback){ + callback.apply(this, arguments); + } + }; + return ajax(url, options); + } + }); +}(jQuery)); + (function ($) { Gogits.showTab = function (selector, index) { diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 2e19b99c10..c0f39f7159 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -5,7 +5,10 @@ package admin import ( + "fmt" + "runtime" "strings" + "time" "github.com/codegangsta/martini" @@ -14,10 +17,93 @@ import ( "github.com/gogits/gogs/modules/middleware" ) +var startTime = time.Now() + +var sysStatus struct { + Uptime string + NumGoroutine int + + // General statistics. + MemAllocated string // bytes allocated and still in use + MemTotal string // bytes allocated (even if freed) + MemSys string // bytes obtained from system (sum of XxxSys below) + Lookups uint64 // number of pointer lookups + MemMallocs uint64 // number of mallocs + MemFrees uint64 // number of frees + + // Main allocation heap statistics. + HeapAlloc string // bytes allocated and still in use + HeapSys string // bytes obtained from system + HeapIdle string // bytes in idle spans + HeapInuse string // bytes in non-idle span + HeapReleased string // bytes released to the OS + HeapObjects uint64 // total number of allocated objects + + // Low-level fixed-size structure allocator statistics. + // Inuse is bytes used now. + // Sys is bytes obtained from system. + StackInuse string // bootstrap stacks + StackSys string + MSpanInuse string // mspan structures + MSpanSys string + MCacheInuse string // mcache structures + MCacheSys string + BuckHashSys string // profiling bucket hash table + GCSys string // GC metadata + OtherSys string // other system allocations + + // Garbage collector statistics. + NextGC string // next run in HeapAlloc time (bytes) + LastGC string // last run in absolute time (ns) + PauseTotalNs string + PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256] + NumGC uint32 +} + +func updateSystemStatus() { + sysStatus.Uptime = base.TimeSincePro(startTime) + + m := new(runtime.MemStats) + runtime.ReadMemStats(m) + sysStatus.NumGoroutine = runtime.NumGoroutine() + + sysStatus.MemAllocated = base.FileSize(int64(m.Alloc)) + sysStatus.MemTotal = base.FileSize(int64(m.TotalAlloc)) + sysStatus.MemSys = base.FileSize(int64(m.Sys)) + sysStatus.Lookups = m.Lookups + sysStatus.MemMallocs = m.Mallocs + sysStatus.MemFrees = m.Frees + + sysStatus.HeapAlloc = base.FileSize(int64(m.HeapAlloc)) + sysStatus.HeapSys = base.FileSize(int64(m.HeapSys)) + sysStatus.HeapIdle = base.FileSize(int64(m.HeapIdle)) + sysStatus.HeapInuse = base.FileSize(int64(m.HeapInuse)) + sysStatus.HeapReleased = base.FileSize(int64(m.HeapReleased)) + sysStatus.HeapObjects = m.HeapObjects + + sysStatus.StackInuse = base.FileSize(int64(m.StackInuse)) + sysStatus.StackSys = base.FileSize(int64(m.StackSys)) + sysStatus.MSpanInuse = base.FileSize(int64(m.MSpanInuse)) + sysStatus.MSpanSys = base.FileSize(int64(m.MSpanSys)) + sysStatus.MCacheInuse = base.FileSize(int64(m.MCacheInuse)) + sysStatus.MCacheSys = base.FileSize(int64(m.MCacheSys)) + sysStatus.BuckHashSys = base.FileSize(int64(m.BuckHashSys)) + sysStatus.GCSys = base.FileSize(int64(m.GCSys)) + sysStatus.OtherSys = base.FileSize(int64(m.OtherSys)) + + sysStatus.NextGC = base.FileSize(int64(m.NextGC)) + sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000) + sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000) + sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000) + sysStatus.NumGC = m.NumGC +} + func Dashboard(ctx *middleware.Context) { ctx.Data["Title"] = "Admin Dashboard" ctx.Data["PageIsDashboard"] = true ctx.Data["Stats"] = models.GetStatistic() + updateSystemStatus() + ctx.Data["SysStatus"] = sysStatus ctx.HTML(200, "admin/dashboard") } @@ -70,6 +156,12 @@ func Config(ctx *middleware.Context) { ctx.Data["CacheAdapter"] = base.CacheAdapter ctx.Data["CacheConfig"] = base.CacheConfig + ctx.Data["SessionProvider"] = base.SessionProvider + ctx.Data["SessionConfig"] = base.SessionConfig + + ctx.Data["PictureService"] = base.PictureService + ctx.Data["PictureRootPath"] = base.PictureRootPath + ctx.Data["LogMode"] = base.LogMode ctx.Data["LogConfig"] = base.LogConfig diff --git a/routers/admin/user.go b/routers/admin/user.go index d6f8523218..7f66c5528c 100644 --- a/routers/admin/user.go +++ b/routers/admin/user.go @@ -107,3 +107,38 @@ func EditUser(ctx *middleware.Context, params martini.Params, form auth.AdminEdi log.Trace("%s User profile updated by admin(%s): %s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.User.LowerName) } + +func DeleteUser(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Edit Account" + ctx.Data["PageIsUsers"] = true + + uid, err := base.StrTo(params["userid"]).Int() + if err != nil { + ctx.Handle(200, "admin.user.EditUser", err) + return + } + + u, err := models.GetUserById(int64(uid)) + if err != nil { + ctx.Handle(200, "admin.user.EditUser", err) + return + } + + if err = models.DeleteUser(u); err != nil { + ctx.Data["HasError"] = true + switch err { + case models.ErrUserOwnRepos: + ctx.Data["ErrorMsg"] = "This account still has ownership of repository, owner has to delete or transfer them first." + ctx.Data["User"] = u + ctx.HTML(200, "admin/users/edit") + default: + ctx.Handle(200, "admin.user.DeleteUser", err) + } + return + } + + log.Trace("%s User deleted by admin(%s): %s", ctx.Req.RequestURI, + ctx.User.LowerName, ctx.User.LowerName) + + ctx.Redirect("/admin/users") +} diff --git a/routers/dashboard.go b/routers/dashboard.go index f61d67b7de..dafe9f31ec 100644 --- a/routers/dashboard.go +++ b/routers/dashboard.go @@ -20,5 +20,12 @@ func Home(ctx *middleware.Context) { func Help(ctx *middleware.Context) { ctx.Data["PageIsHelp"] = true + ctx.Data["Title"] = "Help" ctx.HTML(200, "help") } + +func NotFound(ctx *middleware.Context) { + ctx.Data["PageIsNotFound"] = true + ctx.Data["Title"] = 404 + ctx.Handle(404, "home.NotFound", nil) +} diff --git a/routers/repo/issue.go b/routers/repo/issue.go new file mode 100644 index 0000000000..78fe4b25d0 --- /dev/null +++ b/routers/repo/issue.go @@ -0,0 +1,85 @@ +// 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 repo + +import ( + "fmt" + + "github.com/codegangsta/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/middleware" +) + +func Issues(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Issues" + ctx.Data["IsRepoToolbarIssues"] = true + + milestoneId, _ := base.StrTo(params["milestone"]).Int() + page, _ := base.StrTo(params["page"]).Int() + + var err error + ctx.Data["Issues"], err = models.GetIssues(0, ctx.Repo.Repository.Id, 0, + int64(milestoneId), page, params["state"] == "closed", false, params["labels"], params["sortType"]) + if err != nil { + ctx.Handle(200, "issue.Issues: %v", err) + return + } + + ctx.HTML(200, "repo/issues") +} + +func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { + if !ctx.Repo.IsOwner { + ctx.Handle(404, "issue.CreateIssue", nil) + return + } + + ctx.Data["Title"] = "Create issue" + + if ctx.Req.Method == "GET" { + ctx.HTML(200, "issue/create") + return + } + + if ctx.HasError() { + ctx.HTML(200, "issue/create") + return + } + + issue, err := models.CreateIssue(ctx.User.Id, form.RepoId, form.MilestoneId, form.AssigneeId, + form.IssueName, form.Labels, form.Content, false) + if err == nil { + log.Trace("%s Issue created: %d", form.RepoId, issue.Id) + ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index)) + return + } + ctx.Handle(200, "issue.CreateIssue", err) +} + +func ViewIssue(ctx *middleware.Context, params martini.Params) { + issueid, err := base.StrTo(params["issueid"]).Int() + if err != nil { + ctx.Handle(404, "issue.ViewIssue", err) + return + } + + issue, err := models.GetIssueById(int64(issueid)) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Handle(404, "issue.ViewIssue", err) + } else { + ctx.Handle(200, "issue.ViewIssue", err) + } + return + } + + ctx.Data["Title"] = issue.Name + ctx.Data["Issue"] = issue + ctx.HTML(200, "issue/view") +} diff --git a/routers/repo/repo.go b/routers/repo/repo.go index c83a6df522..0f1ea31235 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -5,8 +5,17 @@ package repo import ( + "path" + "strings" + + "github.com/codegangsta/martini" + + "github.com/gogits/git" + "github.com/gogits/webdav" + "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/middleware" ) @@ -22,11 +31,16 @@ func Create(ctx *middleware.Context, form auth.CreateRepoForm) { return } + if ctx.HasError() { + ctx.HTML(200, "repo/create") + return + } + _, err := models.CreateRepository(ctx.User, form.RepoName, form.Description, form.Language, form.License, form.Visibility == "private", form.InitReadme == "on") if err == nil { log.Trace("%s Repository created: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, form.RepoName) - ctx.Redirect("/"+ctx.User.Name+"/"+form.RepoName, 302) + ctx.Redirect("/" + ctx.User.Name + "/" + form.RepoName) return } else if err == models.ErrRepoAlreadyExist { ctx.RenderWithErr("Repository name has already been used", "repo/create", &form) @@ -59,5 +73,290 @@ func SettingPost(ctx *middleware.Context) { } log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) - ctx.Redirect("/", 302) + ctx.Redirect("/") +} + +func Branches(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsValid { + return + } + + brs, err := models.GetBranches(params["username"], params["reponame"]) + if err != nil { + ctx.Handle(200, "repo.Branches", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "repo.Branches", nil) + return + } + + ctx.Data["Username"] = params["username"] + ctx.Data["Reponame"] = params["reponame"] + + ctx.Data["Branchname"] = brs[0] + ctx.Data["Branches"] = brs + ctx.Data["IsRepoToolbarBranches"] = true + + ctx.HTML(200, "repo/branches") +} + +func Single(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsValid { + return + } + + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + + // Get tree path + treename := params["_1"] + + if len(treename) > 0 && treename[len(treename)-1] == '/' { + ctx.Redirect("/" + ctx.Repo.Owner.LowerName + "/" + + ctx.Repo.Repository.Name + "/src/" + params["branchname"] + "/" + treename[:len(treename)-1]) + return + } + + ctx.Data["IsRepoToolbarSource"] = true + + // Branches. + brs, err := models.GetBranches(params["username"], params["reponame"]) + if err != nil { + //log.Error("repo.Single(GetBranches): %v", err) + ctx.Handle(404, "repo.Single(GetBranches)", err) + return + } else if ctx.Repo.Repository.IsBare { + ctx.Data["IsBareRepo"] = true + ctx.HTML(200, "repo/single") + return + } + + ctx.Data["Branches"] = brs + + repoFile, err := models.GetTargetFile(params["username"], params["reponame"], + params["branchname"], params["commitid"], treename) + + if err != nil && err != models.ErrRepoFileNotExist { + //log.Error("repo.Single(GetTargetFile): %v", err) + ctx.Handle(404, "repo.Single(GetTargetFile)", err) + return + } + + branchLink := "/" + ctx.Repo.Owner.LowerName + "/" + ctx.Repo.Repository.Name + "/src/" + params["branchname"] + + if len(treename) != 0 && repoFile == nil { + ctx.Handle(404, "repo.Single", nil) + return + } + + if repoFile != nil && repoFile.IsFile() { + if repoFile.Size > 1024*1024 || repoFile.Filemode != git.FileModeBlob { + ctx.Data["FileIsLarge"] = true + } else if blob, err := repoFile.LookupBlob(); err != nil { + //log.Error("repo.Single(repoFile.LookupBlob): %v", err) + ctx.Handle(404, "repo.Single(repoFile.LookupBlob)", err) + } else { + ctx.Data["IsFile"] = true + ctx.Data["FileName"] = repoFile.Name + ext := path.Ext(repoFile.Name) + if len(ext) > 0 { + ext = ext[1:] + } + ctx.Data["FileExt"] = ext + + readmeExist := base.IsMarkdownFile(repoFile.Name) || base.IsReadmeFile(repoFile.Name) + ctx.Data["ReadmeExist"] = readmeExist + if readmeExist { + ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), "")) + } else { + ctx.Data["FileContent"] = string(blob.Contents()) + } + } + + } else { + // Directory and file list. + files, err := models.GetReposFiles(params["username"], params["reponame"], + params["branchname"], params["commitid"], treename) + if err != nil { + //log.Error("repo.Single(GetReposFiles): %v", err) + ctx.Handle(404, "repo.Single(GetReposFiles)", err) + return + } + + ctx.Data["Files"] = files + + var readmeFile *models.RepoFile + + for _, f := range files { + if !f.IsFile() || !base.IsReadmeFile(f.Name) { + continue + } else { + readmeFile = f + break + } + } + + if readmeFile != nil { + ctx.Data["ReadmeExist"] = true + // if file large than 1M not show it + if readmeFile.Size > 1024*1024 || readmeFile.Filemode != git.FileModeBlob { + ctx.Data["FileIsLarge"] = true + } else if blob, err := readmeFile.LookupBlob(); err != nil { + //log.Error("repo.Single(readmeFile.LookupBlob): %v", err) + ctx.Handle(404, "repo.Single(readmeFile.LookupBlob)", err) + return + } else { + // current repo branch link + + ctx.Data["FileName"] = readmeFile.Name + ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), branchLink)) + } + } + } + + ctx.Data["Username"] = params["username"] + ctx.Data["Reponame"] = params["reponame"] + ctx.Data["Branchname"] = params["branchname"] + + var treenames []string + Paths := make([]string, 0) + + if len(treename) > 0 { + treenames = strings.Split(treename, "/") + for i, _ := range treenames { + Paths = append(Paths, strings.Join(treenames[0:i+1], "/")) + } + + ctx.Data["HasParentPath"] = true + if len(Paths)-2 >= 0 { + ctx.Data["ParentPath"] = "/" + Paths[len(Paths)-2] + } + } + + // Get latest commit according username and repo name + commit, err := models.GetCommit(params["username"], params["reponame"], + params["branchname"], params["commitid"]) + if err != nil { + log.Error("repo.Single(GetCommit): %v", err) + ctx.Handle(404, "repo.Single(GetCommit)", err) + return + } + ctx.Data["LastCommit"] = commit + + ctx.Data["Paths"] = Paths + ctx.Data["Treenames"] = treenames + ctx.Data["BranchLink"] = branchLink + ctx.HTML(200, "repo/single") +} + +func Http(ctx *middleware.Context, params martini.Params) { + /*if !ctx.Repo.IsValid { + return + }*/ + + // TODO: access check + + username := params["username"] + reponame := params["reponame"] + if strings.HasSuffix(reponame, ".git") { + reponame = reponame[:len(reponame)-4] + } + + prefix := path.Join("/", username, params["reponame"]) + server := &webdav.Server{ + Fs: webdav.Dir(models.RepoPath(username, reponame)), + TrimPrefix: prefix, + Listings: true, + } + + server.ServeHTTP(ctx.ResponseWriter, ctx.Req) +} + +func Setting(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsOwner { + ctx.Handle(404, "repo.Setting", nil) + return + } + + ctx.Data["IsRepoToolbarSetting"] = true + + if ctx.Repo.Repository.IsBare { + ctx.Data["IsBareRepo"] = true + ctx.HTML(200, "repo/setting") + return + } + + var title string + if t, ok := ctx.Data["Title"].(string); ok { + title = t + } + + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + + ctx.Data["Branchname"] = params["branchname"] + ctx.Data["Title"] = title + " - settings" + ctx.HTML(200, "repo/setting") +} + +func Commits(ctx *middleware.Context, params martini.Params) { + brs, err := models.GetBranches(params["username"], params["reponame"]) + if err != nil { + ctx.Handle(200, "repo.Commits", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "repo.Commits", nil) + return + } + + ctx.Data["IsRepoToolbarCommits"] = true + commits, err := models.GetCommits(params["username"], + params["reponame"], params["branchname"]) + if err != nil { + ctx.Handle(404, "repo.Commits", nil) + return + } + ctx.Data["Username"] = params["username"] + ctx.Data["Reponame"] = params["reponame"] + ctx.Data["CommitCount"] = commits.Len() + ctx.Data["Commits"] = commits + ctx.HTML(200, "repo/commits") +} + +func Pulls(ctx *middleware.Context) { + ctx.Data["IsRepoToolbarPulls"] = true + ctx.HTML(200, "repo/pulls") +} + +func Action(ctx *middleware.Context, params martini.Params) { + var err error + switch params["action"] { + case "watch": + err = models.WatchRepo(ctx.User.Id, ctx.Repo.Repository.Id, true) + case "unwatch": + err = models.WatchRepo(ctx.User.Id, ctx.Repo.Repository.Id, false) + case "desc": + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + + ctx.Repo.Repository.Description = ctx.Query("desc") + ctx.Repo.Repository.Website = ctx.Query("site") + err = models.UpdateRepository(ctx.Repo.Repository) + } + + if err != nil { + log.Error("repo.Action(%s): %v", params["action"], err) + ctx.JSON(200, map[string]interface{}{ + "ok": false, + "err": err.Error(), + }) + return + } + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) } diff --git a/routers/repo/single.go b/routers/repo/single.go deleted file mode 100644 index 37c0fabd79..0000000000 --- a/routers/repo/single.go +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package repo - -import ( - "path" - "strings" - - "github.com/codegangsta/martini" - - "github.com/gogits/git" - "github.com/gogits/webdav" - - "github.com/gogits/gogs/models" - "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/log" - "github.com/gogits/gogs/modules/middleware" -) - -func Branches(ctx *middleware.Context, params martini.Params) { - if !ctx.Repo.IsValid { - return - } - - brs, err := models.GetBranches(params["username"], params["reponame"]) - if err != nil { - ctx.Handle(200, "repo.Branches", err) - return - } else if len(brs) == 0 { - ctx.Error(404) - return - } - - ctx.Data["Username"] = params["username"] - ctx.Data["Reponame"] = params["reponame"] - - ctx.Data["Branchname"] = brs[0] - ctx.Data["Branches"] = brs - ctx.Data["IsRepoToolbarBranches"] = true - - ctx.HTML(200, "repo/branches") -} - -func Single(ctx *middleware.Context, params martini.Params) { - if !ctx.Repo.IsValid { - return - } - - if len(params["branchname"]) == 0 { - params["branchname"] = "master" - } - - // Get tree path - treename := params["_1"] - - if len(treename) > 0 && treename[len(treename)-1] == '/' { - ctx.Redirect("/"+ctx.Repo.Owner.LowerName+"/"+ - ctx.Repo.Repository.Name+"/src/"+params["branchname"]+"/"+treename[:len(treename)-1], 302) - return - } - - ctx.Data["IsRepoToolbarSource"] = true - - // Branches. - brs, err := models.GetBranches(params["username"], params["reponame"]) - if err != nil { - log.Error("repo.Single(GetBranches): %v", err) - ctx.Error(404) - return - } else if len(brs) == 0 { - ctx.Data["IsBareRepo"] = true - ctx.HTML(200, "repo/single") - return - } - - ctx.Data["Branches"] = brs - - repoFile, err := models.GetTargetFile(params["username"], params["reponame"], - params["branchname"], params["commitid"], treename) - - if err != nil && err != models.ErrRepoFileNotExist { - log.Error("repo.Single(GetTargetFile): %v", err) - ctx.Error(404) - return - } - - branchLink := "/" + ctx.Repo.Owner.LowerName + "/" + ctx.Repo.Repository.Name + "/src/" + params["branchname"] - - if len(treename) != 0 && repoFile == nil { - ctx.Error(404) - return - } - - if repoFile != nil && repoFile.IsFile() { - if repoFile.Size > 1024*1024 || repoFile.Filemode != git.FileModeBlob { - ctx.Data["FileIsLarge"] = true - } else if blob, err := repoFile.LookupBlob(); err != nil { - log.Error("repo.Single(repoFile.LookupBlob): %v", err) - ctx.Error(404) - } else { - ctx.Data["IsFile"] = true - ctx.Data["FileName"] = repoFile.Name - ext := path.Ext(repoFile.Name) - if len(ext) > 0 { - ext = ext[1:] - } - ctx.Data["FileExt"] = ext - - readmeExist := base.IsMarkdownFile(repoFile.Name) || base.IsReadmeFile(repoFile.Name) - ctx.Data["ReadmeExist"] = readmeExist - if readmeExist { - ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), "")) - } else { - ctx.Data["FileContent"] = string(blob.Contents()) - } - } - - } else { - // Directory and file list. - files, err := models.GetReposFiles(params["username"], params["reponame"], - params["branchname"], params["commitid"], treename) - if err != nil { - log.Error("repo.Single(GetReposFiles): %v", err) - ctx.Error(404) - return - } - - ctx.Data["Files"] = files - - var readmeFile *models.RepoFile - - for _, f := range files { - if !f.IsFile() || !base.IsReadmeFile(f.Name) { - continue - } else { - readmeFile = f - break - } - } - - if readmeFile != nil { - ctx.Data["ReadmeExist"] = true - // if file large than 1M not show it - if readmeFile.Size > 1024*1024 || readmeFile.Filemode != git.FileModeBlob { - ctx.Data["FileIsLarge"] = true - } else if blob, err := readmeFile.LookupBlob(); err != nil { - log.Error("repo.Single(readmeFile.LookupBlob): %v", err) - ctx.Error(404) - return - } else { - // current repo branch link - - ctx.Data["FileName"] = readmeFile.Name - ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), branchLink)) - } - } - } - - ctx.Data["Username"] = params["username"] - ctx.Data["Reponame"] = params["reponame"] - ctx.Data["Branchname"] = params["branchname"] - - var treenames []string - Paths := make([]string, 0) - - if len(treename) > 0 { - treenames = strings.Split(treename, "/") - for i, _ := range treenames { - Paths = append(Paths, strings.Join(treenames[0:i+1], "/")) - } - - ctx.Data["HasParentPath"] = true - if len(Paths)-2 >= 0 { - ctx.Data["ParentPath"] = "/" + Paths[len(Paths)-2] - } - } - - // Get latest commit according username and repo name - commit, err := models.GetCommit(params["username"], params["reponame"], - params["branchname"], params["commitid"]) - if err != nil { - log.Error("repo.Single(GetCommit): %v", err) - ctx.Error(404) - return - } - ctx.Data["LastCommit"] = commit - - ctx.Data["Paths"] = Paths - ctx.Data["Treenames"] = treenames - ctx.Data["BranchLink"] = branchLink - ctx.HTML(200, "repo/single") -} - -func Http(ctx *middleware.Context, params martini.Params) { - /*if !ctx.Repo.IsValid { - return - }*/ - - // TODO: access check - - username := params["username"] - reponame := params["reponame"] - if strings.HasSuffix(reponame, ".git") { - reponame = reponame[:len(reponame)-4] - } - - prefix := path.Join("/", username, params["reponame"]) - server := &webdav.Server{ - Fs: webdav.Dir(models.RepoPath(username, reponame)), - TrimPrefix: prefix, - Listings: true, - } - - server.ServeHTTP(ctx.ResponseWriter, ctx.Req) -} - -func Setting(ctx *middleware.Context, params martini.Params) { - if !ctx.Repo.IsOwner { - ctx.Error(404) - return - } - - ctx.Data["IsRepoToolbarSetting"] = true - - // Branches. - brs, err := models.GetBranches(params["username"], params["reponame"]) - if err != nil { - log.Error("repo.Setting(GetBranches): %v", err) - ctx.Error(404) - return - } else if len(brs) == 0 { - ctx.Data["IsBareRepo"] = true - ctx.HTML(200, "repo/setting") - return - } - - var title string - if t, ok := ctx.Data["Title"].(string); ok { - title = t - } - - if len(params["branchname"]) == 0 { - params["branchname"] = "master" - } - - ctx.Data["Branchname"] = params["branchname"] - ctx.Data["Title"] = title + " - settings" - ctx.HTML(200, "repo/setting") -} - -func Commits(ctx *middleware.Context, params martini.Params) { - brs, err := models.GetBranches(params["username"], params["reponame"]) - if err != nil { - ctx.Handle(200, "repo.Commits", err) - return - } else if len(brs) == 0 { - ctx.Error(404) - return - } - - ctx.Data["IsRepoToolbarCommits"] = true - commits, err := models.GetCommits(params["username"], - params["reponame"], params["branchname"]) - if err != nil { - ctx.Error(404) - return - } - ctx.Data["Username"] = params["username"] - ctx.Data["Reponame"] = params["reponame"] - ctx.Data["CommitCount"] = commits.Len() - ctx.Data["Commits"] = commits - ctx.HTML(200, "repo/commits") -} - -func Issues(ctx *middleware.Context) { - ctx.Data["IsRepoToolbarIssues"] = true - ctx.HTML(200, "repo/issues") -} - -func Pulls(ctx *middleware.Context) { - ctx.Data["IsRepoToolbarPulls"] = true - ctx.HTML(200, "repo/pulls") -} - -func Action(ctx *middleware.Context, params martini.Params) { - var err error - switch params["action"] { - case "watch": - err = models.WatchRepo(ctx.User.Id, ctx.Repo.Repository.Id, true) - case "unwatch": - err = models.WatchRepo(ctx.User.Id, ctx.Repo.Repository.Id, false) - } - - if err != nil { - log.Error("repo.Action(%s): %v", params["action"], err) - ctx.JSON(200, map[string]interface{}{ - "ok": false, - "err": err.Error(), - }) - return - } - ctx.JSON(200, map[string]interface{}{ - "ok": true, - }) -} diff --git a/routers/user/user.go b/routers/user/user.go index d38eb1ceb3..a0321f187b 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -6,6 +6,7 @@ package user import ( "fmt" + "net/url" "strings" "github.com/codegangsta/martini" @@ -77,7 +78,45 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { ctx.Data["Title"] = "Log In" if ctx.Req.Method == "GET" { - ctx.HTML(200, "user/signin") + // Check auto-login. + userName := ctx.GetCookie(base.CookieUserName) + if len(userName) == 0 { + ctx.HTML(200, "user/signin") + return + } + + isSucceed := false + defer func() { + if !isSucceed { + log.Trace("%s auto-login cookie cleared: %s", ctx.Req.RequestURI, userName) + ctx.SetCookie(base.CookieUserName, "", -1) + ctx.SetCookie(base.CookieRememberName, "", -1) + } + }() + + user, err := models.GetUserByName(userName) + if err != nil { + ctx.HTML(200, "user/signin") + return + } + + secret := base.EncodeMd5(user.Rands + user.Passwd) + value, _ := ctx.GetSecureCookie(secret, base.CookieRememberName) + if value != user.Name { + ctx.HTML(200, "user/signin") + return + } + + isSucceed = true + ctx.Session.Set("userId", user.Id) + ctx.Session.Set("userName", user.Name) + redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")) + if len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1) + ctx.Redirect(redirectTo) + } else { + ctx.Redirect("/") + } return } @@ -88,7 +127,8 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { user, err := models.LoginUserPlain(form.UserName, form.Password) if err != nil { - if err.Error() == models.ErrUserNotExist.Error() { + if err == models.ErrUserNotExist { + log.Trace("%s Log in failed: %s/%s", ctx.Req.RequestURI, form.UserName, form.Password) ctx.RenderWithErr("Username or password is not correct", "user/signin", &form) return } @@ -97,14 +137,29 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { return } + if form.Remember == "on" { + secret := base.EncodeMd5(user.Rands + user.Passwd) + days := 86400 * base.LogInRememberDays + ctx.SetCookie(base.CookieUserName, user.Name, days) + ctx.SetSecureCookie(secret, base.CookieRememberName, user.Name, days) + } + ctx.Session.Set("userId", user.Id) ctx.Session.Set("userName", user.Name) - ctx.Redirect("/") + redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")) + if len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1) + ctx.Redirect(redirectTo) + } else { + ctx.Redirect("/") + } } func SignOut(ctx *middleware.Context) { ctx.Session.Delete("userId") ctx.Session.Delete("userName") + ctx.SetCookie(base.CookieUserName, "", -1) + ctx.SetCookie(base.CookieRememberName, "", -1) ctx.Redirect("/") } @@ -246,7 +301,7 @@ func Activate(ctx *middleware.Context) { if len(code) == 0 { ctx.Data["IsActivatePage"] = true if ctx.User.IsActive { - ctx.Error(404) + ctx.Handle(404, "user.Activate", nil) return } // Resend confirmation e-mail. @@ -274,7 +329,7 @@ func Activate(ctx *middleware.Context) { ctx.Session.Set("userId", user.Id) ctx.Session.Set("userName", user.Name) - ctx.Redirect("/", 302) + ctx.Redirect("/") return } diff --git a/serve.go b/serve.go index b786c54356..54296049d5 100644 --- a/serve.go +++ b/serve.go @@ -106,7 +106,7 @@ func runServ(k *cli.Context) { //os.Setenv("userName", user.Name) //os.Setenv("userId", strconv.Itoa(int(user.Id))) - repo, err := models.GetRepositoryByName(user, repoName) + repo, err := models.GetRepositoryByName(user.Id, repoName) var isExist bool = true if err != nil { if err == models.ErrRepoNotExist { diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 6906f2409d..048740e617 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -45,6 +45,7 @@