You've already forked woodpecker
							
							
				mirror of
				https://github.com/woodpecker-ci/woodpecker.git
				synced 2025-10-30 23:27:39 +02:00 
			
		
		
		
	added handlers, rest, angular skeleton
This commit is contained in:
		| @@ -83,3 +83,26 @@ func (db *DB) DeleteRepo(repo *common.Repo) error { | ||||
| 	// TODO(bradrydzewski) delete all tasks | ||||
| 	return t.Commit() | ||||
| } | ||||
|  | ||||
| // GetSubscriber gets the subscriber by login for the | ||||
| // named repository. | ||||
| func (db *DB) GetSubscriber(repo string, login string) (*common.Subscriber, error) { | ||||
| 	sub := &common.Subscriber{} | ||||
| 	key := []byte(login + "/" + repo) | ||||
| 	err := get(db, bucketUserRepos, key, sub) | ||||
| 	return sub, err | ||||
| } | ||||
|  | ||||
| // InsertSubscriber inserts a subscriber for the named | ||||
| // repository. | ||||
| func (db *DB) InsertSubscriber(repo string, sub *common.Subscriber) error { | ||||
| 	key := []byte(sub.Login + "/" + repo) | ||||
| 	return insert(db, bucketUserRepos, key, sub) | ||||
| } | ||||
|  | ||||
| // DeleteSubscriber removes the subscriber by login for the | ||||
| // named repository. | ||||
| func (db *DB) DeleteSubscriber(repo string, sub *common.Subscriber) error { | ||||
| 	key := []byte(sub.Login + "/" + repo) | ||||
| 	return delete(db, bucketUserRepos, key) | ||||
| } | ||||
|   | ||||
| @@ -42,6 +42,18 @@ type Datastore interface { | ||||
| 	// DeleteUser deletes the token. | ||||
| 	DeleteToken(*common.Token) error | ||||
|  | ||||
| 	// GetSubscriber gets the subscriber by login for the | ||||
| 	// named repository. | ||||
| 	GetSubscriber(string, string) (*common.Subscriber, error) | ||||
|  | ||||
| 	// InsertSubscriber inserts a subscriber for the named | ||||
| 	// repository. | ||||
| 	InsertSubscriber(string, *common.Subscriber) error | ||||
|  | ||||
| 	// DeleteSubscriber removes the subscriber by login for the | ||||
| 	// named repository. | ||||
| 	DeleteSubscriber(string, *common.Subscriber) error | ||||
|  | ||||
| 	// GetRepo gets the repository by name. | ||||
| 	GetRepo(string) (*common.Repo, error) | ||||
|  | ||||
| @@ -125,7 +137,3 @@ type Datastore interface { | ||||
| 	// named repository and build number. | ||||
| 	UpsertTaskLogs(string, int, int, []byte) error | ||||
| } | ||||
|  | ||||
| // GetSubscriber(string, string) (*common.Subscriber, error) | ||||
| // InsertSubscriber(string, *common.Subscriber) error | ||||
| // DeleteSubscriber(string, string) error | ||||
|   | ||||
							
								
								
									
										75
									
								
								server/badge.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								server/badge.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
|  | ||||
| 	"github.com/drone/drone/common" | ||||
| 	"github.com/drone/drone/common/ccmenu" | ||||
| 	"github.com/drone/drone/common/httputil" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	badgeSuccess = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="91" height="20" fill="#555"/><rect rx="3" x="37" width="54" height="20" fill="#4c1"/><path fill="#4c1" d="M37 0h4v20h-4z"/><rect rx="3" width="91" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg>`) | ||||
| 	badgeFailure = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="83" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="83" height="20" fill="#555"/><rect rx="3" x="37" width="46" height="20" fill="#e05d44"/><path fill="#e05d44" d="M37 0h4v20h-4z"/><rect rx="3" width="83" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="59" y="15" fill="#010101" fill-opacity=".3">failure</text><text x="59" y="14">failure</text></g></svg>`) | ||||
| 	badgeStarted = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="87" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="87" height="20" fill="#555"/><rect rx="3" x="37" width="50" height="20" fill="#dfb317"/><path fill="#dfb317" d="M37 0h4v20h-4z"/><rect rx="3" width="87" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="61" y="15" fill="#010101" fill-opacity=".3">started</text><text x="61" y="14">started</text></g></svg>`) | ||||
| 	badgeError   = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="76" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="76" height="20" fill="#555"/><rect rx="3" x="37" width="39" height="20" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v20h-4z"/><rect rx="3" width="76" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="55.5" y="15" fill="#010101" fill-opacity=".3">error</text><text x="55.5" y="14">error</text></g></svg>`) | ||||
| 	badgeNone    = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="75" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="75" height="20" fill="#555"/><rect rx="3" x="37" width="38" height="20" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v20h-4z"/><rect rx="3" width="75" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="55" y="15" fill="#010101" fill-opacity=".3">none</text><text x="55" y="14">none</text></g></svg>`) | ||||
| ) | ||||
|  | ||||
| // GetBadge accepts a request to retrieve the named | ||||
| // repo and branhes latest build details from the datastore | ||||
| // and return an SVG badges representing the build results. | ||||
| // | ||||
| //     GET /api/badge/:owner/:name/status.svg | ||||
| // | ||||
| func GetBadge(c *gin.Context) { | ||||
| 	var repo = ToRepo(c) | ||||
|  | ||||
| 	// an SVG response is always served, even when error, so | ||||
| 	// we can go ahead and set the content type appropriately. | ||||
| 	c.Writer.Header().Set("Content-Type", "image/svg+xml") | ||||
|  | ||||
| 	// if no commit was found then display | ||||
| 	// the 'none' badge, instead of throwing | ||||
| 	// an error response | ||||
| 	if repo.Last == nil { | ||||
| 		c.Writer.Write(badgeNone) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	switch repo.Last.State { | ||||
| 	case common.StateSuccess: | ||||
| 		c.Writer.Write(badgeSuccess) | ||||
| 	case common.StateFailure: | ||||
| 		c.Writer.Write(badgeFailure) | ||||
| 	case common.StateError, common.StateKilled: | ||||
| 		c.Writer.Write(badgeError) | ||||
| 	case common.StatePending, common.StateRunning: | ||||
| 		c.Writer.Write(badgeStarted) | ||||
| 	default: | ||||
| 		c.Writer.Write(badgeNone) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetCC accepts a request to retrieve the latest build | ||||
| // status for the given repository from the datastore and | ||||
| // in CCTray XML format. | ||||
| // | ||||
| //     GET /api/badge/:host/:owner/:name/cc.xml | ||||
| // | ||||
| // TODO(bradrydzewski) this will not return in-progress builds, which it should | ||||
| func GetCC(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	last, err := ds.GetBuildLast(repo.FullName) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	link := httputil.GetURL(c.Request) + "/" + repo.FullName | ||||
| 	cc := ccmenu.NewCC(repo, last, link) | ||||
|  | ||||
| 	c.Writer.Header().Set("Content-Type", "application/xml") | ||||
| 	c.XML(200, cc) | ||||
| } | ||||
							
								
								
									
										45
									
								
								server/builds.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								server/builds.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"strconv" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // GetBuild accepts a request to retrieve a build | ||||
| // from the datastore for the given repository and | ||||
| // build number. | ||||
| // | ||||
| //     GET /api/builds/:owner/:name/:number | ||||
| // | ||||
| func GetBuild(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	num, err := strconv.Atoi(c.Params.ByName("number")) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
| 	build, err := ds.GetBuild(repo.FullName, num) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, build) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetBuild accepts a request to retrieve a list | ||||
| // of builds from the datastore for the given repository. | ||||
| // | ||||
| //     GET /api/builds/:owner/:name | ||||
| // | ||||
| func GetBuilds(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	builds, err := ds.GetBuildList(repo.FullName) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, builds) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										90
									
								
								server/hooks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								server/hooks.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/drone/drone/common" | ||||
| 	// "github.com/bradrydzewski/drone/worker" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // PostHook accepts a post-commit hook and parses the payload | ||||
| // in order to trigger a build. | ||||
| // | ||||
| //     GET /api/hook | ||||
| // | ||||
| func PostHook(c *gin.Context) { | ||||
| 	remote := ToRemote(c) | ||||
| 	store := ToDatastore(c) | ||||
|  | ||||
| 	hook, err := remote.Hook(c.Request) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
| 	if hook == nil { | ||||
| 		c.Writer.WriteHeader(200) | ||||
| 		return | ||||
| 	} | ||||
| 	if hook.Repo == nil { | ||||
| 		c.Writer.WriteHeader(400) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// a build may be skipped if the text [CI SKIP] | ||||
| 	// is found inside the commit message | ||||
| 	if hook.Commit != nil && strings.Contains(hook.Commit.Message, "[CI SKIP]") { | ||||
| 		c.Writer.WriteHeader(204) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	repo, err := store.GetRepo(hook.Repo.FullName) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if repo.Disabled || repo.User == nil || (repo.DisablePR && hook.PullRequest != nil) { | ||||
| 		c.Writer.WriteHeader(204) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	user, err := store.GetUser(repo.User.Login) | ||||
| 	if err != nil { | ||||
| 		c.Fail(500, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	build := &common.Build{} | ||||
| 	build.State = common.StatePending | ||||
| 	build.Commit = hook.Commit | ||||
| 	build.PullRequest = hook.PullRequest | ||||
|  | ||||
| 	// featch the .drone.yml file from the database | ||||
| 	_, err = remote.Script(user, repo, build) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = store.InsertBuild(repo.FullName, build) | ||||
| 	if err != nil { | ||||
| 		c.Fail(500, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// w := worker.Work{ | ||||
| 	// 	User: user, | ||||
| 	// 	Repo: repo, | ||||
| 	// 	Build: build, | ||||
| 	// } | ||||
|  | ||||
| 	// verify the branches can be built vs skipped | ||||
| 	// s, _ := script.ParseBuild(string(yml)) | ||||
| 	// if len(hook.PullRequest) == 0 && !s.MatchBranch(hook.Branch) { | ||||
| 	// 	w.WriteHeader(http.StatusOK) | ||||
| 	// 	return | ||||
| 	// } | ||||
|  | ||||
| 	c.JSON(200, build) | ||||
| } | ||||
							
								
								
									
										181
									
								
								server/login.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								server/login.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
|  | ||||
| 	"github.com/drone/drone/common" | ||||
| 	"github.com/drone/drone/common/gravatar" | ||||
| 	"github.com/drone/drone/common/httputil" | ||||
| 	"github.com/drone/drone/common/oauth2" | ||||
| ) | ||||
|  | ||||
| // GetLogin accepts a request to authorize the user and to | ||||
| // return a valid OAuth2 access token. The access token is | ||||
| // returned as url segment #access_token | ||||
| // | ||||
| //     GET /authorize | ||||
| // | ||||
| func GetLogin(c *gin.Context) { | ||||
| 	settings := ToSettings(c) | ||||
| 	session := ToSession(c) | ||||
| 	store := ToDatastore(c) | ||||
|  | ||||
| 	// when dealing with redirects we may need | ||||
| 	// to adjust the content type. I cannot, however, | ||||
| 	// rememver why, so need to revisit this line. | ||||
| 	c.Writer.Header().Del("Content-Type") | ||||
|  | ||||
| 	// depending on the configuration a user may | ||||
| 	// authenticate with OAuth1, OAuth2 or Basic | ||||
| 	// Auth (username and password). This will delegate | ||||
| 	// authorization accordingly. | ||||
| 	switch { | ||||
| 	case settings.Service.OAuth == nil: | ||||
| 		getLoginBasic(c) | ||||
| 	case settings.Service.OAuth.RequestToken != "": | ||||
| 		getLoginOauth1(c) | ||||
| 	default: | ||||
| 		getLoginOauth2(c) | ||||
| 	} | ||||
|  | ||||
| 	// exit if authorization fails | ||||
| 	// TODO(bradrydzewski) return an error message instead | ||||
| 	if c.Writer.Status() != 200 { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// get the user from the database | ||||
| 	login := ToUser(c) | ||||
| 	u, err := store.GetUser(login.Login) | ||||
| 	if err != nil { | ||||
| 		// if self-registration is disabled we should | ||||
| 		// return a notAuthorized error. the only exception | ||||
| 		// is if no users exist yet in the system we'll proceed. | ||||
| 		if !settings.Service.Open { | ||||
| 			count, err := store.GetUserCount() | ||||
| 			if err != nil || count != 0 { | ||||
| 				c.String(400, "Unable to create account. Registration is closed") | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// create the user account | ||||
| 		u = &common.User{} | ||||
| 		u.Login = login.Login | ||||
| 		u.Token = login.Token | ||||
| 		u.Secret = login.Secret | ||||
| 		u.Name = login.Name | ||||
| 		u.Email = login.Email | ||||
| 		u.Gravatar = gravatar.Generate(u.Email) | ||||
|  | ||||
| 		// insert the user into the database | ||||
| 		if err := store.InsertUser(u); err != nil { | ||||
| 			log.Println(err) | ||||
| 			c.Fail(400, err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// // if this is the first user, they | ||||
| 		// // should be an admin. | ||||
| 		//if u.ID == 1 { | ||||
| 		if u.Login == "bradrydzewski" { | ||||
| 			u.Admin = true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// update the user meta data and authorization | ||||
| 	// data and cache in the datastore. | ||||
| 	u.Token = login.Token | ||||
| 	u.Secret = login.Secret | ||||
| 	u.Name = login.Name | ||||
| 	u.Email = login.Email | ||||
| 	u.Gravatar = gravatar.Generate(u.Email) | ||||
|  | ||||
| 	if err := store.UpdateUser(u); err != nil { | ||||
| 		log.Println(err) | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	token, err := session.GenerateToken(c.Request, u) | ||||
| 	if err != nil { | ||||
| 		log.Println(err) | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
| 	c.Redirect(303, "/#access_token="+token) | ||||
| } | ||||
|  | ||||
| // getLoginOauth2 is the default authorization implementation | ||||
| // using the oauth2 protocol. | ||||
| func getLoginOauth2(c *gin.Context) { | ||||
| 	var settings = ToSettings(c) | ||||
| 	var remote = ToRemote(c) | ||||
|  | ||||
| 	var config = &oauth2.Config{ | ||||
| 		ClientId:     settings.Service.OAuth.Client, | ||||
| 		ClientSecret: settings.Service.OAuth.Secret, | ||||
| 		Scope:        strings.Join(settings.Service.OAuth.Scope, ","), | ||||
| 		AuthURL:      settings.Service.OAuth.Authorize, | ||||
| 		TokenURL:     settings.Service.OAuth.AccessToken, | ||||
| 		RedirectURL:  fmt.Sprintf("%s/authorize", httputil.GetURL(c.Request)), | ||||
| 		//settings.Server.Scheme, settings.Server.Hostname), | ||||
| 	} | ||||
|  | ||||
| 	// get the OAuth code | ||||
| 	var code = c.Request.FormValue("code") | ||||
| 	//var state = c.Request.FormValue("state") | ||||
| 	if len(code) == 0 { | ||||
| 		c.Redirect(303, config.AuthCodeURL("random")) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// exhange for a token | ||||
| 	var trans = &oauth2.Transport{Config: config} | ||||
| 	var token, err = trans.Exchange(code) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// get user account | ||||
| 	user, err := remote.Login(token.AccessToken, token.RefreshToken) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// add the user to the request | ||||
| 	c.Set("user", user) | ||||
| } | ||||
|  | ||||
| // getLoginOauth1 is able to authorize a user with the oauth1 | ||||
| // authentication protocol. This is used primarily with Bitbucket | ||||
| // and Stash only, and one day I hope can be removed. | ||||
| func getLoginOauth1(c *gin.Context) { | ||||
|  | ||||
| } | ||||
|  | ||||
| // getLoginBasic is able to authorize a user with a username and | ||||
| // password. This can be used for systems that do not support oauth. | ||||
| func getLoginBasic(c *gin.Context) { | ||||
| 	var ( | ||||
| 		remote   = ToRemote(c) | ||||
| 		username = c.Request.FormValue("username") | ||||
| 		password = c.Request.FormValue("username") | ||||
| 	) | ||||
|  | ||||
| 	// get user account | ||||
| 	user, err := remote.Login(username, password) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// add the user to the request | ||||
| 	c.Set("user", user) | ||||
| } | ||||
							
								
								
									
										27
									
								
								server/logs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								server/logs.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"strconv" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // GetLogs accepts a request to retrieve logs from the | ||||
| // datastore for the given repository, build and task | ||||
| // number. | ||||
| // | ||||
| //     GET /api/logs/:owner/:name/:number/:task | ||||
| // | ||||
| func GetLogs(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	build, _ := strconv.Atoi(c.Params.ByName("number")) | ||||
| 	task, _ := strconv.Atoi(c.Params.ByName("task")) | ||||
|  | ||||
| 	logs, err := ds.GetTaskLogs(repo.FullName, build, task) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} else { | ||||
| 		c.Writer.Write(logs) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										242
									
								
								server/repos.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								server/repos.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,242 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/gin-gonic/gin/binding" | ||||
|  | ||||
| 	"github.com/drone/drone/common" | ||||
| 	"github.com/drone/drone/common/httputil" | ||||
| 	"github.com/drone/drone/common/sshutil" | ||||
| 	"github.com/drone/drone/remote" | ||||
| ) | ||||
|  | ||||
| // repoResp is a data structure used for sending | ||||
| // repository data to the client, augmented with | ||||
| // additional repository meta-data. | ||||
| type repoResp struct { | ||||
| 	*common.Repo | ||||
| 	Perms  *common.Perm       `json:"permissions,omitempty"` | ||||
| 	Watch  *common.Subscriber `json:"subscription,omitempty"` | ||||
| 	Params map[string]string  `json:"params,omitempty"` | ||||
| } | ||||
|  | ||||
| // repoReq is a data structure used for receiving | ||||
| // repository data from the client to modify the | ||||
| // attributes of an existing repository. | ||||
| // | ||||
| // note that attributes are pointers so that we can | ||||
| // accept null values, effectively patching an existing | ||||
| // repository object with only the supplied fields. | ||||
| type repoReq struct { | ||||
| 	Disabled   *bool  `json:"disabled"` | ||||
| 	DisablePR  *bool  `json:"disable_prs"` | ||||
| 	DisableTag *bool  `json:"disable_tags"` | ||||
| 	Trusted    *bool  `json:"privileged"` | ||||
| 	Timeout    *int64 `json:"timeout"` | ||||
|  | ||||
| 	// optional private parameters can only be | ||||
| 	// supplied by the repository admin. | ||||
| 	Params *map[string]string `json:"params"` | ||||
| } | ||||
|  | ||||
| // GetRepo accepts a request to retrieve a commit | ||||
| // from the datastore for the given repository, branch and | ||||
| // commit hash. | ||||
| // | ||||
| //     GET /api/repos/:owner/:name | ||||
| // | ||||
| func GetRepo(c *gin.Context) { | ||||
| 	store := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	user := ToUser(c) | ||||
| 	perm := ToPerm(c) | ||||
| 	data := repoResp{repo, perm, nil, nil} | ||||
| 	// if the user is an administrator of the project | ||||
| 	// we should display the private parameter data. | ||||
| 	if perm.Admin { | ||||
| 		data.Params, _ = store.GetRepoParams(repo.FullName) | ||||
| 	} | ||||
| 	// if the user is authenticated, we should display | ||||
| 	// if she is watching the current repository. | ||||
| 	if user != nil { | ||||
| 		data.Watch, _ = store.GetSubscriber(repo.FullName, user.Login) | ||||
| 	} | ||||
| 	c.JSON(200, data) | ||||
| } | ||||
|  | ||||
| // PutRepo accapets a request to update the named repository | ||||
| // in the datastore. It expects a JSON input and returns the | ||||
| // updated repository in JSON format if successful. | ||||
| // | ||||
| //     PUT /api/repos/:owner/:name | ||||
| // | ||||
| func PutRepo(c *gin.Context) { | ||||
| 	store := ToDatastore(c) | ||||
| 	perm := ToPerm(c) | ||||
| 	u := ToUser(c) | ||||
| 	r := ToRepo(c) | ||||
|  | ||||
| 	in := &repoReq{} | ||||
| 	if !c.BindWith(in, binding.JSON) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if in.Params != nil { | ||||
| 		err := store.UpsertRepoParams(r.FullName, *in.Params) | ||||
| 		if err != nil { | ||||
| 			c.Fail(400, err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	if in.Disabled != nil { | ||||
| 		r.Disabled = *in.Disabled | ||||
| 	} | ||||
| 	if in.DisablePR != nil { | ||||
| 		r.DisablePR = *in.DisablePR | ||||
| 	} | ||||
| 	if in.DisableTag != nil { | ||||
| 		r.DisableTag = *in.DisableTag | ||||
| 	} | ||||
| 	if in.Trusted != nil && u.Admin { | ||||
| 		r.Trusted = *in.Trusted | ||||
| 	} | ||||
| 	if in.Timeout != nil && u.Admin { | ||||
| 		r.Timeout = *in.Timeout | ||||
| 	} | ||||
|  | ||||
| 	err := store.UpdateRepo(r) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data := repoResp{r, perm, nil, nil} | ||||
| 	data.Params, _ = store.GetRepoParams(r.FullName) | ||||
| 	data.Watch, _ = store.GetSubscriber(r.FullName, u.Login) | ||||
| 	c.JSON(200, data) | ||||
| } | ||||
|  | ||||
| // DeleteRepo accepts a request to delete the named | ||||
| // repository. | ||||
| // | ||||
| //     DEL /api/repos/:owner/:name | ||||
| // | ||||
| func DeleteRepo(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	u := ToUser(c) | ||||
| 	r := ToRepo(c) | ||||
|  | ||||
| 	link := fmt.Sprintf( | ||||
| 		"%s/api/hook", | ||||
| 		httputil.GetURL(c.Request), | ||||
| 	) | ||||
|  | ||||
| 	remote := ToRemote(c) | ||||
| 	err := remote.Deactivate(u, r, link) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} | ||||
|  | ||||
| 	err = ds.DeleteRepo(r) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} | ||||
| 	c.Writer.WriteHeader(200) | ||||
| } | ||||
|  | ||||
| // PostRepo accapets a request to activate the named repository | ||||
| // in the datastore. It returns a 201 status created if successful | ||||
| // | ||||
| //     POST /api/repos/:owner/:name | ||||
| // | ||||
| func PostRepo(c *gin.Context) { | ||||
| 	user := ToUser(c) | ||||
| 	store := ToDatastore(c) | ||||
| 	owner := c.Params.ByName("owner") | ||||
| 	name := c.Params.ByName("name") | ||||
|  | ||||
| 	link := fmt.Sprintf( | ||||
| 		"%s/api/hook", | ||||
| 		httputil.GetURL(c.Request), | ||||
| 	) | ||||
|  | ||||
| 	// TODO(bradrydzewski) verify repo not exists | ||||
|  | ||||
| 	// get the repository and user permissions | ||||
| 	// from the remote system. | ||||
| 	remote := ToRemote(c) | ||||
| 	r, err := remote.Repo(user, owner, name) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} | ||||
| 	m, err := remote.Perm(user, owner, name) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
| 	if !m.Admin { | ||||
| 		c.Fail(403, fmt.Errorf("must be repository admin")) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// set the repository owner to the | ||||
| 	// currently authenticated user. | ||||
| 	r.User = user | ||||
|  | ||||
| 	// generate an RSA key and add to the repo | ||||
| 	key, err := sshutil.GeneratePrivateKey() | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
| 	keypair := &common.Keypair{} | ||||
| 	keypair.Public = sshutil.MarshalPublicKey(&key.PublicKey) | ||||
| 	keypair.Private = sshutil.MarshalPrivateKey(key) | ||||
| 	err = store.UpsertRepoKeys(r.FullName, keypair) | ||||
| 	if err != nil { | ||||
| 		c.Fail(500, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// store the repository and the users' permissions | ||||
| 	// in the datastore. | ||||
| 	err = store.InsertRepo(user, r) | ||||
| 	if err != nil { | ||||
| 		c.Fail(500, err) | ||||
| 		return | ||||
| 	} | ||||
| 	err = store.InsertSubscriber(r.FullName, &common.Subscriber{Subscribed: true}) | ||||
| 	if err != nil { | ||||
| 		c.Fail(500, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = remote.Activate(user, r, keypair, link) | ||||
| 	if err != nil { | ||||
| 		c.Fail(500, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.JSON(200, r) | ||||
| } | ||||
|  | ||||
| // perms is a helper function that returns user permissions | ||||
| // for a particular repository. | ||||
| func perms(remote remote.Remote, u *common.User, r *common.Repo) *common.Perm { | ||||
| 	switch { | ||||
| 	case u == nil && r.Private: | ||||
| 		return &common.Perm{} | ||||
| 	case u == nil && r.Private == false: | ||||
| 		return &common.Perm{Pull: true} | ||||
| 	case u.Admin: | ||||
| 		return &common.Perm{Pull: true, Push: true, Admin: true} | ||||
| 	} | ||||
|  | ||||
| 	p, err := remote.Perm(u, r.Owner, r.Name) | ||||
| 	if err != nil { | ||||
| 		return &common.Perm{} | ||||
| 	} | ||||
| 	return p | ||||
| } | ||||
							
								
								
									
										241
									
								
								server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								server/server.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
|  | ||||
| 	"github.com/drone/drone/common" | ||||
| 	"github.com/drone/drone/datastore" | ||||
| 	"github.com/drone/drone/eventbus" | ||||
| 	"github.com/drone/drone/remote" | ||||
| 	"github.com/drone/drone/server/session" | ||||
| 	"github.com/drone/drone/settings" | ||||
| ) | ||||
|  | ||||
| func SetBus(r eventbus.Bus) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		c.Set("eventbus", r) | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ToBus(c *gin.Context) eventbus.Bus { | ||||
| 	v, err := c.Get("eventbus") | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return v.(eventbus.Bus) | ||||
| } | ||||
|  | ||||
| func ToRemote(c *gin.Context) remote.Remote { | ||||
| 	v, err := c.Get("remote") | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return v.(remote.Remote) | ||||
| } | ||||
|  | ||||
| func SetRemote(r remote.Remote) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		c.Set("remote", r) | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ToSettings(c *gin.Context) *settings.Settings { | ||||
| 	v, err := c.Get("settings") | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return v.(*settings.Settings) | ||||
| } | ||||
|  | ||||
| func SetSettings(s *settings.Settings) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		c.Set("settings", s) | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ToPerm(c *gin.Context) *common.Perm { | ||||
| 	v, err := c.Get("perm") | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return v.(*common.Perm) | ||||
| } | ||||
|  | ||||
| func ToUser(c *gin.Context) *common.User { | ||||
| 	v, err := c.Get("user") | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return v.(*common.User) | ||||
| } | ||||
|  | ||||
| func ToRepo(c *gin.Context) *common.Repo { | ||||
| 	v, err := c.Get("repo") | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return v.(*common.Repo) | ||||
| } | ||||
|  | ||||
| func ToDatastore(c *gin.Context) datastore.Datastore { | ||||
| 	return c.MustGet("datastore").(datastore.Datastore) | ||||
| } | ||||
|  | ||||
| func ToSession(c *gin.Context) session.Session { | ||||
| 	return c.MustGet("session").(session.Session) | ||||
| } | ||||
|  | ||||
| func SetDatastore(ds datastore.Datastore) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		c.Set("datastore", ds) | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func SetSession(s session.Session) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		c.Set("session", s) | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func SetUser(s session.Session) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		ds := ToDatastore(c) | ||||
| 		login := s.GetLogin(c.Request) | ||||
| 		if len(login) == 0 { | ||||
| 			c.Next() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		u, err := ds.GetUser(login) | ||||
| 		if err == nil { | ||||
| 			c.Set("user", u) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func SetRepo() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		ds := ToDatastore(c) | ||||
| 		u := ToUser(c) | ||||
| 		owner := c.Params.ByName("owner") | ||||
| 		name := c.Params.ByName("name") | ||||
| 		r, err := ds.GetRepo(owner + "/" + name) | ||||
| 		switch { | ||||
| 		case err != nil && u != nil: | ||||
| 			c.Fail(401, err) | ||||
| 			return | ||||
| 		case err != nil && u == nil: | ||||
| 			c.Fail(404, err) | ||||
| 			return | ||||
| 		} | ||||
| 		c.Set("repo", r) | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func SetPerm() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		remote := ToRemote(c) | ||||
| 		user := ToUser(c) | ||||
| 		repo := ToRepo(c) | ||||
| 		perm := perms(remote, user, repo) | ||||
| 		c.Set("perm", perm) | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func MustUser() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		u := ToUser(c) | ||||
| 		if u == nil { | ||||
| 			c.AbortWithStatus(401) | ||||
| 		} else { | ||||
| 			c.Set("user", u) | ||||
| 			c.Next() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func MustAdmin() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		u := ToUser(c) | ||||
| 		if u == nil { | ||||
| 			c.AbortWithStatus(401) | ||||
| 		} else if !u.Admin { | ||||
| 			c.AbortWithStatus(403) | ||||
| 		} else { | ||||
| 			c.Set("user", u) | ||||
| 			c.Next() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func CheckPull() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		u := ToUser(c) | ||||
| 		m := ToPerm(c) | ||||
|  | ||||
| 		switch { | ||||
| 		case u == nil && m == nil: | ||||
| 			c.AbortWithStatus(401) | ||||
| 		case u == nil && m.Pull == false: | ||||
| 			c.AbortWithStatus(401) | ||||
| 		case u != nil && m.Pull == false: | ||||
| 			c.AbortWithStatus(404) | ||||
| 		default: | ||||
| 			c.Next() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func CheckPush() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		switch c.Request.Method { | ||||
| 		case "GET", "OPTIONS": | ||||
| 			c.Next() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		u := ToUser(c) | ||||
| 		m := ToPerm(c) | ||||
|  | ||||
| 		switch { | ||||
| 		case u == nil && m.Push == false: | ||||
| 			c.AbortWithStatus(401) | ||||
| 		case u != nil && m.Push == false: | ||||
| 			c.AbortWithStatus(404) | ||||
| 		default: | ||||
| 			c.Next() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func SetHeaders() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
|  | ||||
| 		c.Writer.Header().Add("Access-Control-Allow-Origin", "*") | ||||
| 		c.Writer.Header().Add("X-Frame-Options", "DENY") | ||||
| 		c.Writer.Header().Add("X-Content-Type-Options", "nosniff") | ||||
| 		c.Writer.Header().Add("X-XSS-Protection", "1; mode=block") | ||||
| 		c.Writer.Header().Add("Cache-Control", "no-cache") | ||||
| 		c.Writer.Header().Add("Cache-Control", "no-store") | ||||
| 		c.Writer.Header().Add("Cache-Control", "max-age=0") | ||||
| 		c.Writer.Header().Add("Cache-Control", "must-revalidate") | ||||
| 		c.Writer.Header().Add("Cache-Control", "value") | ||||
| 		c.Writer.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) | ||||
| 		c.Writer.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") | ||||
| 		if c.Request.TLS != nil { | ||||
| 			c.Writer.Header().Add("Strict-Transport-Security", "max-age=31536000") | ||||
| 		} | ||||
|  | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										109
									
								
								server/session/session.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								server/session/session.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| package session | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/dgrijalva/jwt-go" | ||||
| 	"github.com/drone/drone/common" | ||||
| 	"github.com/drone/drone/common/httputil" | ||||
| 	"github.com/drone/drone/settings" | ||||
| 	"github.com/gorilla/securecookie" | ||||
| ) | ||||
|  | ||||
| type Session interface { | ||||
| 	GenerateToken(*http.Request, *common.User) (string, error) | ||||
| 	GetLogin(*http.Request) string | ||||
| } | ||||
|  | ||||
| type session struct { | ||||
| 	secret []byte | ||||
| 	expire time.Duration | ||||
| } | ||||
|  | ||||
| func New(s *settings.Session) Session { | ||||
| 	secret := securecookie.GenerateRandomKey(32) | ||||
| 	expire := time.Hour * 72 | ||||
| 	return &session{ | ||||
| 		secret: secret, | ||||
| 		expire: expire, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GenerateToken generates a JWT token for the user session | ||||
| // that can be appended to the #access_token segment to | ||||
| // facilitate client-based OAuth2. | ||||
| func (s *session) GenerateToken(r *http.Request, user *common.User) (string, error) { | ||||
| 	token := jwt.New(jwt.GetSigningMethod("HS256")) | ||||
| 	token.Claims["user_id"] = user.Login | ||||
| 	token.Claims["audience"] = httputil.GetURL(r) | ||||
| 	token.Claims["expires"] = time.Now().UTC().Add(s.expire).Unix() | ||||
| 	return token.SignedString(s.secret) | ||||
| } | ||||
|  | ||||
| // GetLogin gets the currently authenticated user for the | ||||
| // http.Request. The user details will be stored as either | ||||
| // a simple API token or JWT bearer token. | ||||
| func (s *session) GetLogin(r *http.Request) (_ string) { | ||||
| 	token := getToken(r) | ||||
| 	if len(token) == 0 { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	claims := getClaims(token, s.secret) | ||||
| 	if claims == nil || claims["user_id"] == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	userid, ok := claims["user_id"].(string) | ||||
| 	if !ok { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// tokenid, ok := claims["token_id"].(string) | ||||
| 	// if ok { | ||||
| 	// 	_, err := datastore.GetToken(c, int64(tokenid)) | ||||
| 	// 	if err != nil { | ||||
| 	// 		return nil | ||||
| 	// 	} | ||||
| 	// } | ||||
|  | ||||
| 	return userid | ||||
| } | ||||
|  | ||||
| // getToken is a helper function that extracts the token | ||||
| // from the http.Request. | ||||
| func getToken(r *http.Request) string { | ||||
| 	token := getTokenHeader(r) | ||||
| 	if len(token) == 0 { | ||||
| 		token = getTokenParam(r) | ||||
| 	} | ||||
| 	return token | ||||
| } | ||||
|  | ||||
| // getTokenHeader parses the JWT token value from | ||||
| // the http Authorization header. | ||||
| func getTokenHeader(r *http.Request) string { | ||||
| 	var tokenstr = r.Header.Get("Authorization") | ||||
| 	fmt.Sscanf(tokenstr, "Bearer %s", &tokenstr) | ||||
| 	return tokenstr | ||||
| } | ||||
|  | ||||
| // getTokenParam parses the JWT token value from | ||||
| // the http Request's query parameter. | ||||
| func getTokenParam(r *http.Request) string { | ||||
| 	return r.FormValue("access_token") | ||||
| } | ||||
|  | ||||
| // getClaims is a helper function that extracts the token | ||||
| // claims from the JWT token string. | ||||
| func getClaims(token string, secret []byte) map[string]interface{} { | ||||
| 	t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { | ||||
| 		return secret, nil | ||||
| 	}) | ||||
| 	if err != nil || !t.Valid { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return t.Claims | ||||
| } | ||||
							
								
								
									
										39
									
								
								server/static/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								server/static/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| <!doctype html> | ||||
| <html ng-app="drone" lang="en"> | ||||
| 	<head> | ||||
| 		<base href="/"> | ||||
| 		<meta charset="utf-8"> | ||||
| 		<meta name="author" content="Brad Rydzewski and the Drone Authors"> | ||||
| 		<meta name="viewport" content="width=device-width, user-scalable=no"> | ||||
| 		<link rel="shortcut icon" href="/favicon.ico"/> | ||||
| 		<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet" type="text/css"> | ||||
| 		<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,300,700" rel="stylesheet" type="text/css" /> | ||||
| 		<link href="https://cdnjs.cloudflare.com/ajax/libs/octicons/2.1.2/octicons.min.css" rel="stylesheet" type="text/css" /> | ||||
| 	</head> | ||||
| 	<body ng-cloak> | ||||
| 		<main role="main" ng-view></main> | ||||
|  | ||||
| 		<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.8/angular.js"></script> | ||||
| 		<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.8/angular-route.js"></script> | ||||
| 		<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.8/angular-resource.js"></script> | ||||
| 		<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui/0.4.0/angular-ui.js"></script> | ||||
| 		<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.6.0/moment.min.js"></script> | ||||
|  | ||||
| 		<!-- main javascript application --> | ||||
| 		<script src="/static/scripts/drone.js"></script> | ||||
| 		<script src="/static/scripts/controllers/repos.js"></script> | ||||
| 		<script src="/static/scripts/controllers/builds.js"></script> | ||||
| 		<script src="/static/scripts/controllers/users.js"></script> | ||||
|  | ||||
| 		<script src="/static/scripts/services/repos.js"></script> | ||||
| 		<script src="/static/scripts/services/builds.js"></script> | ||||
| 		<script src="/static/scripts/services/tasks.js"></script> | ||||
| 		<script src="/static/scripts/services/users.js"></script> | ||||
| 		<script src="/static/scripts/services/logs.js"></script> | ||||
|  | ||||
| 		<script src="/static/scripts/filters/filter.js"></script> | ||||
| 		<script src="/static/scripts/filters/gravatar.js"></script> | ||||
| 		<script src="/static/scripts/filters/time.js"></script> | ||||
|  | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										101
									
								
								server/static/scripts/controllers/builds.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								server/static/scripts/controllers/builds.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * BuildsCtrl responsible for rendering the repo's | ||||
| 	 * recent build history. | ||||
| 	 */	 | ||||
| 	function BuildsCtrl($scope, $routeParams, builds, repos, users) { | ||||
|  | ||||
| 		var owner = $routeParams.owner; | ||||
| 		var name  = $routeParams.name; | ||||
| 		var fullName = owner+'/'+name; | ||||
|  | ||||
| 		// Gets the currently authenticated user  | ||||
| 		users.getCached().then(function(payload){ | ||||
| 			$scope.user = payload.data; | ||||
| 		}); | ||||
| 		 | ||||
| 		// Gets a repository | ||||
| 		repos.get(fullName).then(function(payload){ | ||||
| 			$scope.repo = payload.data; | ||||
| 		}).catch(function(err){ | ||||
| 			$scope.error = err; | ||||
| 		}); | ||||
|  | ||||
| 		// Gets a list of builds | ||||
| 		builds.list(fullName).then(function(payload){ | ||||
| 			$scope.builds = angular.isArray(payload.data) ? payload.data : []; | ||||
| 		}).catch(function(err){ | ||||
| 			$scope.error = err; | ||||
| 		}); | ||||
|  | ||||
| 		$scope.watch = function(repo) { | ||||
| 			repos.watch(repo.full_name).then(function(payload) { | ||||
| 				$scope.repo.subscription = payload.data; | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		$scope.unwatch = function(repo) { | ||||
| 			repos.unwatch(repo.full_name).then(function() { | ||||
| 				delete $scope.repo.subscription; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * BuildCtrl responsible for rendering a build. | ||||
| 	 */	 | ||||
| 	function BuildCtrl($scope, $routeParams, logs, tasks, builds, repos, users) { | ||||
|  | ||||
| 		var step = parseInt($routeParams.step) || 1; | ||||
| 		var number = $routeParams.number; | ||||
| 		var owner = $routeParams.owner; | ||||
| 		var name  = $routeParams.name; | ||||
| 		var fullName = owner+'/'+name; | ||||
|  | ||||
| 		// Gets the currently authenticated user  | ||||
| 		users.getCached().then(function(payload){ | ||||
| 			$scope.user = payload.data; | ||||
| 		}); | ||||
| 		 | ||||
| 		// Gets a repository | ||||
| 		repos.get(fullName).then(function(payload){ | ||||
| 			$scope.repo = payload.data; | ||||
| 		}).catch(function(err){ | ||||
| 			$scope.error = err; | ||||
| 		}); | ||||
|  | ||||
| 		// Gets the build | ||||
| 		builds.get(fullName, number).then(function(payload){ | ||||
| 			$scope.build = payload.data; | ||||
| 		}).catch(function(err){ | ||||
| 			$scope.error = err; | ||||
| 		}); | ||||
|  | ||||
| 		// Gets a list of build steps | ||||
| 		tasks.list(fullName, number).then(function(payload){ | ||||
| 			$scope.tasks = payload.data || []; | ||||
| 			$scope.tasks.forEach(function(task) { | ||||
| 				if (task.number === step) { | ||||
| 					$scope.task = task; | ||||
| 				} | ||||
| 			}); | ||||
| 		}).catch(function(err){ | ||||
| 			$scope.error = err; | ||||
| 		}); | ||||
|  | ||||
| 		if (step) { | ||||
| 			// Gets a list of build steps | ||||
| 			logs.get(fullName, number, step).then(function(payload){ | ||||
| 				$scope.logs = payload.data; | ||||
| 			}).catch(function(err){ | ||||
| 				$scope.error = err; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.controller('BuildCtrl', BuildCtrl) | ||||
| 		.controller('BuildsCtrl', BuildsCtrl); | ||||
| })(); | ||||
							
								
								
									
										92
									
								
								server/static/scripts/controllers/repos.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								server/static/scripts/controllers/repos.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * ReposCtrl responsible for rendering the user's | ||||
| 	 * repository home screen. | ||||
| 	 */	 | ||||
| 	function ReposCtrl($scope, $routeParams, repos, users) { | ||||
|  | ||||
| 		// Gets the currently authenticated user  | ||||
| 		users.getCached().then(function(payload){ | ||||
| 			$scope.user = payload.data; | ||||
| 		}); | ||||
| 		 | ||||
| 		// Gets a list of repos to display in the | ||||
| 		// dropdown. | ||||
| 		repos.list().then(function(payload){ | ||||
| 			$scope.repos = angular.isArray(payload.data) ? payload.data : []; | ||||
| 		}).catch(function(err){ | ||||
| 			$scope.error = err; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * RepoAddCtrl responsible for activaing a new | ||||
| 	 * repository. | ||||
| 	 */	 | ||||
| 	function RepoAddCtrl($scope, $location, repos, users) { | ||||
| 		$scope.add = function(slug) { | ||||
| 			repos.post(slug).then(function(payload) { | ||||
| 				$location.path('/'+slug); | ||||
| 			}).catch(function(err){ | ||||
| 				$scope.error = err; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * RepoEditCtrl responsible for editing a repository. | ||||
| 	 */	 | ||||
| 	function RepoEditCtrl($scope, $location, $routeParams, repos, users) { | ||||
| 		var owner = $routeParams.owner; | ||||
| 		var name  = $routeParams.name; | ||||
| 		var fullName = owner+'/'+name; | ||||
|  | ||||
| 		// Gets the currently authenticated user  | ||||
| 		users.getCached().then(function(payload){ | ||||
| 			$scope.user = payload.data; | ||||
| 		}); | ||||
| 		 | ||||
| 		// Gets a repository | ||||
| 		repos.get(fullName).then(function(payload){ | ||||
| 			$scope.repo = payload.data; | ||||
| 		}).catch(function(err){ | ||||
| 			$scope.error = err; | ||||
| 		}); | ||||
|  | ||||
| 		$scope.save = function(repo) { | ||||
| 			repos.update(repo).then(function(payload) { | ||||
| 				$scope.repo = payload.data; | ||||
| 			}).catch(function(err){ | ||||
| 				$scope.error = err; | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		$scope.delete = function(repo) { | ||||
| 			repos.delete(repo).then(function(payload) { | ||||
| 				$location.path('/'); | ||||
| 			}).catch(function(err){ | ||||
| 				$scope.error = err; | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		$scope.param={} | ||||
| 		$scope.addParam = function(param) { | ||||
| 			if (!$scope.repo.params) { | ||||
| 				$scope.repo.params = {} | ||||
| 			} | ||||
| 			$scope.repo.params[param.key]=param.value; | ||||
| 			$scope.param={} | ||||
| 		} | ||||
|  | ||||
| 		$scope.deleteParam = function(key) { | ||||
| 			delete $scope.repo.params[key]; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.controller('ReposCtrl', ReposCtrl) | ||||
| 		.controller('RepoAddCtrl', RepoAddCtrl) | ||||
| 		.controller('RepoEditCtrl', RepoEditCtrl); | ||||
| })(); | ||||
							
								
								
									
										54
									
								
								server/static/scripts/controllers/users.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								server/static/scripts/controllers/users.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * UserCtrl is responsible for managing user settings. | ||||
| 	 */	 | ||||
| 	function UserCtrl($scope, users) { | ||||
|  | ||||
| 		// Gets the currently authenticated user  | ||||
| 		users.getCurrent().then(function(payload){ | ||||
| 			$scope.user = payload.data; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * UsersCtrl is responsible for managing user accounts. | ||||
| 	 * This part of the site is for administrators only. | ||||
| 	 */	 | ||||
| 	function UsersCtrl($scope, users) { | ||||
| 		// Gets the currently authenticated user  | ||||
| 		users.getCached().then(function(payload){ | ||||
| 			$scope.user = payload.data; | ||||
| 		}); | ||||
|  | ||||
| 		users.list().then(function(payload){ | ||||
| 			$scope.users = payload.data; | ||||
| 		}); | ||||
|  | ||||
| 		$scope.login=""; | ||||
| 		$scope.add = function(login) { | ||||
| 			users.post(login).then(function(payload){ | ||||
| 				$scope.users.push(payload.data); | ||||
| 				$scope.login=""; | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		$scope.toggle = function(user) { | ||||
| 			user.admin = !user.admin; | ||||
| 			users.put(user); | ||||
| 		} | ||||
|  | ||||
| 		$scope.remove = function(user) { | ||||
| 			users.delete(user).then(function(){ | ||||
| 				users.list().then(function(payload){ | ||||
| 					$scope.users = payload.data; | ||||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.controller('UserCtrl', UserCtrl) | ||||
| 		.controller('UsersCtrl', UsersCtrl); | ||||
| })(); | ||||
							
								
								
									
										140
									
								
								server/static/scripts/drone.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								server/static/scripts/drone.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Creates the angular application. | ||||
| 	 */ | ||||
| 	angular.module('drone', [ | ||||
| 			'ngRoute', | ||||
| 			'ui.filters' | ||||
| 		]); | ||||
|  | ||||
| 	/** | ||||
| 	 * Bootstraps the application and retrieves the | ||||
| 	 * token from the  | ||||
| 	 */ | ||||
| 	function Authorize() { | ||||
| 		// First, parse the query string | ||||
| 		var params = {}, queryString = location.hash.substring(1), | ||||
| 			regex  = /([^&=]+)=([^&]*)/g, m; | ||||
|  | ||||
| 		// Loop through and retrieve the token | ||||
| 		while (m = regex.exec(queryString)) { | ||||
| 			params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); | ||||
| 		} | ||||
|  | ||||
| 		// if the user has just received an auth token we | ||||
| 		// should extract from the URL, save to local storage | ||||
| 		// and then remove from the URL for good measure. | ||||
| 		if (params.access_token) { | ||||
| 			localStorage.setItem("access_token", params.access_token); | ||||
| 			history.replaceState({}, document.title, location.pathname); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Defines the route configuration for the | ||||
| 	 * main application. | ||||
| 	 */ | ||||
| 	function Config ($routeProvider, $httpProvider, $locationProvider) { | ||||
|  | ||||
| 		// Resolver that will attempt to load the currently | ||||
| 		// authenticated user prior to loading the page. | ||||
| 		var resolveUser = { | ||||
| 			user: function(users) { | ||||
| 				return users.getCached(); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		$routeProvider | ||||
| 		.when('/', { | ||||
| 			templateUrl: '/static/scripts/views/repos.html', | ||||
| 			controller: 'ReposCtrl', | ||||
| 			resolve: resolveUser | ||||
| 		}) | ||||
| 		.when('/login', { | ||||
| 			templateUrl: '/static/scripts/views/login.html', | ||||
| 		}) | ||||
| 		.when('/profile', { | ||||
| 			templateUrl: '/static/scripts/views/user.html', | ||||
| 			controller: 'UserCtrl', | ||||
| 			resolve: resolveUser | ||||
| 		}) | ||||
| 		.when('/users', { | ||||
| 			templateUrl: '/static/scripts/views/users.html', | ||||
| 			controller: 'UsersCtrl', | ||||
| 			resolve: resolveUser | ||||
| 		}) | ||||
| 		.when('/new', { | ||||
| 			templateUrl: '/static/scripts/views/repos_add.html', | ||||
| 			controller: 'RepoAddCtrl', | ||||
| 			resolve: resolveUser | ||||
| 		}) | ||||
| 		.when('/:owner/:name', { | ||||
| 			templateUrl: '/static/scripts/views/builds.html', | ||||
| 			controller: 'BuildsCtrl', | ||||
| 			resolve: resolveUser | ||||
| 		}) | ||||
| 		.when('/:owner/:name/edit', { | ||||
| 			templateUrl: '/static/scripts/views/repos_edit.html', | ||||
| 			controller: 'RepoEditCtrl', | ||||
| 			resolve: resolveUser | ||||
| 		}) | ||||
| 		.when('/:owner/:name/:number', { | ||||
| 			templateUrl: '/static/scripts/views/build.html', | ||||
| 			controller: 'BuildCtrl', | ||||
| 			resolve: resolveUser | ||||
| 		}) | ||||
| 		.when('/:owner/:name/:number/:step', { | ||||
| 			templateUrl: '/static/scripts/views/build.html', | ||||
| 			controller: 'BuildCtrl', | ||||
| 			resolve: resolveUser | ||||
| 		}); | ||||
|  | ||||
| 		// Enables html5 mode | ||||
| 		$locationProvider.html5Mode(true) | ||||
|  | ||||
| 		// Appends the Bearer token to authorize every | ||||
| 		// outbound http request. | ||||
| 		$httpProvider.defaults.headers.common.Authorization = 'Bearer '+localStorage.getItem('access_token'); | ||||
|  | ||||
| 		// Intercepts every oubput http response and redirects | ||||
| 		// the user to the logic screen if the request was rejected. | ||||
| 		$httpProvider.interceptors.push(function($q, $location) { | ||||
| 			return { | ||||
| 				'responseError': function(rejection) { | ||||
| 					if (rejection.status === 401) {// && rejection.config.url != "/api/user") { | ||||
| 						$location.path('/login'); | ||||
| 					} | ||||
| 					if (rejection.status === 0) { | ||||
| 						// this happens when the app is down or | ||||
| 						// the browser loses internet connectivity. | ||||
| 					} | ||||
| 					return $q.reject(rejection); | ||||
| 				} | ||||
| 			}; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	// /** | ||||
| 	//  * | ||||
| 	//  */ | ||||
| 	// function RouteChange($rootScope, stdout, projs) { | ||||
| 	// 	$rootScope.$on('$routeChangeStart', function (event, next) { | ||||
| 	// 		projs.unsubscribe(); | ||||
| 	// 		stdout.unsubscribe(); | ||||
| 	// 	}); | ||||
|  | ||||
| 	// 	//$rootScope.$on('$routeChangeSuccess', function (event, current, previous) { | ||||
|  // 		//	document.title = current.$$route.title + ' · drone.io'; | ||||
| 	// 	//}); | ||||
| 	// } | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.config(Authorize) | ||||
| 		.config(Config); | ||||
| 		// .run(RouteChange); | ||||
|  | ||||
| })(); | ||||
							
								
								
									
										61
									
								
								server/static/scripts/filters/filter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								server/static/scripts/filters/filter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * author is a helper function that return the builds | ||||
| 	 * commit or pull request author. | ||||
| 	*/ | ||||
| 	function author() { | ||||
| 		return function(build) { | ||||
| 			if (!build) { return ""; } | ||||
| 			if (!build.head_commit && !build.pull_request) { return ""; } | ||||
| 			if (build.head_commit) { return build.head_commit.author.login || ""; } | ||||
| 			return build.pull_request.source.author.login; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * sha is a helper function that return the builds sha. | ||||
| 	*/ | ||||
| 	function sha() { | ||||
| 		return function(build) { | ||||
| 			if (!build) { return ""; } | ||||
| 			if (!build.head_commit && !build.pull_request) { return ""; } | ||||
| 			if (build.head_commit) { return build.head_commit.sha || ""; } | ||||
| 			return build.pull_request.source.sha; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * ref is a helper function that return the builds sha. | ||||
| 	*/ | ||||
| 	function ref() { | ||||
| 		return function(build) { | ||||
| 			if (!build) { return ""; } | ||||
| 			if (!build.head_commit && !build.pull_request) { return ""; } | ||||
| 			if (build.head_commit) { return build.head_commit.ref || ""; } | ||||
| 			return build.pull_request.source.ref; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * message is a helper function that return the builds message. | ||||
| 	*/ | ||||
| 	function message() { | ||||
| 		return function(build) { | ||||
| 			if (!build) { return ""; } | ||||
| 			if (!build.head_commit && !build.pull_request) { return ""; } | ||||
| 			if (build.head_commit) { return build.head_commit.message || ""; } | ||||
| 			return build.pull_request.title || ""; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.filter('author', author) | ||||
| 		.filter('message', message) | ||||
| 		.filter('sha', sha) | ||||
| 		.filter('ref', ref); | ||||
|  | ||||
| })(); | ||||
							
								
								
									
										32
									
								
								server/static/scripts/filters/gravatar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								server/static/scripts/filters/gravatar.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * gravatar is a helper function that return the user's gravatar | ||||
| 	 * image URL given an email hash. | ||||
| 	*/ | ||||
| 	function gravatar() { | ||||
| 		return function(hash) { | ||||
| 			if (hash  === undefined) { return ""; } | ||||
| 			return "https://secure.gravatar.com/avatar/"+hash+"?s=48&d=mm"; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * gravatarLarge is a helper function that return the user's gravatar | ||||
| 	 * image URL given an email hash. | ||||
| 	 */ | ||||
| 	function gravatarLarge() { | ||||
| 		return function(hash) { | ||||
| 			if (hash === undefined) { return ""; } | ||||
| 			return "https://secure.gravatar.com/avatar/"+hash+"?s=128&d=mm"; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.filter('gravatar', gravatar) | ||||
| 		.filter('gravatarLarge', gravatarLarge) | ||||
|  | ||||
| })(); | ||||
							
								
								
									
										45
									
								
								server/static/scripts/filters/time.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								server/static/scripts/filters/time.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| (function () { | ||||
|  | ||||
|   /** | ||||
|    * fromNow is a helper function that returns a human readable | ||||
|    * string for the elapsed time between the given unix date and the | ||||
|    * current time (ex. 10 minutes ago). | ||||
|    */ | ||||
| 	function fromNow() { | ||||
| 		return function(date) { | ||||
| 			if (!date) { | ||||
| 				return; | ||||
| 			} | ||||
|  			return moment(new Date(date*1000)).fromNow(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * toDuration is a helper function that returns a human readable | ||||
| 	 * string for the given duration in seconds (ex. 1 hour and 20 minutes). | ||||
| 	*/ | ||||
| 	function toDuration() { | ||||
| 		return function(seconds) { | ||||
| 			return moment.duration(seconds, "seconds").humanize(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|  	/** | ||||
| 	 * toDate is a helper function that returns a human readable | ||||
| 	 * string gor the given unix date. | ||||
| 	*/ | ||||
| 	function toDate() { | ||||
| 		return function(date) { | ||||
| 			return moment(new Date(date*1000)).format('ll'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.filter('fromNow', fromNow) | ||||
| 		.filter('toDate', toDate) | ||||
| 		.filter('toDuration', toDuration) | ||||
|  | ||||
| })(); | ||||
							
								
								
									
										34
									
								
								server/static/scripts/services/builds.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								server/static/scripts/services/builds.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * The BuildsService provides access to build | ||||
| 	 * data using REST API calls. | ||||
| 	 */ | ||||
| 	function BuildService($http, $window) { | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a list of builds. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 */ | ||||
| 		this.list = function(repoName) { | ||||
| 			return $http.get('/api/builds/'+repoName); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a build. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 * @param {number} Number of the build. | ||||
| 		 */ | ||||
| 		this.get = function(repoName, buildNumber) { | ||||
| 			return $http.get('/api/builds/'+repoName+'/'+buildNumber); | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.service('builds', BuildService); | ||||
| })(); | ||||
							
								
								
									
										26
									
								
								server/static/scripts/services/logs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								server/static/scripts/services/logs.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * The LogService provides access to build | ||||
| 	 * log data using REST API calls. | ||||
| 	 */ | ||||
| 	function LogService($http, $window) { | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a task logs. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 * @param {number} Number of the build. | ||||
| 		 * @param {number} Number of the task. | ||||
| 		 */ | ||||
| 		this.get = function(repoName, number, step) { | ||||
| 			return $http.get('/api/logs/'+repoName+'/'+number+'/'+step); | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.service('logs', LogService); | ||||
| })(); | ||||
							
								
								
									
										80
									
								
								server/static/scripts/services/repos.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								server/static/scripts/services/repos.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * The RepoService provides access to repository | ||||
| 	 * data using REST API calls. | ||||
| 	 */ | ||||
| 	function RepoService($http, $window) { | ||||
|  | ||||
| 		var callback, | ||||
| 			websocket, | ||||
| 			token = localStorage.getItem('access_token'); | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a list of all repositories. | ||||
| 		 */ | ||||
| 		this.list = function() { | ||||
| 			return $http.get('/api/user/repos'); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a repository by name. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 */ | ||||
| 		this.get = function(repoName) { | ||||
| 			return $http.get('/api/repos/'+repoName); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Creates a new repository. | ||||
| 		 * | ||||
| 		 * @param {object} JSON representation of a repository. | ||||
| 		 */ | ||||
| 		this.post = function(repoName) { | ||||
| 			return $http.post('/api/repos/' + repoName); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Updates an existing repository. | ||||
| 		 * | ||||
| 		 * @param {object} JSON representation of a repository. | ||||
| 		 */ | ||||
| 		this.update = function(repo) { | ||||
| 			return $http.put('/api/repos/'+repo.full_name, repo); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Deletes a repository. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 */ | ||||
| 		this.delete = function(repoName) { | ||||
| 			return $http.delete('/api/repos/'+repoName); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Watch a repository. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 */ | ||||
| 		this.watch = function(repoName) { | ||||
| 			return $http.post('/api/subscribers/'+repoName); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Unwatch a repository. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 */ | ||||
| 		this.unwatch = function(repoName) { | ||||
| 			return $http.delete('/api/subscribers/'+repoName); | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.service('repos', RepoService); | ||||
| })(); | ||||
							
								
								
									
										47
									
								
								server/static/scripts/services/tasks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								server/static/scripts/services/tasks.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * The TaskService provides access to build | ||||
| 	 * task data using REST API calls. | ||||
| 	 */ | ||||
| 	function TaskService($http, $window) { | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a list of builds. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 * @param {number} Number of the build. | ||||
| 		 */ | ||||
| 		this.list = function(repoName, number) { | ||||
| 			return $http.get('/api/tasks/'+repoName+'/'+number); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a task. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 * @param {number} Number of the build. | ||||
| 		 * @param {number} Number of the task. | ||||
| 		 */ | ||||
| 		this.get = function(repoName, number, step) { | ||||
| 			return $http.get('/api/tasks/'+repoName+'/'+name+'/'+step); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a task. | ||||
| 		 * | ||||
| 		 * @param {string} Name of the repository. | ||||
| 		 * @param {number} Number of the build. | ||||
| 		 * @param {number} Number of the task. | ||||
| 		 */ | ||||
| 		this.get = function(repoName, number, step) { | ||||
| 			return $http.get('/api/tasks/'+repoName+'/'+name+'/'+step); | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.service('tasks', TaskService); | ||||
| })(); | ||||
							
								
								
									
										86
									
								
								server/static/scripts/services/users.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								server/static/scripts/services/users.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| (function () { | ||||
|  | ||||
| 	/** | ||||
| 	 * Cached user object. | ||||
| 	 */ | ||||
| 	var _user; | ||||
|  | ||||
| 	/** | ||||
| 	 * The UserService provides access to useer | ||||
| 	 * data using REST API calls. | ||||
| 	 */ | ||||
| 	function UserService($http, $q) { | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a list of all users. | ||||
| 		 */ | ||||
| 		this.list = function() { | ||||
| 			return $http.get('/api/users'); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets a user by login. | ||||
| 		 */ | ||||
| 		this.get = function(login) { | ||||
| 			return $http.get('/api/users/'+login); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets the currently authenticated user. | ||||
| 		 */ | ||||
| 		this.getCurrent = function() { | ||||
| 			return $http.get('/api/user'); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Updates an existing user | ||||
| 		 */ | ||||
| 		this.post = function(user) { | ||||
| 			return $http.post('/api/users/'+user); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Updates an existing user | ||||
| 		 */ | ||||
| 		this.put = function(user) { | ||||
| 			return $http.put('/api/users/'+user.login, user); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Deletes a user. | ||||
| 		 */ | ||||
| 		this.delete = function(user) { | ||||
| 			return $http.delete('/api/users/'+user.login); | ||||
| 		}; | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets the currently authenticated user from | ||||
| 		 * the local cache. If not exists, it will fetch | ||||
| 		 * from the server. | ||||
| 		 */ | ||||
| 		this.getCached = function() { | ||||
| 			var defer = $q.defer(); | ||||
|  | ||||
| 			// if the user is already authenticated | ||||
| 			if (_user) { | ||||
| 				defer.resolve(_user); | ||||
| 				return defer.promise; | ||||
| 			} | ||||
|  | ||||
| 			// else fetch the currently authenticated | ||||
| 			// user using the REST API. | ||||
| 			this.getCurrent().then(function(payload){ | ||||
| 				_user=payload; | ||||
| 				defer.resolve(_user); | ||||
| 			}); | ||||
|  | ||||
| 			return defer.promise; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	angular | ||||
| 		.module('drone') | ||||
| 		.service('users', UserService); | ||||
| })(); | ||||
							
								
								
									
										80
									
								
								server/static/scripts/views/build.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								server/static/scripts/views/build.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| <h1>{{ repo.full_name }}/{{ build.number }}</h1> | ||||
|  | ||||
| <a href="/{{ repo.full_name }}">Back</a> | ||||
|  | ||||
| <dl> | ||||
| 	<dt>Build State</dt> | ||||
| 	<dd>{{ build.state }}</dd> | ||||
|  | ||||
| 	<dt>Started</dt> | ||||
| 	<dd>{{ build.started_at | fromNow }}</dd> | ||||
|  | ||||
| 	<dt>Duration</dt> | ||||
| 	<dd>{{ build.duration | toDuration }}</dd> | ||||
|  | ||||
| 	<dt>Type</dt> | ||||
| 	<dd>{{ build.head_commit ? "push" : "pull request" }}</dd> | ||||
|  | ||||
| 	<dt>Ref</dt> | ||||
| 	<dd>{{ build | ref }}</dd> | ||||
|  | ||||
| 	<dt>Sha</dt> | ||||
| 	<dd>{{ build | sha }}</dd> | ||||
|  | ||||
| 	<dt>Author</dt> | ||||
| 	<dd>{{ build | author }}</dd> | ||||
|  | ||||
| 	<dt>Message</dt> | ||||
| 	<dd>{{ build | message }}</dd> | ||||
| </dl> | ||||
|  | ||||
| <hr> | ||||
|  | ||||
| <dl> | ||||
| 	<dt>Task State</dt> | ||||
| 	<dd>{{ task.state }}</dd> | ||||
|  | ||||
| 	<dt>Started</dt> | ||||
| 	<dd>{{ task.started_at | fromNow }}</dd> | ||||
|  | ||||
| 	<dt>Finished</dt> | ||||
| 	<dd>{{ task.finished_at | fromNow }}</dd> | ||||
|  | ||||
| 	<dt>Duration</dt> | ||||
| 	<dd>{{ task.duration | toDuration }}</dd> | ||||
|  | ||||
| 	<dt>Exit Code</dt> | ||||
| 	<dd>{{ task.exit_code }}</dd> | ||||
|  | ||||
| 	<dt>Matrix</dt> | ||||
| 	<dd>{{ task.environment }}</dd> | ||||
| </dl> | ||||
|  | ||||
| <hr> | ||||
|  | ||||
| <pre>{{ logs }}</pre> | ||||
|  | ||||
| <table border="1"> | ||||
| 	<thead> | ||||
| 		<tr> | ||||
| 			<th>Number</th> | ||||
| 			<th>Status</th> | ||||
| 			<th>Started</th> | ||||
| 			<th>Finished</th> | ||||
| 			<th>Duration</th> | ||||
| 			<th>Exit Code</th> | ||||
| 			<th>Matrix</th> | ||||
| 		</tr> | ||||
| 	</thead> | ||||
| 	<tbody> | ||||
| 		<tr ng-repeat="task in tasks"> | ||||
| 			<td><a ng-href="{{ repo.full_name }}/{{ build.number }}/{{ task.number }}">{{ task.number }}</a></td> | ||||
| 			<td>{{ task.state }}</td> | ||||
| 			<td>{{ task.started_at | fromNow }}</td> | ||||
| 			<td>{{ task.finished_at | fromNow }}</td> | ||||
| 			<td>{{ task.duration | toDuration }}</td> | ||||
| 			<td>{{ task.exit_code }}</td> | ||||
| 			<td>{{ task.environment }}</td> | ||||
| 		</tr> | ||||
| 	</tbody> | ||||
| </table> | ||||
							
								
								
									
										36
									
								
								server/static/scripts/views/builds.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								server/static/scripts/views/builds.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| <h1>{{ repo.full_name }}</h1> | ||||
|  | ||||
| <a href="/">Back</a> | ||||
| <a ng-href="/{{ repo.full_name }}/edit">Settings</a> | ||||
|  | ||||
| <button ng-click="watch(repo)" ng-if="!repo.subscription || !repo.subscription.subscribed">Watch</button> | ||||
| <button ng-click="unwatch(repo)" ng-if="repo.subscription && repo.subscription.subscribed">Unwatch</button> | ||||
|  | ||||
| <table border="1"> | ||||
| 	<thead> | ||||
| 		<tr> | ||||
| 			<th>Number</th> | ||||
| 			<th>Status</th> | ||||
| 			<th>Started</th> | ||||
| 			<th>Duration</th> | ||||
| 			<th>Type</th> | ||||
| 			<th>Ref</th> | ||||
| 			<th>Commit</th> | ||||
| 			<th>Author</th> | ||||
| 			<th>Message</th> | ||||
| 		</tr> | ||||
| 	</thead> | ||||
| 	<tbody> | ||||
| 		<tr ng-repeat="build in builds"> | ||||
| 			<td><a ng-href="/{{ repo.full_name }}/{{ build.number }}">{{ build.number }}</a></td> | ||||
| 			<td>{{ build.state }}</td> | ||||
| 			<td>{{ build.started_at | fromNow }}</td> | ||||
| 			<td>{{ build.duration | toDuration }}</td> | ||||
| 			<td>{{ build.head_commit ? "push" : "pull request" }}</td> | ||||
| 			<td>{{ build | ref }}</td> | ||||
| 			<td>{{ build | sha }}</td> | ||||
| 			<td>{{ build | author }}</td> | ||||
| 			<td>{{ build | message }}</td> | ||||
| 		</tr> | ||||
| 	</tbody> | ||||
| </table> | ||||
							
								
								
									
										3
									
								
								server/static/scripts/views/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								server/static/scripts/views/login.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| <h1>Login</h1> | ||||
|  | ||||
| <a href="/authorize" target="_self">Login</a> | ||||
							
								
								
									
										30
									
								
								server/static/scripts/views/repos.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								server/static/scripts/views/repos.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <h1>Dashboard</h1> | ||||
|  | ||||
| <a href="/new">New</a> | ||||
| <a href="/profile">Settings</a> | ||||
| <a href="/users" ng-if="user.admin">User Management</a> | ||||
|  | ||||
| <table border="1"> | ||||
| 	<thead> | ||||
| 		<tr> | ||||
| 			<th>Repo</th> | ||||
| 			<th>Status</th> | ||||
| 			<th>Number</th> | ||||
| 			<th>Started</th> | ||||
| 			<th>Duration</th> | ||||
| 			<th>Branch</th> | ||||
| 			<th>Commit</th> | ||||
| 		</tr> | ||||
| 	</thead> | ||||
| 	<tbody> | ||||
| 		<tr ng-repeat="repo in repos"> | ||||
| 			<td><a ng-href="{{ repo.full_name }}">{{ repo.full_name }}</a></td> | ||||
| 			<td>{{ repo.last_build.state }}</td> | ||||
| 			<td><a ng-href="{{ repo.full_name }}/{{ repo.last_build.number }}">{{ repo.last_build.number }}</a></td> | ||||
| 			<td>{{ repo.last_build.started_at | fromNow }}</td> | ||||
| 			<td>{{ repo.last_build.duration | toDuration }}</td> | ||||
| 			<td>{{ repo.last_build.head_commit.ref || repo.last_build.pull_request.source.ref }}</td> | ||||
| 			<td>{{ repo.last_build.head_commit.sha || repo.last_build.pull_request.source.ref }}</td> | ||||
| 		</tr> | ||||
| 	</tbody> | ||||
| </table> | ||||
							
								
								
									
										8
									
								
								server/static/scripts/views/repos_add.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								server/static/scripts/views/repos_add.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| <h1>Add Repository</h1> | ||||
|  | ||||
| <a href="/">Back</a> | ||||
|  | ||||
| <form> | ||||
| 	<input type="text" placeholder="octocat/Hello-World" ng-model="slug" /> | ||||
| 	<button ng-click="add(slug)">Add</button> | ||||
| </form> | ||||
							
								
								
									
										42
									
								
								server/static/scripts/views/repos_edit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								server/static/scripts/views/repos_edit.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| <h1>{{ repo.full_name }} / Edit</h1> | ||||
|  | ||||
| <a ng-href="/{{ repo.full_name }}">Back</a> | ||||
|  | ||||
| <form> | ||||
| 	<div> | ||||
| 		<label>Disable Pushes</label> | ||||
| 		<input type="checkbox" ng-model="repo.disabled" /> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<label>Disable PRs</label> | ||||
| 		<input type="checkbox" ng-model="repo.disabled_prs" /> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<label>Disable Tags</label> | ||||
| 		<input type="checkbox" ng-model="repo.disabled_tags" /> | ||||
| 	</div> | ||||
|  | ||||
| 	<div> | ||||
| 		<label>Trusted</label> | ||||
| 		<input type="checkbox" ng-model="repo.trusted" /> | ||||
| 	</div> | ||||
| 	<div> | ||||
| 		<label>Timeout</label> | ||||
| 		<input type="number" ng-model="repo.timeout" /> | ||||
| 	</div> | ||||
| 	<ul> | ||||
| 		<li ng-repeat="(key, value) in repo.params"> | ||||
| 			<label>{{ key }}</label> | ||||
| 			<input type="text" ng-model="repo.params[key]"/> | ||||
| 			<button ng-click="deleteParam(key)">remove</button> | ||||
| 		</li> | ||||
| 		<li> | ||||
| 			<input type="text" ng-model="param.key" /> | ||||
| 			<input type="text" ng-model="param.value" /> | ||||
| 			<button ng-click="addParam(param)">add</button> | ||||
| 		</li> | ||||
| 	</ul> | ||||
|  | ||||
| 	<button ng-click="save(repo)">Save</button> | ||||
| 	<button ng-click="delete(repo.full_name)">Delete</button> | ||||
| </form> | ||||
							
								
								
									
										26
									
								
								server/static/scripts/views/user.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								server/static/scripts/views/user.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| <h1>{{ user.login }}</h1> | ||||
|  | ||||
| <a href="/">Back</a> | ||||
|  | ||||
| <dl> | ||||
| 	<dt>Login</dt> | ||||
| 	<dd>{{ user.login }}</dd> | ||||
|  | ||||
| 	<dt>Full Name</dt> | ||||
| 	<dd>{{ user.name }}</dd> | ||||
|  | ||||
| 	<dt>Created</dt> | ||||
| 	<dd>{{ user.created_at | fromNow }}</dd> | ||||
|  | ||||
| 	<dt>Updated</dt> | ||||
| 	<dd>{{ user.update_at | fromNow }}</dd> | ||||
|  | ||||
| 	<dt>Email</dt> | ||||
| 	<dd>{{ user.email }}</dd> | ||||
|  | ||||
| 	<dt>Site Admin</dt> | ||||
| 	<dd>{{ user.admin }}</dd> | ||||
|  | ||||
| 	<dt>Gravatar</dt> | ||||
| 	<dd><img ng-src="{{ user.gravatar_id | gravatar }}"</dd> | ||||
| </dl> | ||||
							
								
								
									
										37
									
								
								server/static/scripts/views/users.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								server/static/scripts/views/users.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| <h1>Users</h1> | ||||
|  | ||||
| <a href="/">Back</a> | ||||
|  | ||||
| <table border="1"> | ||||
| 	<thead> | ||||
| 		<tr> | ||||
| 			<th>Login</th> | ||||
| 			<th>Full Name</th> | ||||
| 			<th>Created</th> | ||||
| 			<th>Updated</th> | ||||
| 			<th>Email</th> | ||||
| 			<th>Site Admin</th> | ||||
| 			<th>Gravatar</th> | ||||
| 			<th></th> | ||||
| 			<th></th> | ||||
| 		</tr> | ||||
| 	</thead> | ||||
| 	<tbody> | ||||
| 		<tr ng-repeat="user in users"> | ||||
| 			<td>{{ user.login }}</td> | ||||
| 			<td>{{ user.name }}</td> | ||||
| 			<td>{{ user.created_at | fromNow }}</td> | ||||
| 			<td>{{ user.updated_at | fromNow }}</td> | ||||
| 			<td>{{ user.email }}</td> | ||||
| 			<td>{{ !!user.admin }}</td> | ||||
| 			<td><img ng-src="{{ user.gravatar_id | gravatar }}" /></td> | ||||
| 			<td><button ng-click="toggle(user)">toggle admin</button></td> | ||||
| 			<td><button ng-click="remove(user)">delete</button></td> | ||||
| 		</tr> | ||||
| 	</tbody> | ||||
| </table> | ||||
|  | ||||
| <form> | ||||
| 	<input type="text" ng-model="login" /> | ||||
| 	<button ng-click="add(login)">Add</button> | ||||
| </form> | ||||
							
								
								
									
										75
									
								
								server/status.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								server/status.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"strconv" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/gin-gonic/gin/binding" | ||||
|  | ||||
| 	"github.com/drone/drone/common" | ||||
| ) | ||||
|  | ||||
| // GetStatus accepts a request to retrieve a build status | ||||
| // from the datastore for the given repository and | ||||
| // build number. | ||||
| // | ||||
| //     GET /api/status/:owner/:name/:number/:context | ||||
| // | ||||
| func GetStatus(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	num, _ := strconv.Atoi(c.Params.ByName("number")) | ||||
| 	ctx := c.Params.ByName("context") | ||||
|  | ||||
| 	status, err := ds.GetBuildStatus(repo.FullName, num, ctx) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, status) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // PostStatus accepts a request to create a new build | ||||
| // status. The created user status is returned in JSON | ||||
| // format if successful. | ||||
| // | ||||
| //     POST /api/status/:owner/:name/:number | ||||
| // | ||||
| func PostStatus(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	num, err := strconv.Atoi(c.Params.ByName("number")) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
| 	in := &common.Status{} | ||||
| 	if !c.BindWith(in, binding.JSON) { | ||||
| 		c.AbortWithStatus(400) | ||||
| 		return | ||||
| 	} | ||||
| 	if err := ds.InsertBuildStatus(repo.Name, num, in); err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} else { | ||||
| 		c.JSON(201, in) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetStatusList accepts a request to retrieve a list of | ||||
| // all build status from the datastore for the given repository | ||||
| // and build number. | ||||
| // | ||||
| //     GET /api/status/:owner/:name/:number/:context | ||||
| // | ||||
| func GetStatusList(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	num, _ := strconv.Atoi(c.Params.ByName("number")) | ||||
|  | ||||
| 	list, err := ds.GetBuildStatusList(repo.FullName, num) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, list) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										84
									
								
								server/subscribe.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								server/subscribe.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
|  | ||||
| 	"github.com/drone/drone/common" | ||||
| ) | ||||
|  | ||||
| // GetSubscriber accepts a request to retrieve a repository | ||||
| // subscriber from the datastore for the given repository by | ||||
| // user Login. | ||||
| // | ||||
| //     GET /api/subscribers/:owner/:name/:login | ||||
| // | ||||
| func GetSubscriber(c *gin.Context) { | ||||
| 	store := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	login := c.Params.ByName("login") | ||||
| 	subsc, err := store.GetSubscriber(repo.FullName, login) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, subsc) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetSubscribers accepts a request to retrieve a repository | ||||
| // watchers from the datastore for the given repository. | ||||
| // | ||||
| //     GET /api/subscribers/:owner/:name | ||||
| // | ||||
| func GetSubscribers(c *gin.Context) { | ||||
| 	// store := ToDatastore(c) | ||||
| 	// repo := ToRepo(c) | ||||
| 	// subs, err := store.GetSubscribers(repo.FullName) | ||||
| 	// if err != nil { | ||||
| 	// 	c.Fail(404, err) | ||||
| 	// } else { | ||||
| 	// 	c.JSON(200, subs) | ||||
| 	// } | ||||
| 	c.Writer.WriteHeader(501) | ||||
| } | ||||
|  | ||||
| // Unubscribe accapets a request to unsubscribe the | ||||
| // currently authenticated user to the repository. | ||||
| // | ||||
| //     DEL /api/subscribers/:owner/:name | ||||
| // | ||||
| func Unsubscribe(c *gin.Context) { | ||||
| 	store := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	user := ToUser(c) | ||||
| 	sub, err := store.GetSubscriber(repo.FullName, user.Login) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} | ||||
| 	err = store.DeleteSubscriber(repo.FullName, sub) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} else { | ||||
| 		c.Writer.WriteHeader(200) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Subscribe accapets a request to subscribe the | ||||
| // currently authenticated user to the repository. | ||||
| // | ||||
| //     POST /api/subscriber/:owner/:name | ||||
| // | ||||
| func Subscribe(c *gin.Context) { | ||||
| 	store := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	user := ToUser(c) | ||||
| 	subscriber := &common.Subscriber{ | ||||
| 		Login:      user.Login, | ||||
| 		Subscribed: true, | ||||
| 	} | ||||
| 	err := store.InsertSubscriber(repo.FullName, subscriber) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, subscriber) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										46
									
								
								server/tasks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								server/tasks.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"strconv" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // GetTask accepts a request to retrieve a build task | ||||
| // from the datastore for the given repository and | ||||
| // build number. | ||||
| // | ||||
| //     GET /api/tasks/:owner/:name/:number/:task | ||||
| // | ||||
| func GetTask(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	b, _ := strconv.Atoi(c.Params.ByName("number")) | ||||
| 	t, _ := strconv.Atoi(c.Params.ByName("task")) | ||||
|  | ||||
| 	task, err := ds.GetTask(repo.FullName, b, t) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, task) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetTasks accepts a request to retrieve a list of | ||||
| // build tasks from the datastore for the given repository | ||||
| // and build number. | ||||
| // | ||||
| //     GET /api/tasks/:owner/:name/:number | ||||
| // | ||||
| func GetTasks(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	repo := ToRepo(c) | ||||
| 	num, _ := strconv.Atoi(c.Params.ByName("number")) | ||||
|  | ||||
| 	tasks, err := ds.GetTaskList(repo.FullName, num) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, tasks) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										59
									
								
								server/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								server/user.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/gin-gonic/gin/binding" | ||||
|  | ||||
| 	"github.com/drone/drone/common" | ||||
| 	"github.com/drone/drone/common/gravatar" | ||||
| ) | ||||
|  | ||||
| // GetUserCurr accepts a request to retrieve the | ||||
| // currently authenticated user from the datastore | ||||
| // and return in JSON format. | ||||
| // | ||||
| //     GET /api/user | ||||
| // | ||||
| func GetUserCurr(c *gin.Context) { | ||||
| 	c.JSON(200, ToUser(c)) | ||||
| } | ||||
|  | ||||
| // PutUserCurr accepts a request to update the currently | ||||
| // authenticated User profile. | ||||
| // | ||||
| //     PUT /api/user | ||||
| // | ||||
| func PutUserCurr(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	me := ToUser(c) | ||||
|  | ||||
| 	in := &common.User{} | ||||
| 	if !c.BindWith(in, binding.JSON) { | ||||
| 		return | ||||
| 	} | ||||
| 	me.Email = in.Email | ||||
| 	me.Gravatar = gravatar.Generate(in.Email) | ||||
| 	err := ds.UpdateUser(me) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, me) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetUserRepos accepts a request to get the currently | ||||
| // authenticated user's repository list from the datastore, | ||||
| // encoded and returned in JSON format. | ||||
| // | ||||
| //     GET /api/user/repos | ||||
| // | ||||
| func GetUserRepos(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	me := ToUser(c) | ||||
| 	repos, err := ds.GetUserRepos(me.Login) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, &repos) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										125
									
								
								server/users.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								server/users.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/gin-gonic/gin/binding" | ||||
|  | ||||
| 	"github.com/drone/drone/common" | ||||
| 	"github.com/drone/drone/common/gravatar" | ||||
| ) | ||||
|  | ||||
| // GetUsers accepts a request to retrieve all users | ||||
| // from the datastore and return encoded in JSON format. | ||||
| // | ||||
| //     GET /api/users | ||||
| // | ||||
| func GetUsers(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	users, err := ds.GetUserList() | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, users) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // PostUser accepts a request to create a new user in the | ||||
| // system. The created user account is returned in JSON | ||||
| // format if successful. | ||||
| // | ||||
| //     POST /api/users | ||||
| // | ||||
| func PostUser(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	name := c.Params.ByName("name") | ||||
| 	user := &common.User{Login: name, Name: name} | ||||
| 	if err := ds.InsertUser(user); err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} else { | ||||
| 		c.JSON(201, user) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetUser accepts a request to retrieve a user by hostname | ||||
| // and login from the datastore and return encoded in JSON | ||||
| // format. | ||||
| // | ||||
| //     GET /api/users/:name | ||||
| // | ||||
| func GetUser(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	name := c.Params.ByName("name") | ||||
| 	user, err := ds.GetUser(name) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, user) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // PutUser accepts a request to update an existing user in | ||||
| // the system. The modified user account is returned in JSON | ||||
| // format if successful. | ||||
| // | ||||
| //     PUT /api/users/:name | ||||
| // | ||||
| func PutUser(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	me := ToUser(c) | ||||
| 	name := c.Params.ByName("name") | ||||
| 	user, err := ds.GetUser(name) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	in := &common.User{} | ||||
| 	if !c.BindWith(in, binding.JSON) { | ||||
| 		return | ||||
| 	} | ||||
| 	user.Email = in.Email | ||||
| 	user.Gravatar = gravatar.Generate(user.Email) | ||||
|  | ||||
| 	// an administrator must not be able to | ||||
| 	// downgrade her own account. | ||||
| 	if me.Login != user.Login { | ||||
| 		user.Admin = in.Admin | ||||
| 	} | ||||
|  | ||||
| 	err = ds.UpdateUser(user) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} else { | ||||
| 		c.JSON(200, user) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // DeleteUser accepts a request to delete the specified | ||||
| // user account from the system. A successful request will | ||||
| // respond with an OK 200 status. | ||||
| // | ||||
| //     DELETE /api/users/:name | ||||
| // | ||||
| func DeleteUser(c *gin.Context) { | ||||
| 	ds := ToDatastore(c) | ||||
| 	me := ToUser(c) | ||||
| 	name := c.Params.ByName("name") | ||||
| 	user, err := ds.GetUser(name) | ||||
| 	if err != nil { | ||||
| 		c.Fail(404, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// an administrator must not be able to | ||||
| 	// delete her own account. | ||||
| 	if user.Login == me.Login { | ||||
| 		c.Writer.WriteHeader(403) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err := ds.DeleteUser(user); err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 	} else { | ||||
| 		c.Writer.WriteHeader(204) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										92
									
								
								server/ws.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								server/ws.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/drone/drone/eventbus" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/gorilla/websocket" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// Time allowed to write the message to the client. | ||||
| 	writeWait = 10 * time.Second | ||||
|  | ||||
| 	// Time allowed to read the next pong message from the client. | ||||
| 	pongWait = 60 * time.Second | ||||
|  | ||||
| 	// Send pings to client with this period. Must be less than pongWait. | ||||
| 	pingPeriod = (pongWait * 9) / 10 | ||||
| ) | ||||
|  | ||||
| var upgrader = websocket.Upgrader{ | ||||
| 	ReadBufferSize:  1024, | ||||
| 	WriteBufferSize: 1024, | ||||
| } | ||||
|  | ||||
| // GetEvents will upgrade the connection to a Websocket and will stream | ||||
| // event updates to the browser. | ||||
| func GetEvents(c *gin.Context) { | ||||
| 	bus := ToBus(c) | ||||
| 	user := ToUser(c) | ||||
| 	remote := ToRemote(c) | ||||
|  | ||||
| 	// upgrade the websocket | ||||
| 	ws, err := upgrader.Upgrade(c.Writer, c.Request, nil) | ||||
| 	if err != nil { | ||||
| 		c.Fail(400, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ticker := time.NewTicker(pingPeriod) | ||||
| 	eventc := make(chan *eventbus.Event, 1) | ||||
| 	bus.Subscribe(eventc) | ||||
| 	defer func() { | ||||
| 		bus.Unsubscribe(eventc) | ||||
| 		ticker.Stop() | ||||
| 		ws.Close() | ||||
| 		close(eventc) | ||||
| 	}() | ||||
|  | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			select { | ||||
| 			case event := <-eventc: | ||||
| 				if event == nil { | ||||
| 					return // why would this ever happen? | ||||
| 				} | ||||
| 				perms := perms(remote, user, event.Repo) | ||||
| 				if perms != nil && perms.Pull { | ||||
| 					ws.WriteJSON(event) | ||||
| 				} | ||||
| 			case <-ticker.C: | ||||
| 				ws.SetWriteDeadline(time.Now().Add(writeWait)) | ||||
| 				err := ws.WriteMessage(websocket.PingMessage, []byte{}) | ||||
| 				if err != nil { | ||||
| 					ws.Close() | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	readWebsocket(ws) | ||||
| } | ||||
|  | ||||
| // readWebsocket will block while reading the websocket data | ||||
| func readWebsocket(ws *websocket.Conn) { | ||||
| 	defer ws.Close() | ||||
| 	ws.SetReadLimit(512) | ||||
| 	ws.SetReadDeadline(time.Now().Add(pongWait)) | ||||
| 	ws.SetPongHandler(func(string) error { | ||||
| 		ws.SetReadDeadline(time.Now().Add(pongWait)) | ||||
| 		return nil | ||||
| 	}) | ||||
| 	for { | ||||
| 		_, _, err := ws.ReadMessage() | ||||
| 		if err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user