diff --git a/api/git.go b/api/git.go
index cd7a379..db8da4e 100644
--- a/api/git.go
+++ b/api/git.go
@@ -16,13 +16,6 @@ import (
func (api *WebAPI) ReceiveGitWebHook(r *Request) {
- if !signatureValid(r) {
- logrus.Error("WebHook signature does not match!")
- r.Ctx.SetStatusCode(403)
- _, _ = fmt.Fprintf(r.Ctx, "Signature does not match")
- return
- }
-
payload := &GitPayload{}
err := json.Unmarshal(r.Ctx.Request.Body(), payload)
if err != nil {
@@ -35,11 +28,27 @@ func (api *WebAPI) ReceiveGitWebHook(r *Request) {
}).Info("Received git WebHook")
if !isProductionBranch(payload) {
+ r.Ctx.SetStatusCode(400)
return
}
project := api.getAssociatedProject(payload)
if project == nil {
+ r.Ctx.SetStatusCode(400)
+ return
+ }
+
+ signature, err := api.Database.GetWebhookSecret(project.Id)
+ if err != nil {
+ _, _ = fmt.Fprintf(r.Ctx, err.Error())
+ r.Ctx.SetStatusCode(400)
+ return
+ }
+
+ if !signatureValid(r, signature) {
+ logrus.Error("WebHook signature does not match!")
+ r.Ctx.SetStatusCode(403)
+ _, _ = fmt.Fprintf(r.Ctx, "Signature does not match")
return
}
@@ -50,7 +59,7 @@ func (api *WebAPI) ReceiveGitWebHook(r *Request) {
handleErr(err, r)
}
-func signatureValid(r *Request) (matches bool) {
+func signatureValid(r *Request, webhookSignature string) (matches bool) {
signature := parseSignatureFromRequest(r.Ctx)
@@ -60,7 +69,7 @@ func signatureValid(r *Request) (matches bool) {
body := r.Ctx.PostBody()
- mac := hmac.New(getHashFuncFromConfig(), config.Cfg.WebHookSecret)
+ mac := hmac.New(getHashFuncFromConfig(), []byte(webhookSignature))
mac.Write(body)
expectedMac := hex.EncodeToString(mac.Sum(nil))
diff --git a/api/main.go b/api/main.go
index d5b3ff8..3008e46 100644
--- a/api/main.go
+++ b/api/main.go
@@ -93,6 +93,8 @@ func New() *WebAPI {
api.router.POST("/project/reject_request/:id/:wid", LogRequestMiddleware(api.RejectAccessRequest))
api.router.GET("/project/secret/:id", LogRequestMiddleware(api.GetSecret))
api.router.POST("/project/secret/:id", LogRequestMiddleware(api.SetSecret))
+ api.router.GET("/project/webhook_secret/:id", LogRequestMiddleware(api.GetWebhookSecret))
+ api.router.POST("/project/webhook_secret/:id", LogRequestMiddleware(api.SetWebhookSecret))
api.router.POST("/task/submit", LogRequestMiddleware(api.SubmitTask))
api.router.GET("/task/get/:project", LogRequestMiddleware(api.GetTaskFromProject))
diff --git a/api/models.go b/api/models.go
index d86bb9b..c4f8b23 100644
--- a/api/models.go
+++ b/api/models.go
@@ -292,3 +292,10 @@ type SetSecretRequest struct {
type GetSecretResponse struct {
Secret string `json:"secret"`
}
+
+type SetWebhookSecretRequest struct {
+ WebhookSecret string `json:"webhook_secret"`
+}
+type GetWebhookSecretResponse struct {
+ WebhookSecret string `json:"webhook_secret"`
+}
diff --git a/api/project.go b/api/project.go
index 56ef278..1b9552a 100644
--- a/api/project.go
+++ b/api/project.go
@@ -3,6 +3,7 @@ package api
import (
"encoding/json"
"github.com/Sirupsen/logrus"
+ "github.com/google/uuid"
"github.com/simon987/task_tracker/storage"
"strconv"
)
@@ -97,7 +98,9 @@ func (api *WebAPI) CreateProject(r *Request) {
return
}
- id, err := api.Database.SaveProject(project)
+ webhookSecret := makeWebhookSecret()
+
+ id, err := api.Database.SaveProject(project, webhookSecret)
if err != nil {
r.Json(JsonResponse{
Ok: false,
@@ -107,7 +110,7 @@ func (api *WebAPI) CreateProject(r *Request) {
}
api.Database.SetManagerRoleOn(manager.(*storage.Manager).Id, id,
- storage.ROLE_MANAGE_ACCESS|storage.ROLE_READ|storage.ROLE_EDIT)
+ storage.RoleManageAccess|storage.RoleRead|storage.RoleEdit|storage.RoleSecret)
r.OkJson(JsonResponse{
Ok: true,
Content: CreateProjectResponse{
@@ -119,6 +122,10 @@ func (api *WebAPI) CreateProject(r *Request) {
}).Debug("Created project")
}
+func makeWebhookSecret() string {
+ return uuid.New().String()
+}
+
func (api *WebAPI) UpdateProject(r *Request) {
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
@@ -163,7 +170,7 @@ func (api *WebAPI) UpdateProject(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
- if !isActionOnProjectAuthorized(project.Id, manager, storage.ROLE_EDIT, api.Database) {
+ if !isActionOnProjectAuthorized(project.Id, manager, storage.RoleEdit, api.Database) {
r.Json(JsonResponse{
Ok: false,
Message: "Unauthorized",
@@ -238,7 +245,7 @@ func isProjectReadAuthorized(project *storage.Project, manager interface{}, db *
return true
}
role := db.GetManagerRoleOn(manager.(*storage.Manager), project.Id)
- if role&storage.ROLE_READ == 1 {
+ if role&storage.RoleRead == 1 {
return true
}
@@ -302,7 +309,7 @@ func (api *WebAPI) GetWorkerAccessListForProject(r *Request) {
return
}
- if !isActionOnProjectAuthorized(id, manager, storage.ROLE_MANAGE_ACCESS, api.Database) {
+ if !isActionOnProjectAuthorized(id, manager, storage.RoleManageAccess, api.Database) {
r.Json(JsonResponse{
Ok: false,
Message: "Unauthorized",
@@ -391,7 +398,7 @@ func (api *WebAPI) AcceptAccessRequest(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
- if !isActionOnProjectAuthorized(pid, manager, storage.ROLE_MANAGE_ACCESS, api.Database) {
+ if !isActionOnProjectAuthorized(pid, manager, storage.RoleManageAccess, api.Database) {
r.Json(JsonResponse{
Message: "Unauthorized",
Ok: false,
@@ -471,7 +478,7 @@ func (api *WebAPI) SetManagerRoleOnProject(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
- if !isActionOnProjectAuthorized(pid, manager, storage.ROLE_MANAGE_ACCESS, api.Database) {
+ if !isActionOnProjectAuthorized(pid, manager, storage.RoleManageAccess, api.Database) {
r.Json(JsonResponse{
Message: "Unauthorized",
Ok: false,
@@ -500,7 +507,7 @@ func (api *WebAPI) SetSecret(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
- if !isActionOnProjectAuthorized(pid, manager, storage.ROLE_EDIT, api.Database) {
+ if !isActionOnProjectAuthorized(pid, manager, storage.RoleSecret, api.Database) {
r.Json(JsonResponse{
Ok: false,
Message: "Unauthorized",
@@ -560,7 +567,7 @@ func (api *WebAPI) GetSecret(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
- if !isActionOnProjectAuthorized(pid, manager, storage.ROLE_EDIT, api.Database) {
+ if !isActionOnProjectAuthorized(pid, manager, storage.RoleSecret, api.Database) {
r.Json(JsonResponse{
Ok: false,
Message: "Unauthorized",
@@ -576,3 +583,79 @@ func (api *WebAPI) GetSecret(r *Request) {
},
})
}
+
+func (api *WebAPI) GetWebhookSecret(r *Request) {
+
+ pid, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
+ if err != nil || pid <= 0 {
+ r.Json(JsonResponse{
+ Ok: false,
+ Message: "Invalid project id",
+ }, 400)
+ return
+ }
+
+ sess := api.Session.StartFasthttp(r.Ctx)
+ manager := sess.Get("manager")
+
+ if !isActionOnProjectAuthorized(pid, manager, storage.RoleSecret, api.Database) {
+ r.Json(JsonResponse{
+ Ok: false,
+ Message: "Unauthorized",
+ }, 403)
+ return
+ }
+
+ secret, err := api.Database.GetWebhookSecret(pid)
+ r.OkJson(JsonResponse{
+ Ok: true,
+ Content: GetWebhookSecretResponse{
+ WebhookSecret: secret,
+ },
+ })
+}
+
+func (api *WebAPI) SetWebhookSecret(r *Request) {
+
+ pid, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
+ if err != nil || pid <= 0 {
+ r.Json(JsonResponse{
+ Ok: false,
+ Message: "Invalid project id",
+ }, 400)
+ return
+ }
+
+ req := &SetWebhookSecretRequest{}
+ err = json.Unmarshal(r.Ctx.Request.Body(), req)
+ if err != nil {
+ r.Json(JsonResponse{
+ Ok: false,
+ Message: "Could not parse request",
+ }, 400)
+ return
+ }
+
+ sess := api.Session.StartFasthttp(r.Ctx)
+ manager := sess.Get("manager")
+
+ if !isActionOnProjectAuthorized(pid, manager, storage.RoleSecret, api.Database) {
+ r.Json(JsonResponse{
+ Ok: false,
+ Message: "Unauthorized",
+ }, 403)
+ return
+ }
+
+ err = api.Database.SetWebhookSecret(pid, req.WebhookSecret)
+ if err == nil {
+ r.OkJson(JsonResponse{
+ Ok: true,
+ })
+ } else {
+ r.OkJson(JsonResponse{
+ Ok: false,
+ Message: err.Error(),
+ })
+ }
+}
diff --git a/config.yml b/config.yml
index d24d2d0..30f6512 100755
--- a/config.yml
+++ b/config.yml
@@ -7,7 +7,6 @@ database:
log_levels: ["error", "info", "warn"]
git:
- webhook_secret: "very_secret_secret"
# Github: sha1, Gogs: sha256
webhook_hash: "sha256"
# Github: 'X-Hub-Signature', Gogs: 'X-Gogs-Signature'
diff --git a/storage/auth.go b/storage/auth.go
index d195224..5c69e2f 100644
--- a/storage/auth.go
+++ b/storage/auth.go
@@ -11,10 +11,11 @@ import (
type ManagerRole int
const (
- ROLE_NONE ManagerRole = 0
- ROLE_READ ManagerRole = 1
- ROLE_EDIT ManagerRole = 2
- ROLE_MANAGE_ACCESS ManagerRole = 4
+ RoleNone ManagerRole = 0
+ RoleRead ManagerRole = 1
+ RoleEdit ManagerRole = 2
+ RoleManageAccess ManagerRole = 4
+ RoleSecret ManagerRole = 8
)
type Manager struct {
@@ -142,7 +143,7 @@ func (database *Database) GetManagerRoleOn(manager *Manager, projectId int64) Ma
var role ManagerRole
err := row.Scan(&role)
if err != nil {
- return ROLE_NONE
+ return RoleNone
}
return role
diff --git a/storage/project.go b/storage/project.go
index 49f16c3..dba5b8d 100644
--- a/storage/project.go
+++ b/storage/project.go
@@ -25,14 +25,14 @@ type AssignedTasks struct {
TaskCount int64 `json:"task_count"`
}
-func (database *Database) SaveProject(project *Project) (int64, error) {
+func (database *Database) SaveProject(project *Project, webhookSecret string) (int64, error) {
db := database.getDB()
row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority,
- motd, public, hidden, chain, paused)
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULLIF($9, 0),$10) RETURNING id`,
+ motd, public, hidden, chain, paused, webhook_secret)
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULLIF($9, 0),$10,$11) RETURNING id`,
project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd,
- project.Public, project.Hidden, project.Chain, project.Paused)
+ project.Public, project.Hidden, project.Chain, project.Paused, webhookSecret)
var id int64
err := row.Scan(&id)
@@ -225,3 +225,23 @@ func (database *Database) SetSecret(pid int64, secret string) {
"project": pid,
}).Info("Set secret")
}
+
+func (database *Database) GetWebhookSecret(pid int64) (secret string, err error) {
+ db := database.getDB()
+ row := db.QueryRow(`SELECT webhook_secret FROM project WHERE id=$1`, pid)
+ err = row.Scan(&secret)
+ return
+}
+
+func (database *Database) SetWebhookSecret(pid int64, secret string) (err error) {
+ db := database.getDB()
+ res, err := db.Exec(`UPDATE project SET webhook_secret=$1 WHERE id=$2`, secret, pid)
+ handleErr(err)
+
+ rowsAffected, _ := res.RowsAffected()
+ logrus.WithFields(logrus.Fields{
+ "project": pid,
+ "rowsAffected": rowsAffected,
+ }).Trace("Update webhook secret")
+ return
+}
diff --git a/test/api_auth_test.go b/test/api_auth_test.go
index fc371ce..ed1b201 100644
--- a/test/api_auth_test.go
+++ b/test/api_auth_test.go
@@ -3,6 +3,7 @@ package test
import (
"bytes"
"encoding/json"
+ "fmt"
"github.com/simon987/task_tracker/api"
"github.com/simon987/task_tracker/config"
"golang.org/x/net/publicsuffix"
@@ -177,3 +178,15 @@ func getSessionCtx(username string, password string, admin bool) *http.Client {
return client
}
+
+func setRoleOnProject(req api.SetManagerRoleOnProjectRequest, pid int64, s *http.Client) (ar api.JsonResponse) {
+ r := Post(fmt.Sprintf("/manager/set_role_for_project/%d", pid), req, nil, s)
+ UnmarshalResponse(r, &ar)
+ return
+}
+
+func getAccountDetails(s *http.Client) (ar AccountAR) {
+ r := Get("/account", nil, s)
+ UnmarshalResponse(r, &ar)
+ return
+}
diff --git a/test/api_git_test.go b/test/api_git_test.go
index 8291668..0123e5d 100644
--- a/test/api_git_test.go
+++ b/test/api_git_test.go
@@ -15,7 +15,7 @@ func TestWebHookNoSignature(t *testing.T) {
r := Post("/git/receivehook", api.GitPayload{}, nil, nil)
- if r.StatusCode != 403 {
+ if r.StatusCode == 200 {
t.Error()
}
}
@@ -28,7 +28,7 @@ func TestWebHookInvalidSignature(t *testing.T) {
client := http.Client{}
r, _ := client.Do(req)
- if r.StatusCode != 403 {
+ if r.StatusCode == 200 {
t.Error()
}
}
@@ -41,10 +41,12 @@ func TestWebHookDontUpdateVersion(t *testing.T) {
GitRepo: "username/not_this_one",
}).Content
+ webhookSecret := getWebhookSecret(resp.Id, testAdminCtx).Content.WebhookSecret
+
body := []byte(`{"ref": "refs/heads/master", "after": "new", "repository": {"full_name": "username/repo_name"}}`)
bodyReader := bytes.NewReader(body)
- mac := hmac.New(crypto.SHA1.New, config.Cfg.WebHookSecret)
+ mac := hmac.New(crypto.SHA1.New, []byte(webhookSecret))
mac.Write(body)
signature := hex.EncodeToString(mac.Sum(nil))
signature = "sha1=" + signature
@@ -53,11 +55,7 @@ func TestWebHookDontUpdateVersion(t *testing.T) {
req.Header.Add("X-Hub-Signature", signature)
client := http.Client{}
- r, _ := client.Do(req)
-
- if r.StatusCode != 200 {
- t.Error()
- }
+ _, _ = client.Do(req)
getResp := getProjectAsAdmin(resp.Id).Content
@@ -76,7 +74,9 @@ func TestWebHookUpdateVersion(t *testing.T) {
body := []byte(`{"ref": "refs/heads/master", "after": "new", "repository": {"full_name": "username/repo_name"}}`)
bodyReader := bytes.NewReader(body)
- mac := hmac.New(crypto.SHA1.New, config.Cfg.WebHookSecret)
+ webhookSecret := getWebhookSecret(resp.Id, testAdminCtx).Content.WebhookSecret
+
+ mac := hmac.New(crypto.SHA1.New, []byte(webhookSecret))
mac.Write(body)
signature := hex.EncodeToString(mac.Sum(nil))
signature = "sha1=" + signature
diff --git a/test/api_project_test.go b/test/api_project_test.go
index c28c258..0cc02c6 100644
--- a/test/api_project_test.go
+++ b/test/api_project_test.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/simon987/task_tracker/api"
+ "github.com/simon987/task_tracker/storage"
"io/ioutil"
"net/http"
"testing"
@@ -465,6 +466,82 @@ func TestPausedProjectShouldNotDispatchTasks(t *testing.T) {
}, testProject, testAdminCtx)
}
+func TestGetWebhookSecret(t *testing.T) {
+
+ resp := getWebhookSecret(testProject, testAdminCtx)
+
+ if resp.Ok != true {
+ t.Error()
+ }
+ if len(resp.Content.WebhookSecret) <= 0 {
+ t.Error()
+ }
+}
+
+func TestSetWebhookSecret(t *testing.T) {
+
+ resp1 := setWebhookSecret(api.SetWebhookSecretRequest{
+ WebhookSecret: "new",
+ }, testProject, testAdminCtx)
+
+ if resp1.Ok != true {
+ t.Error()
+ }
+
+ resp := getWebhookSecret(testProject, testAdminCtx)
+
+ if resp.Ok != true {
+ t.Error()
+ }
+ if resp.Content.WebhookSecret != "new" {
+ t.Error()
+ }
+}
+
+func TestGetWebhookRequiresRole(t *testing.T) {
+
+ otherUser := getSessionCtx("testwebhookrole", "testwebhookrole", false)
+ otherUserId := getAccountDetails(otherUser).Content.Id
+
+ user := getSessionCtx("testwebhookroleu", "testwebhookroleu", false)
+ userId := getAccountDetails(user).Content.Id
+
+ resp := setRoleOnProject(api.SetManagerRoleOnProjectRequest{
+ Role: storage.RoleEdit | storage.RoleManageAccess | storage.RoleRead,
+ Manager: otherUserId,
+ }, testProject, testAdminCtx)
+ if resp.Ok != true {
+ t.Fail()
+ }
+ resp = setRoleOnProject(api.SetManagerRoleOnProjectRequest{
+ Role: storage.RoleSecret,
+ Manager: userId,
+ }, testProject, testAdminCtx)
+ if resp.Ok != true {
+ t.Fail()
+ }
+
+ rUser := setWebhookSecret(api.SetWebhookSecretRequest{
+ WebhookSecret: "test",
+ }, testProject, user)
+ rOther := setWebhookSecret(api.SetWebhookSecretRequest{
+ WebhookSecret: "test",
+ }, testProject, otherUser)
+ rGuest := setWebhookSecret(api.SetWebhookSecretRequest{
+ WebhookSecret: "test",
+ }, testProject, nil)
+
+ if rUser.Ok != true {
+ t.Error()
+ }
+ if rOther.Ok != false {
+ t.Error()
+ }
+ if rGuest.Ok != false {
+ t.Error()
+ }
+}
+
func createProjectAsAdmin(req api.CreateProjectRequest) CreateProjectAR {
return createProject(req, testAdminCtx)
}
@@ -502,3 +579,15 @@ func getProjectList(s *http.Client) (ar ProjectListAR) {
UnmarshalResponse(r, &ar)
return
}
+
+func getWebhookSecret(pid int64, s *http.Client) (ar WebhookSecretAR) {
+ r := Get(fmt.Sprintf("/project/webhook_secret/%d", pid), nil, s)
+ UnmarshalResponse(r, &ar)
+ return
+}
+
+func setWebhookSecret(req api.SetWebhookSecretRequest, pid int64, s *http.Client) (ar api.JsonResponse) {
+ r := Post(fmt.Sprintf("/project/webhook_secret/%d", pid), req, nil, s)
+ UnmarshalResponse(r, &ar)
+ return
+}
diff --git a/test/common.go b/test/common.go
index a19b6d8..8892f44 100644
--- a/test/common.go
+++ b/test/common.go
@@ -172,3 +172,19 @@ type ReleaseAR struct {
Updated bool `json:"updated"`
} `json:"content"`
}
+
+type WebhookSecretAR struct {
+ Ok bool `json:"ok"`
+ Message string `json:"message"`
+ Content struct {
+ WebhookSecret string `json:"webhook_secret"`
+ } `json:"content"`
+}
+
+type AccountAR struct {
+ Ok bool `json:"ok"`
+ Message string `json:"message"`
+ Content struct {
+ *storage.Manager `json:"manager"`
+ } `json:"content"`
+}
diff --git a/test/config.yml b/test/config.yml
index d4efd92..4fbfaff 100644
--- a/test/config.yml
+++ b/test/config.yml
@@ -6,7 +6,6 @@ database:
log_levels: ["debug", "error", "trace", "info", "warn"]
git:
- webhook_secret: "very_secret_secret"
webhook_hash: "sha1"
webhook_sig_header: "X-Hub-Signature"
diff --git a/test/schema.sql b/test/schema.sql
index 878f063..b8cf304 100755
--- a/test/schema.sql
+++ b/test/schema.sql
@@ -25,7 +25,8 @@ CREATE TABLE project
git_repo TEXT NOT NULL,
version TEXT NOT NULL,
motd TEXT NOT NULL,
- secret TEXT NOT NULL DEFAULT '{}'
+ secret TEXT NOT NULL DEFAULT '{}',
+ webhook_secret TEXT NOT NULL
);
CREATE TABLE worker_access
diff --git a/web/angular/src/app/api.service.ts b/web/angular/src/app/api.service.ts
index 80c4f1a..df55562 100755
--- a/web/angular/src/app/api.service.ts
+++ b/web/angular/src/app/api.service.ts
@@ -106,4 +106,12 @@ export class ApiService {
return this.http.post(this.url + `/project/secret/${pid}`, {"secret": secret})
}
+ getWebhookSecret(pid: number) {
+ return this.http.get(this.url + `/project/webhook_secret/${pid}`,)
+ }
+
+ setWebhookSecret(pid: number, secret: string) {
+ return this.http.post(this.url + `/project/webhook_secret/${pid}`, {"webhook_secret": secret})
+ }
+
}
diff --git a/web/angular/src/app/models/manager.ts b/web/angular/src/app/models/manager.ts
index ffa2240..94195dd 100644
--- a/web/angular/src/app/models/manager.ts
+++ b/web/angular/src/app/models/manager.ts
@@ -51,4 +51,16 @@ export class ManagerRoleOnProject {
this.role &= ~4
}
}
+
+ get secretRole(): boolean {
+ return (this.role & 8) != 0
+ }
+
+ set secretRole(role: boolean) {
+ if (role) {
+ this.role |= 8
+ } else {
+ this.role &= ~8
+ }
+ }
}
diff --git a/web/angular/src/app/project-dashboard/project-dashboard.component.ts b/web/angular/src/app/project-dashboard/project-dashboard.component.ts
index 0501d84..85b9306 100644
--- a/web/angular/src/app/project-dashboard/project-dashboard.component.ts
+++ b/web/angular/src/app/project-dashboard/project-dashboard.component.ts
@@ -366,13 +366,14 @@ export class ProjectDashboardComponent implements OnInit {
this.dialog.open(AreYouSureComponent, {
width: '250px',
}).afterClosed().subscribe(result => {
- if (result) {
- this.project.paused = paused;
- this.apiService.updateProject(this.project).subscribe(() => {
- this.translate.get("messenger.acknowledged").subscribe(t =>
- this.messenger.show(t))
- })
- }
+ this.project.paused = paused;
+ this.apiService.updateProject(this.project).subscribe(() => {
+ this.translate.get("messenger.acknowledged").subscribe(t =>
+ this.messenger.show(t))
+ }, error => {
+ this.translate.get("messenger.unauthorized").subscribe(t =>
+ this.messenger.show(t))
+ })
});
}
diff --git a/web/angular/src/app/project-list/project-list.component.html b/web/angular/src/app/project-list/project-list.component.html
index 270edfc..b530eaf 100755
--- a/web/angular/src/app/project-list/project-list.component.html
+++ b/web/angular/src/app/project-list/project-list.component.html
@@ -33,6 +33,7 @@
diff --git a/web/angular/src/app/project-perms/project-perms.component.html b/web/angular/src/app/project-perms/project-perms.component.html
index 4cb384b..d07b6ae 100644
--- a/web/angular/src/app/project-perms/project-perms.component.html
+++ b/web/angular/src/app/project-perms/project-perms.component.html
@@ -55,6 +55,10 @@
(change)="onRoleChange(m)"
[disabled]="m.manager.id==auth.account.id"
>{{"perms.manage"|translate}}
+