mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-01-22 15:18:43 +00:00
added handlers, rest, angular skeleton
This commit is contained in:
parent
09bd7cf71a
commit
9298f16155
37 changed files with 2625 additions and 4 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue