mirror of
https://github.com/simon987/task_tracker.git
synced 2025-04-19 18:16:45 +00:00
Some work on permissions (lacks tests)
This commit is contained in:
parent
4edf354f8d
commit
c3e5bd77f7
@ -145,7 +145,6 @@ func (api *WebAPI) AccountDetails(r *Request) {
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"manager": manager,
|
||||
"session": sess,
|
||||
}).Trace("Account details request")
|
||||
|
||||
if manager == nil {
|
||||
|
@ -90,6 +90,8 @@ func New() *WebAPI {
|
||||
api.router.GET("/project/monitoring-between/:id", LogRequestMiddleware(api.GetSnapshotsBetween))
|
||||
api.router.GET("/project/monitoring/:id", LogRequestMiddleware(api.GetNSnapshots))
|
||||
api.router.GET("/project/assignees/:id", LogRequestMiddleware(api.ProjectGetAssigneeStats))
|
||||
api.router.GET("/project/requests/:id", LogRequestMiddleware(api.ProjectGetAccessRequests))
|
||||
api.router.GET("/project/request_access/:id", LogRequestMiddleware(api.WorkerRequestAccess))
|
||||
|
||||
api.router.POST("/task/create", LogRequestMiddleware(api.TaskCreate))
|
||||
api.router.GET("/task/get/:project", LogRequestMiddleware(api.TaskGetFromProject))
|
||||
|
159
api/project.go
159
api/project.go
@ -15,6 +15,7 @@ type CreateProjectRequest struct {
|
||||
Priority int64 `json:"priority"`
|
||||
Motd string `json:"motd"`
|
||||
Public bool `json:"public"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
type UpdateProjectRequest struct {
|
||||
@ -24,6 +25,7 @@ type UpdateProjectRequest struct {
|
||||
Priority int64 `json:"priority"`
|
||||
Motd string `json:"motd"`
|
||||
Public bool `json:"public"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
type UpdateProjectResponse struct {
|
||||
@ -55,8 +57,22 @@ type GetAssigneeStatsResponse struct {
|
||||
Assignees *[]storage.AssignedTasks `json:"assignees"`
|
||||
}
|
||||
|
||||
type WorkerRequestAccessResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type ProjectGetAccessRequestsResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Requests *[]storage.Worker `json:"requests,omitempty"`
|
||||
}
|
||||
|
||||
func (api *WebAPI) ProjectCreate(r *Request) {
|
||||
|
||||
sess := api.Session.StartFasthttp(r.Ctx)
|
||||
manager := sess.Get("manager")
|
||||
|
||||
createReq := &CreateProjectRequest{}
|
||||
err := json.Unmarshal(r.Ctx.Request.Body(), createReq)
|
||||
if err != nil {
|
||||
@ -74,26 +90,10 @@ func (api *WebAPI) ProjectCreate(r *Request) {
|
||||
Priority: createReq.Priority,
|
||||
Motd: createReq.Motd,
|
||||
Public: createReq.Public,
|
||||
Hidden: createReq.Hidden,
|
||||
}
|
||||
|
||||
if isValidProject(project) {
|
||||
id, err := api.Database.SaveProject(project)
|
||||
|
||||
if err != nil {
|
||||
r.Json(CreateProjectResponse{
|
||||
Ok: false,
|
||||
Message: err.Error(),
|
||||
}, 500)
|
||||
} else {
|
||||
r.OkJson(CreateProjectResponse{
|
||||
Ok: true,
|
||||
Id: id,
|
||||
})
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"project": project,
|
||||
}).Debug("Created project")
|
||||
}
|
||||
} else {
|
||||
if !isValidProject(project) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"project": project,
|
||||
}).Warn("Invalid project")
|
||||
@ -102,7 +102,36 @@ func (api *WebAPI) ProjectCreate(r *Request) {
|
||||
Ok: false,
|
||||
Message: "Invalid project",
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
|
||||
if !isProjectCreationAuthorized(project, manager) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"project": project,
|
||||
}).Warn("Unauthorized project creation")
|
||||
|
||||
r.Json(CreateProjectResponse{
|
||||
Ok: false,
|
||||
Message: "You are not permitted to create a project with this configuration",
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := api.Database.SaveProject(project)
|
||||
if err != nil {
|
||||
r.Json(CreateProjectResponse{
|
||||
Ok: false,
|
||||
Message: err.Error(),
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
r.OkJson(CreateProjectResponse{
|
||||
Ok: true,
|
||||
Id: id,
|
||||
})
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"project": project,
|
||||
}).Debug("Created project")
|
||||
}
|
||||
|
||||
func (api *WebAPI) ProjectUpdate(r *Request) {
|
||||
@ -133,6 +162,7 @@ func (api *WebAPI) ProjectUpdate(r *Request) {
|
||||
Priority: updateReq.Priority,
|
||||
Motd: updateReq.Motd,
|
||||
Public: updateReq.Public,
|
||||
Hidden: updateReq.Hidden,
|
||||
}
|
||||
|
||||
if isValidProject(project) {
|
||||
@ -180,6 +210,38 @@ func isValidProject(project *storage.Project) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func isProjectCreationAuthorized(project *storage.Project, manager interface{}) bool {
|
||||
|
||||
return true
|
||||
if manager == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if project.Public && manager.(*storage.Manager).WebsiteAdmin {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isProjectUpdateAuthorized(project *storage.Project, manager interface{}, db *storage.Database) bool {
|
||||
|
||||
var man storage.Manager
|
||||
if manager != nil {
|
||||
man = manager.(storage.Manager)
|
||||
}
|
||||
|
||||
if man.WebsiteAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
role := db.ManagerHasRoleOn(&man, project.Id)
|
||||
if role&storage.ROLE_EDIT == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (api *WebAPI) ProjectGet(r *Request) {
|
||||
|
||||
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||
@ -230,3 +292,64 @@ func (api *WebAPI) ProjectGetAssigneeStats(r *Request) {
|
||||
Assignees: stats,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *WebAPI) ProjectGetAccessRequests(r *Request) {
|
||||
|
||||
sess := api.Session.StartFasthttp(r.Ctx)
|
||||
manager := sess.Get("manager")
|
||||
|
||||
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||
handleErr(err, r) //todo handle invalid id
|
||||
|
||||
if manager == nil {
|
||||
r.Json(ProjectGetAccessRequestsResponse{
|
||||
Ok: false,
|
||||
Message: "Unauthorized",
|
||||
}, 401)
|
||||
return
|
||||
}
|
||||
|
||||
if !manager.(*storage.Manager).WebsiteAdmin &&
|
||||
api.Database.ManagerHasRoleOn(manager.(*storage.Manager), 1)&
|
||||
storage.ROLE_MANAGE_ACCESS == 0 {
|
||||
r.Json(ProjectGetAccessRequestsResponse{
|
||||
Ok: false,
|
||||
Message: "Unauthorized",
|
||||
}, 403)
|
||||
return
|
||||
}
|
||||
requests := api.Database.GetAllAccessRequests(id)
|
||||
|
||||
r.OkJson(ProjectGetAccessRequestsResponse{
|
||||
Ok: true,
|
||||
Requests: requests,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *WebAPI) WorkerRequestAccess(r *Request) {
|
||||
|
||||
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||
handleErr(err, r) //todo handle invalid id
|
||||
|
||||
worker, err := api.validateSignature(r)
|
||||
if err != nil {
|
||||
r.Json(WorkerRequestAccessResponse{
|
||||
Ok: false,
|
||||
Message: err.Error(),
|
||||
}, 401)
|
||||
}
|
||||
|
||||
res := api.Database.SaveAccessRequest(worker, id)
|
||||
|
||||
if res {
|
||||
r.OkJson(WorkerRequestAccessResponse{
|
||||
Ok: true,
|
||||
})
|
||||
} else {
|
||||
r.Json(WorkerRequestAccessResponse{
|
||||
Ok: false,
|
||||
Message: "Project is public, you already have " +
|
||||
"an active request or you already have access to this project",
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
|
@ -177,6 +177,10 @@ func (api WebAPI) validateSignature(r *Request) (*storage.Worker, error) {
|
||||
widStr := string(r.Ctx.Request.Header.Peek("X-Worker-Id"))
|
||||
signature := r.Ctx.Request.Header.Peek("X-Signature")
|
||||
|
||||
if widStr == "" {
|
||||
return nil, errors.New("worker id not specified")
|
||||
}
|
||||
|
||||
wid, err := strconv.ParseInt(widStr, 10, 64)
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithFields(logrus.Fields{
|
||||
|
@ -58,11 +58,9 @@ func (api *WebAPI) WorkerCreate(r *Request) {
|
||||
return
|
||||
}
|
||||
|
||||
identity := getIdentity(r)
|
||||
if !canCreateWorker(r, workerReq, identity) {
|
||||
if !canCreateWorker(r, workerReq) {
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"identity": identity,
|
||||
"createWorkerRequest": workerReq,
|
||||
}).Warn("Failed CreateWorkerRequest")
|
||||
|
||||
@ -73,7 +71,7 @@ func (api *WebAPI) WorkerCreate(r *Request) {
|
||||
return
|
||||
}
|
||||
|
||||
worker, err := api.workerCreate(workerReq, getIdentity(r))
|
||||
worker, err := api.workerCreate(workerReq)
|
||||
if err != nil {
|
||||
handleErr(err, r)
|
||||
} else {
|
||||
@ -185,7 +183,7 @@ func (api *WebAPI) WorkerUpdate(r *Request) {
|
||||
r.Json(GetTaskResponse{
|
||||
Ok: false,
|
||||
Message: err.Error(),
|
||||
}, 403)
|
||||
}, 401)
|
||||
return
|
||||
}
|
||||
|
||||
@ -224,24 +222,23 @@ func (api *WebAPI) GetAllWorkerStats(r *Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (*storage.Worker, error) {
|
||||
func (api *WebAPI) workerCreate(request *CreateWorkerRequest) (*storage.Worker, error) {
|
||||
|
||||
if request.Alias == "" {
|
||||
request.Alias = "default_alias"
|
||||
}
|
||||
|
||||
worker := storage.Worker{
|
||||
Created: time.Now().Unix(),
|
||||
Identity: identity,
|
||||
Secret: makeSecret(),
|
||||
Alias: request.Alias,
|
||||
Created: time.Now().Unix(),
|
||||
Secret: makeSecret(),
|
||||
Alias: request.Alias,
|
||||
}
|
||||
|
||||
api.Database.SaveWorker(&worker)
|
||||
return &worker, nil
|
||||
}
|
||||
|
||||
func canCreateWorker(r *Request, cwr *CreateWorkerRequest, identity *storage.Identity) bool {
|
||||
func canCreateWorker(r *Request, cwr *CreateWorkerRequest) bool {
|
||||
|
||||
if cwr.Alias == "unassigned" {
|
||||
//Reserved alias
|
||||
@ -260,13 +257,3 @@ func makeSecret() []byte {
|
||||
|
||||
return secret
|
||||
}
|
||||
|
||||
func getIdentity(r *Request) *storage.Identity {
|
||||
|
||||
identity := storage.Identity{
|
||||
RemoteAddr: r.Ctx.RemoteAddr().String(),
|
||||
UserAgent: string(r.Ctx.UserAgent()),
|
||||
}
|
||||
|
||||
return &identity
|
||||
}
|
||||
|
106
schema.sql
106
schema.sql
@ -1,39 +1,30 @@
|
||||
DROP TABLE IF EXISTS worker_identity, worker, project, task, log_entry,
|
||||
DROP TABLE IF EXISTS worker, project, task, log_entry,
|
||||
worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring_snapshot,
|
||||
worker_verifies_task;
|
||||
worker_verifies_task, worker_requests_access_to_project;
|
||||
DROP TYPE IF EXISTS status;
|
||||
DROP TYPE IF EXISTS log_level;
|
||||
|
||||
CREATE TABLE worker_identity
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
remote_addr TEXT,
|
||||
user_agent TEXT,
|
||||
|
||||
UNIQUE (remote_addr)
|
||||
);
|
||||
|
||||
CREATE TABLE worker
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
alias TEXT,
|
||||
created INTEGER,
|
||||
identity INTEGER REFERENCES worker_identity (id),
|
||||
secret BYTEA,
|
||||
closed_task_count INTEGER DEFAULT 0
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
alias TEXT NOT NULL,
|
||||
created INTEGER NOT NULL,
|
||||
secret BYTEA NOT NULL,
|
||||
closed_task_count INTEGER DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE project
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
priority INTEGER DEFAULT 0,
|
||||
name TEXT UNIQUE,
|
||||
clone_url TEXT,
|
||||
git_repo TEXT UNIQUE,
|
||||
version TEXT,
|
||||
motd TEXT,
|
||||
public boolean,
|
||||
closed_task_count INT DEFAULT 0
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
priority INTEGER DEFAULT 0 NOT NULL,
|
||||
closed_task_count INT DEFAULT 0 NOT NULL,
|
||||
public boolean NOT NULL,
|
||||
hidden boolean NOT NULL,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
clone_url TEXT NOT NULL,
|
||||
git_repo TEXT UNIQUE NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
motd TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE worker_has_access_to_project
|
||||
@ -61,43 +52,51 @@ CREATE TABLE task
|
||||
|
||||
CREATE TABLE worker_verifies_task
|
||||
(
|
||||
verification_hash BIGINT,
|
||||
task BIGINT REFERENCES task (id) ON DELETE CASCADE,
|
||||
worker INT REFERENCES worker (id)
|
||||
verification_hash BIGINT NOT NULL,
|
||||
task BIGINT REFERENCES task (id) ON DELETE CASCADE NOT NULL,
|
||||
worker INT REFERENCES worker (id) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE log_entry
|
||||
(
|
||||
level INTEGER,
|
||||
message TEXT,
|
||||
message_data TEXT,
|
||||
timestamp INTEGER
|
||||
level INTEGER NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
message_data TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE manager
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT UNIQUE,
|
||||
password BYTEA,
|
||||
website_admin BOOLEAN
|
||||
register_time INTEGER NOT NULL,
|
||||
website_admin BOOLEAN NOT NULL,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE manager_has_role_on_project
|
||||
(
|
||||
manager INTEGER REFERENCES manager (id),
|
||||
role SMALLINT,
|
||||
project INTEGER REFERENCES project (id)
|
||||
manager INTEGER REFERENCES manager (id) NOT NULL,
|
||||
role SMALLINT NOT NULl,
|
||||
project INTEGER REFERENCES project (id) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE project_monitoring_snapshot
|
||||
(
|
||||
project INT REFERENCES project (id),
|
||||
new_task_count INT,
|
||||
failed_task_count INT,
|
||||
closed_task_count INT,
|
||||
awaiting_verification_task_count INT,
|
||||
worker_access_count INT,
|
||||
timestamp INT
|
||||
project INT REFERENCES project (id) NOT NULL,
|
||||
new_task_count INT NOT NULL,
|
||||
failed_task_count INT NOT NULL,
|
||||
closed_task_count INT NOT NULL,
|
||||
awaiting_verification_task_count INT NOT NULL,
|
||||
worker_access_count INT NOT NULL,
|
||||
timestamp INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE worker_requests_access_to_project
|
||||
(
|
||||
worker INT REFERENCES worker (id),
|
||||
project INT REFERENCES project (id),
|
||||
PRIMARY KEY (worker, project)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS
|
||||
@ -114,6 +113,21 @@ CREATE TRIGGER on_task_delete
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE on_task_delete_proc();
|
||||
|
||||
CREATE OR REPLACE FUNCTION on_manager_insert() RETURNS TRIGGER AS
|
||||
$$
|
||||
BEGIN
|
||||
IF NEW.id = 1 THEN
|
||||
UPDATE manager SET website_admin= TRUE WHERE id = 1;
|
||||
end if;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE 'plpgsql';
|
||||
CREATE TRIGGER on_manager_insert
|
||||
AFTER INSERT
|
||||
ON manager
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE on_manager_insert();
|
||||
|
||||
CREATE OR REPLACE FUNCTION release_task_ok(wid INT, tid INT, ver INT) RETURNS BOOLEAN AS
|
||||
$$
|
||||
DECLARE
|
||||
|
@ -10,9 +10,10 @@ import (
|
||||
type ManagerRole int
|
||||
|
||||
const (
|
||||
ROLE_NONE ManagerRole = 0
|
||||
ROLE_READ ManagerRole = 1
|
||||
ROLE_EDIT ManagerRole = 2
|
||||
ROLE_MANAGE_ACCESS ManagerRole = 3
|
||||
ROLE_MANAGE_ACCESS ManagerRole = 4
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
@ -65,8 +66,8 @@ func (database *Database) SaveManager(manager *Manager, password []byte) error {
|
||||
hash.Write([]byte(manager.Username))
|
||||
hashedPassword := hash.Sum(nil)
|
||||
|
||||
row := db.QueryRow(`INSERT INTO manager (username, password, website_admin)
|
||||
VALUES ($1,$2,$3) RETURNING ID`,
|
||||
row := db.QueryRow(`INSERT INTO manager (username, password, website_admin, register_time)
|
||||
VALUES ($1,$2,$3, extract(epoch from now() at time zone 'utc')) RETURNING ID`,
|
||||
manager.Username, hashedPassword, manager.WebsiteAdmin)
|
||||
|
||||
err := row.Scan(&manager.Id)
|
||||
@ -78,6 +79,8 @@ func (database *Database) SaveManager(manager *Manager, password []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.WebsiteAdmin = manager.Id == 1
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"manager": manager,
|
||||
}).Info("Database.SaveManager INSERT")
|
||||
@ -121,3 +124,19 @@ func (database *Database) UpdateManagerPassword(manager *Manager, newPassword []
|
||||
"id": manager.Id,
|
||||
}).Warning("Database.UpdateManagerPassword UPDATE")
|
||||
}
|
||||
|
||||
func (database *Database) ManagerHasRoleOn(manager *Manager, projectId int64) ManagerRole {
|
||||
|
||||
db := database.getDB()
|
||||
|
||||
row := db.QueryRow(`SELECT role FROM manager_has_role_on_project
|
||||
WHERE project=$1 AND manager=$2`, projectId, manager.Id)
|
||||
|
||||
var role ManagerRole
|
||||
err := row.Scan(&role)
|
||||
if err != nil {
|
||||
return ROLE_NONE
|
||||
}
|
||||
|
||||
return role
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ func (database *Database) MakeProjectSnapshots() {
|
||||
"took": time.Now().Sub(startTime),
|
||||
"add": inserted,
|
||||
"remove": deleted,
|
||||
}).Trace("Took monitoring snapshot")
|
||||
}).Trace("Took project monitoring snapshot")
|
||||
}
|
||||
|
||||
func (database *Database) GetMonitoringSnapshotsBetween(pid int64, from int, to int) (ss *[]ProjectMonitoringSnapshot) {
|
||||
|
@ -15,6 +15,7 @@ type Project struct {
|
||||
Version string `json:"version"`
|
||||
Motd string `json:"motd"`
|
||||
Public bool `json:"public"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
type AssignedTasks struct {
|
||||
@ -31,9 +32,10 @@ func (database *Database) SaveProject(project *Project) (int64, error) {
|
||||
|
||||
func saveProject(project *Project, db *sql.DB) (int64, error) {
|
||||
|
||||
row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority, motd, public)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`,
|
||||
project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd, project.Public)
|
||||
row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority, motd, public, hidden)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id`,
|
||||
project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd,
|
||||
project.Public, project.Hidden)
|
||||
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
@ -64,7 +66,7 @@ func (database *Database) GetProject(id int64) *Project {
|
||||
|
||||
func getProject(id int64, db *sql.DB) *Project {
|
||||
|
||||
row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, motd, public
|
||||
row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, motd, public, hidden
|
||||
FROM project WHERE id=$1`, id)
|
||||
|
||||
project, err := scanProject(row)
|
||||
@ -87,7 +89,7 @@ func scanProject(row *sql.Row) (*Project, error) {
|
||||
|
||||
project := &Project{}
|
||||
err := row.Scan(&project.Id, &project.Priority, &project.Name, &project.CloneUrl,
|
||||
&project.GitRepo, &project.Version, &project.Motd, &project.Public)
|
||||
&project.GitRepo, &project.Version, &project.Motd, &project.Public, &project.Hidden)
|
||||
|
||||
return project, err
|
||||
}
|
||||
@ -95,7 +97,7 @@ func scanProject(row *sql.Row) (*Project, error) {
|
||||
func (database *Database) GetProjectWithRepoName(repoName string) *Project {
|
||||
|
||||
db := database.getDB()
|
||||
row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, motd, public
|
||||
row := db.QueryRow(`SELECT id, priority, name, clone_url, git_repo, version, motd, public, hidden
|
||||
FROM project WHERE LOWER(git_repo)=$1`,
|
||||
strings.ToLower(repoName))
|
||||
|
||||
@ -115,8 +117,9 @@ func (database *Database) UpdateProject(project *Project) error {
|
||||
db := database.getDB()
|
||||
|
||||
res, err := db.Exec(`UPDATE project
|
||||
SET (priority, name, clone_url, git_repo, version, motd, public) = ($1,$2,$3,$4,$5,$6,$7) WHERE id=$8`,
|
||||
project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd, project.Public, project.Id)
|
||||
SET (priority, name, clone_url, git_repo, version, motd, public, hidden) = ($1,$2,$3,$4,$5,$6,$7,$8) WHERE id=$9`,
|
||||
project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd,
|
||||
project.Public, project.Hidden, project.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -139,26 +142,24 @@ func (database Database) GetAllProjects(workerId int64) *[]Project {
|
||||
var err error
|
||||
if workerId == 0 {
|
||||
rows, err = db.Query(`SELECT
|
||||
Id, priority, name, clone_url, git_repo, version, motd, public
|
||||
Id, priority, name, clone_url, git_repo, version, motd, public, hidden
|
||||
FROM project
|
||||
LEFT JOIN worker_has_access_to_project whatp ON whatp.project = id
|
||||
WHERE public
|
||||
WHERE NOT hidden
|
||||
ORDER BY name`)
|
||||
} else {
|
||||
rows, err = db.Query(`SELECT
|
||||
Id, priority, name, clone_url, git_repo, version, motd, public
|
||||
Id, priority, name, clone_url, git_repo, version, motd, public, hidden
|
||||
FROM project
|
||||
LEFT JOIN worker_has_access_to_project whatp ON whatp.project = id
|
||||
WHERE public OR whatp.worker = $1
|
||||
WHERE NOT hidden OR whatp.worker = $1
|
||||
ORDER BY name`, workerId)
|
||||
}
|
||||
handleErr(err)
|
||||
|
||||
for rows.Next() {
|
||||
|
||||
p := Project{}
|
||||
err := rows.Scan(&p.Id, &p.Priority, &p.Name, &p.CloneUrl,
|
||||
&p.GitRepo, &p.Version, &p.Motd, &p.Public)
|
||||
&p.GitRepo, &p.Version, &p.Motd, &p.Public, &p.Hidden)
|
||||
handleErr(err)
|
||||
projects = append(projects, p)
|
||||
}
|
||||
|
@ -1,22 +1,14 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
Id int64 `json:"id"`
|
||||
Created int64 `json:"created"`
|
||||
Identity *Identity `json:"identity"`
|
||||
Alias string `json:"alias,omitempty"`
|
||||
Secret []byte `json:"secret"`
|
||||
Id int64 `json:"id"`
|
||||
Created int64 `json:"created"`
|
||||
Alias string `json:"alias,omitempty"`
|
||||
Secret []byte `json:"secret"`
|
||||
}
|
||||
|
||||
type WorkerStats struct {
|
||||
@ -28,10 +20,8 @@ func (database *Database) SaveWorker(worker *Worker) {
|
||||
|
||||
db := database.getDB()
|
||||
|
||||
identityId := getOrCreateIdentity(worker.Identity, db)
|
||||
|
||||
row := db.QueryRow("INSERT INTO worker (created, identity, secret, alias) VALUES ($1,$2,$3,$4) RETURNING id",
|
||||
worker.Created, identityId, worker.Secret, worker.Alias)
|
||||
row := db.QueryRow("INSERT INTO worker (created, secret, alias) VALUES ($1,$2,$3) RETURNING id",
|
||||
worker.Created, worker.Secret, worker.Alias)
|
||||
|
||||
err := row.Scan(&worker.Id)
|
||||
handleErr(err)
|
||||
@ -46,10 +36,9 @@ func (database *Database) GetWorker(id int64) *Worker {
|
||||
db := database.getDB()
|
||||
|
||||
worker := &Worker{}
|
||||
var identityId int64
|
||||
|
||||
row := db.QueryRow("SELECT id, created, identity, secret, alias FROM worker WHERE id=$1", id)
|
||||
err := row.Scan(&worker.Id, &worker.Created, &identityId, &worker.Secret, &worker.Alias)
|
||||
row := db.QueryRow("SELECT id, created, secret, alias FROM worker WHERE id=$1", id)
|
||||
err := row.Scan(&worker.Id, &worker.Created, &worker.Secret, &worker.Alias)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
@ -57,9 +46,6 @@ func (database *Database) GetWorker(id int64) *Worker {
|
||||
return nil
|
||||
}
|
||||
|
||||
worker.Identity, err = getIdentity(identityId, db)
|
||||
handleErr(err)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"worker": worker,
|
||||
}).Trace("Database.getWorker SELECT worker")
|
||||
@ -67,48 +53,6 @@ func (database *Database) GetWorker(id int64) *Worker {
|
||||
return worker
|
||||
}
|
||||
|
||||
func getIdentity(id int64, db *sql.DB) (*Identity, error) {
|
||||
|
||||
identity := &Identity{}
|
||||
|
||||
row := db.QueryRow("SELECT remote_addr, user_agent FROM worker_identity WHERE id=$1", id)
|
||||
err := row.Scan(&identity.RemoteAddr, &identity.UserAgent)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New("identity not found")
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"identity": identity,
|
||||
}).Trace("Database.getIdentity SELECT workerIdentity")
|
||||
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
func getOrCreateIdentity(identity *Identity, db *sql.DB) int64 {
|
||||
|
||||
res, err := db.Exec("INSERT INTO worker_identity (remote_addr, user_agent) VALUES ($1,$2) ON CONFLICT DO NOTHING",
|
||||
identity.RemoteAddr, identity.UserAgent)
|
||||
handleErr(err)
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rowsAffected": rowsAffected,
|
||||
}).Trace("Database.saveWorker INSERT workerIdentity")
|
||||
|
||||
row := db.QueryRow("SELECT (id) FROM worker_identity WHERE remote_addr=$1", identity.RemoteAddr)
|
||||
|
||||
var rowId int64
|
||||
err = row.Scan(&rowId)
|
||||
handleErr(err)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rowId": rowId,
|
||||
}).Trace("Database.saveWorker SELECT workerIdentity")
|
||||
|
||||
return rowId
|
||||
}
|
||||
|
||||
func (database *Database) GrantAccess(workerId int64, projectId int64) bool {
|
||||
|
||||
db := database.getDB()
|
||||
@ -169,6 +113,88 @@ func (database *Database) UpdateWorker(worker *Worker) bool {
|
||||
return rowsAffected == 1
|
||||
}
|
||||
|
||||
func (database *Database) SaveAccessRequest(worker *Worker, projectId int64) bool {
|
||||
|
||||
db := database.getDB()
|
||||
|
||||
res, err := db.Exec(`INSERT INTO worker_requests_access_to_project
|
||||
SELECT $1, id FROM project WHERE id=$2 AND NOT project.public
|
||||
AND NOT EXISTS(SELECT * FROM worker_has_access_to_project WHERE worker=$1 AND project=$2)`,
|
||||
worker.Id, projectId)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
rowsAffected, _ := res.RowsAffected()
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rowsAffected": rowsAffected,
|
||||
}).Trace("Database.SaveAccessRequest INSERT")
|
||||
|
||||
return rowsAffected == 1
|
||||
}
|
||||
|
||||
func (database *Database) AcceptAccessRequest(worker *Worker, projectId int64) bool {
|
||||
|
||||
db := database.getDB()
|
||||
|
||||
res, err := db.Exec(`DELETE FROM worker_requests_access_to_project
|
||||
WHERE worker=$1 AND project=$2`)
|
||||
handleErr(err)
|
||||
|
||||
rowsAffected, _ := res.RowsAffected()
|
||||
if rowsAffected == 1 {
|
||||
_, err := db.Exec(`INSERT INTO worker_has_access_to_project
|
||||
(worker, project) VALUES ($1,$2)`,
|
||||
worker.Id, projectId)
|
||||
handleErr(err)
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rowsAffected": rowsAffected,
|
||||
}).Trace("Database.AcceptAccessRequest")
|
||||
|
||||
return rowsAffected == 1
|
||||
}
|
||||
|
||||
func (database *Database) RejectAccessRequest(worker *Worker, projectId int64) bool {
|
||||
|
||||
db := database.getDB()
|
||||
|
||||
res, err := db.Exec(`DELETE FROM worker_requests_access_to_project
|
||||
WHERE worker=$1 AND project=$2`, worker.Id, projectId)
|
||||
handleErr(err)
|
||||
|
||||
rowsAffected, _ := res.RowsAffected()
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rowsAffected": rowsAffected,
|
||||
}).Trace("Database.AcceptAccessRequest")
|
||||
|
||||
return rowsAffected == 1
|
||||
}
|
||||
|
||||
func (database *Database) GetAllAccessRequests(projectId int64) *[]Worker {
|
||||
|
||||
db := database.getDB()
|
||||
|
||||
rows, err := db.Query(`SELECT id, alias, created FROM worker_requests_access_to_project
|
||||
INNER JOIN worker w on worker_requests_access_to_project.worker = w.id
|
||||
WHERE project=$1`,
|
||||
projectId)
|
||||
handleErr(err)
|
||||
|
||||
requests := make([]Worker, 0)
|
||||
|
||||
for rows.Next() {
|
||||
w := Worker{}
|
||||
_ = rows.Scan(&w.Id, &w.Alias, &w.Created)
|
||||
requests = append(requests, w)
|
||||
}
|
||||
|
||||
return &requests
|
||||
}
|
||||
|
||||
func (database *Database) GetAllWorkerStats() *[]WorkerStats {
|
||||
|
||||
db := database.getDB()
|
||||
|
@ -163,3 +163,7 @@ func login(request *api.LoginRequest) (*api.LoginResponse, *http.Response) {
|
||||
|
||||
return resp, r
|
||||
}
|
||||
|
||||
func getSessionCtx(username string, password string, admin bool) {
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ func TestCreateGetProject(t *testing.T) {
|
||||
Priority: 123,
|
||||
Motd: "motd",
|
||||
Public: true,
|
||||
Hidden: true,
|
||||
})
|
||||
|
||||
id := resp.Id
|
||||
@ -59,6 +60,9 @@ func TestCreateGetProject(t *testing.T) {
|
||||
if getResp.Project.Public != true {
|
||||
t.Error()
|
||||
}
|
||||
if getResp.Project.Hidden != true {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProjectInvalid(t *testing.T) {
|
||||
@ -141,6 +145,7 @@ func TestUpdateProjectValid(t *testing.T) {
|
||||
Name: "NameB",
|
||||
Motd: "MotdB",
|
||||
Public: false,
|
||||
Hidden: true,
|
||||
}, pid)
|
||||
|
||||
if updateResp.Ok != true {
|
||||
@ -164,6 +169,9 @@ func TestUpdateProjectValid(t *testing.T) {
|
||||
if proj.Project.Priority != 2 {
|
||||
t.Error()
|
||||
}
|
||||
if proj.Project.Hidden != true {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProjectInvalid(t *testing.T) {
|
||||
|
@ -33,12 +33,6 @@ func TestCreateGetWorker(t *testing.T) {
|
||||
t.Error()
|
||||
}
|
||||
|
||||
if len(getResp.Worker.Identity.RemoteAddr) <= 0 {
|
||||
t.Error()
|
||||
}
|
||||
if len(getResp.Worker.Identity.UserAgent) <= 0 {
|
||||
t.Error()
|
||||
}
|
||||
if resp.Worker.Alias != "my_worker_alias" {
|
||||
t.Error()
|
||||
}
|
||||
|
@ -15,6 +15,11 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type SessionContext struct {
|
||||
Manager *storage.Manager
|
||||
SessionCookie *http.Cookie
|
||||
}
|
||||
|
||||
func Post(path string, x interface{}, worker *storage.Worker) *http.Response {
|
||||
|
||||
body, err := json.Marshal(x)
|
||||
|
105
test/schema.sql
105
test/schema.sql
@ -1,39 +1,30 @@
|
||||
DROP TABLE IF EXISTS worker_identity, worker, project, task, log_entry,
|
||||
DROP TABLE IF EXISTS worker, project, task, log_entry,
|
||||
worker_has_access_to_project, manager, manager_has_role_on_project, project_monitoring_snapshot,
|
||||
worker_verifies_task;
|
||||
worker_verifies_task, worker_requests_access_to_project;
|
||||
DROP TYPE IF EXISTS status;
|
||||
DROP TYPE IF EXISTS log_level;
|
||||
|
||||
CREATE TABLE worker_identity
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
remote_addr TEXT,
|
||||
user_agent TEXT,
|
||||
|
||||
UNIQUE (remote_addr)
|
||||
);
|
||||
|
||||
CREATE TABLE worker
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
alias TEXT,
|
||||
created INTEGER,
|
||||
identity INTEGER REFERENCES worker_identity (id),
|
||||
secret BYTEA,
|
||||
closed_task_count INTEGER DEFAULT 0
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
alias TEXT NOT NULL,
|
||||
created INTEGER NOT NULL,
|
||||
secret BYTEA NOT NULL,
|
||||
closed_task_count INTEGER DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE project
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
priority INTEGER DEFAULT 0,
|
||||
name TEXT UNIQUE,
|
||||
clone_url TEXT,
|
||||
git_repo TEXT UNIQUE,
|
||||
version TEXT,
|
||||
motd TEXT,
|
||||
public boolean,
|
||||
closed_task_count INT DEFAULT 0
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
priority INTEGER DEFAULT 0 NOT NULL,
|
||||
closed_task_count INT DEFAULT 0 NOT NULL,
|
||||
public boolean NOT NULL,
|
||||
hidden boolean NOT NULL,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
clone_url TEXT NOT NULL,
|
||||
git_repo TEXT UNIQUE NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
motd TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE worker_has_access_to_project
|
||||
@ -61,43 +52,50 @@ CREATE TABLE task
|
||||
|
||||
CREATE TABLE worker_verifies_task
|
||||
(
|
||||
verification_hash BIGINT,
|
||||
task BIGINT REFERENCES task (id) ON DELETE CASCADE,
|
||||
worker INT REFERENCES worker (id)
|
||||
verification_hash BIGINT NOT NULL,
|
||||
task BIGINT REFERENCES task (id) ON DELETE CASCADE NOT NULL,
|
||||
worker INT REFERENCES worker (id) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE log_entry
|
||||
(
|
||||
level INTEGER,
|
||||
message TEXT,
|
||||
message_data TEXT,
|
||||
timestamp INTEGER
|
||||
level INTEGER NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
message_data TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE manager
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT UNIQUE,
|
||||
password BYTEA,
|
||||
website_admin BOOLEAN
|
||||
register_time INTEGER NOT NULL,
|
||||
website_admin BOOLEAN NOT NULL,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE manager_has_role_on_project
|
||||
(
|
||||
manager INTEGER REFERENCES manager (id),
|
||||
role SMALLINT,
|
||||
project INTEGER REFERENCES project (id)
|
||||
manager INTEGER REFERENCES manager (id) NOT NULL,
|
||||
role SMALLINT NOT NULl,
|
||||
project INTEGER REFERENCES project (id) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE project_monitoring_snapshot
|
||||
(
|
||||
project INT REFERENCES project (id),
|
||||
new_task_count INT,
|
||||
failed_task_count INT,
|
||||
closed_task_count INT,
|
||||
awaiting_verification_task_count INT,
|
||||
worker_access_count INT,
|
||||
timestamp INT
|
||||
project INT REFERENCES project (id) NOT NULL,
|
||||
new_task_count INT NOT NULL,
|
||||
failed_task_count INT NOT NULL,
|
||||
closed_task_count INT NOT NULL,
|
||||
awaiting_verification_task_count INT NOT NULL,
|
||||
worker_access_count INT NOT NULL,
|
||||
timestamp INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE worker_requests_access_to_project
|
||||
(
|
||||
worker INT REFERENCES worker (id) NOT NULL,
|
||||
project INT REFERENCES project (id) NOT NULL
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS
|
||||
@ -114,6 +112,21 @@ CREATE TRIGGER on_task_delete
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE on_task_delete_proc();
|
||||
|
||||
CREATE OR REPLACE FUNCTION on_manager_insert() RETURNS TRIGGER AS
|
||||
$$
|
||||
BEGIN
|
||||
IF NEW.id = 1 THEN
|
||||
UPDATE manager SET website_admin= TRUE WHERE id = 1;
|
||||
end if;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE 'plpgsql';
|
||||
CREATE TRIGGER on_manager_insert
|
||||
AFTER INSERT
|
||||
ON manager
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE on_manager_insert();
|
||||
|
||||
CREATE OR REPLACE FUNCTION release_task_ok(wid INT, tid INT, ver INT) RETURNS BOOLEAN AS
|
||||
$$
|
||||
DECLARE
|
||||
|
@ -65,4 +65,8 @@ export class ApiService {
|
||||
return this.http.get(this.url + `/worker/stats`, this.options)
|
||||
}
|
||||
|
||||
getProjectAccessRequests(project: number) {
|
||||
return this.http.get(this.url + `/project/requests/${project}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import {TranslateService} from "@ngx-translate/core";
|
||||
import {LoginComponent} from "./login/login.component";
|
||||
import {AccountDetailsComponent} from "./account-details/account-details.component";
|
||||
import {WorkerDashboardComponent} from "./worker-dashboard/worker-dashboard.component";
|
||||
import {ProjectPermsComponent} from "./project-perms/project-perms.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{path: "log", component: LogsComponent},
|
||||
@ -19,6 +20,7 @@ const routes: Routes = [
|
||||
{path: "projects", component: ProjectListComponent},
|
||||
{path: "project/:id", component: ProjectDashboardComponent},
|
||||
{path: "project/:id/update", component: UpdateProjectComponent},
|
||||
{path: "project/:id/perms", component: ProjectPermsComponent},
|
||||
{path: "new_project", component: CreateProjectComponent},
|
||||
{path: "workers", component: WorkerDashboardComponent}
|
||||
];
|
||||
|
@ -6,10 +6,11 @@
|
||||
[routerLink]="'log'">{{"nav.logs" | translate}}</button>
|
||||
<button mat-button [class.mat-accent]="router.url == '/projects'" class="nav-link"
|
||||
[routerLink]="'projects'">{{"nav.project_list" | translate}}</button>
|
||||
<button mat-button [class.mat-accent]="router.url == '/new_project'" class="nav-link"
|
||||
[routerLink]="'new_project'">{{"nav.new_project" | translate}}</button>
|
||||
<button mat-button [class.mat-accent]="router.url == '/workers'" class="nav-link"
|
||||
[routerLink]="'workers'">{{"nav.worker_dashboard" | translate}}</button>
|
||||
<button mat-button [class.mat-accent]="router.url == '/new_project'" class="nav-link"
|
||||
[routerLink]="'new_project'"
|
||||
*ngIf="authService.logged">{{"nav.new_project" | translate}}</button>
|
||||
</div>
|
||||
<div class="small-nav">
|
||||
<button mat-button [matMenuTriggerFor]="smallNav">
|
||||
@ -22,10 +23,11 @@
|
||||
[routerLink]="'log'">{{"nav.logs" | translate}}</button>
|
||||
<button mat-menu-item [class.mat-accent]="router.url == '/projects'" class="nav-link"
|
||||
[routerLink]="'projects'">{{"nav.project_list" | translate}}</button>
|
||||
<button mat-menu-item [class.mat-accent]="router.url == '/new_project'" class="nav-link"
|
||||
[routerLink]="'new_project'">{{"nav.new_project" | translate}}</button>
|
||||
<button mat-menu-item [class.mat-accent]="router.url == '/workers'" class="nav-link"
|
||||
[routerLink]="'workers'">{{"nav.worker_dashboard" | translate}}</button>
|
||||
<button mat-menu-item [class.mat-accent]="router.url == '/new_project'" class="nav-link"
|
||||
[routerLink]="'new_project'"
|
||||
*ngIf="authService.logged">{{"nav.new_project" | translate}}</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<span class="nav-spacer"></span>
|
||||
|
@ -20,7 +20,7 @@ export class AppComponent {
|
||||
];
|
||||
|
||||
constructor(private translate: TranslateService,
|
||||
private router: Router,
|
||||
public router: Router,
|
||||
public authService: AuthService) {
|
||||
|
||||
translate.addLangs([
|
||||
|
@ -47,6 +47,7 @@ import {TranslatedPaginator} from "./TranslatedPaginatorConfiguration";
|
||||
import {LoginComponent} from './login/login.component';
|
||||
import {AccountDetailsComponent} from './account-details/account-details.component';
|
||||
import {WorkerDashboardComponent} from './worker-dashboard/worker-dashboard.component';
|
||||
import {ProjectPermsComponent} from './project-perms/project-perms.component';
|
||||
|
||||
|
||||
export function createTranslateLoader(http: HttpClient) {
|
||||
@ -66,6 +67,7 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
LoginComponent,
|
||||
AccountDetailsComponent,
|
||||
WorkerDashboardComponent,
|
||||
ProjectPermsComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -10,6 +10,7 @@ import {Router} from "@angular/router";
|
||||
export class AuthService {
|
||||
|
||||
account: Manager;
|
||||
logged: boolean;
|
||||
|
||||
constructor(private apiService: ApiService,
|
||||
private messengerService: MessengerService,
|
||||
@ -17,6 +18,7 @@ export class AuthService {
|
||||
this.apiService.getAccountDetails()
|
||||
.subscribe((data: any) => {
|
||||
this.account = data.manager;
|
||||
this.logged = data.logged_in;
|
||||
})
|
||||
}
|
||||
|
||||
@ -27,6 +29,7 @@ export class AuthService {
|
||||
this.apiService.getAccountDetails()
|
||||
.subscribe((data: any) => {
|
||||
this.account = data.manager;
|
||||
this.logged = true;
|
||||
this.router.navigateByUrl("/account");
|
||||
})
|
||||
},
|
||||
@ -42,7 +45,8 @@ export class AuthService {
|
||||
.subscribe(
|
||||
() => {
|
||||
this.account = null;
|
||||
this.router.navigateByUrl("");
|
||||
this.logged = false;
|
||||
this.router.navigateByUrl("login");
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
|
@ -3,40 +3,47 @@
|
||||
|
||||
<mat-tab-group>
|
||||
<mat-tab [label]="'login.title' | translate" class="pad">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{"login.username" | translate}}</mat-label>
|
||||
<input type="text" matInput [(ngModel)]="credentials.username">
|
||||
</mat-form-field>
|
||||
<form (ngSubmit)="login()">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{"login.username" | translate}}</mat-label>
|
||||
<input type="text" matInput [(ngModel)]="credentials.username"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ "login.password" | translate}}</mat-label>
|
||||
<input type="password" matInput [(ngModel)]="credentials.password">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ "login.password" | translate}}</mat-label>
|
||||
<input type="password" matInput [(ngModel)]="credentials.password"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-raised-button color="primary"
|
||||
(click)="login()">{{"login.login" | translate}}</button>
|
||||
<button mat-raised-button color="primary"
|
||||
type="submit">{{"login.login" | translate}}</button>
|
||||
</form>
|
||||
</mat-tab>
|
||||
<mat-tab [label]="'create_account.title' | translate" class="pad">
|
||||
<mat-form-field appearance="outline" [hideRequiredMarker]="true">
|
||||
<mat-label>{{"login.username" | translate}}</mat-label>
|
||||
<mat-hint align="end">{{credentials.username?.length || 0}}/16</mat-hint>
|
||||
<input maxlength="16" type="text" matInput [(ngModel)]="credentials.username" name="username"
|
||||
required>
|
||||
</mat-form-field>
|
||||
<form (ngSubmit)="register()">
|
||||
<mat-form-field appearance="outline" [hideRequiredMarker]="true">
|
||||
<mat-label>{{"login.username" | translate}}</mat-label>
|
||||
<mat-hint align="end">{{credentials.username?.length || 0}}/16</mat-hint>
|
||||
<input maxlength="16" type="text" matInput [(ngModel)]="credentials.username" name="username"
|
||||
[ngModelOptions]="{standalone: true}" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" [hideRequiredMarker]="true">
|
||||
<mat-label>{{ "login.password" | translate}}</mat-label>
|
||||
<input type="password" matInput [(ngModel)]="credentials.password" name="password" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" [hideRequiredMarker]="true">
|
||||
<mat-label>{{ "login.password" | translate}}</mat-label>
|
||||
<input type="password" matInput [(ngModel)]="credentials.password" name="password"
|
||||
[ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ "login.repeat_password" | translate}}</mat-label>
|
||||
<input type="password" matInput [(ngModel)]="credentials.repeatPassword" name="password2"
|
||||
>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ "login.repeat_password" | translate}}</mat-label>
|
||||
<input type="password" matInput [(ngModel)]="credentials.repeatPassword" name="password2"
|
||||
[ngModelOptions]="{standalone: true}" required>
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-raised-button color="primary" [disabled]="!canCreate()"
|
||||
(click)="register()">{{"create_account.create" | translate}}</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!canCreate()"
|
||||
type="submit">{{"create_account.create" | translate}}</button>
|
||||
</form>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-card>
|
||||
|
@ -3,6 +3,7 @@ import {MessengerService} from "../messenger.service";
|
||||
import {MessengerState} from "./messenger";
|
||||
import {Subscription} from "rxjs";
|
||||
import {MatSnackBar, MatSnackBarConfig} from "@angular/material";
|
||||
import {TranslateService} from "@ngx-translate/core";
|
||||
|
||||
@Component({
|
||||
selector: 'messenger-snack-bar',
|
||||
@ -13,7 +14,10 @@ export class SnackBarComponent implements OnInit {
|
||||
|
||||
private subscription: Subscription;
|
||||
|
||||
constructor(private messengerService: MessengerService, private snackBar: MatSnackBar) {
|
||||
constructor(
|
||||
private messengerService: MessengerService,
|
||||
private snackBar: MatSnackBar,
|
||||
private translate: TranslateService) {
|
||||
|
||||
}
|
||||
|
||||
@ -23,9 +27,11 @@ export class SnackBarComponent implements OnInit {
|
||||
if (state.hidden) {
|
||||
this.snackBar.dismiss();
|
||||
} else {
|
||||
this.snackBar.open(state.message, "Close", <MatSnackBarConfig>{
|
||||
duration: 10 * 1000,
|
||||
})
|
||||
this.translate.get("messenger.close")
|
||||
.subscribe(t =>
|
||||
this.snackBar.open(state.message, t, <MatSnackBarConfig>{
|
||||
duration: 10 * 1000,
|
||||
}))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
interface Manager {
|
||||
id: number;
|
||||
username: string
|
||||
website_admin: boolean;
|
||||
}
|
||||
|
6
web/angular/src/app/models/worker.ts
Normal file
6
web/angular/src/app/models/worker.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface Worker {
|
||||
id: number
|
||||
alias: string
|
||||
created: number
|
||||
secret: string
|
||||
}
|
@ -56,6 +56,8 @@
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" *ngIf="project"
|
||||
[routerLink]="'/project/' + project.id + '/update'">{{"project.update" | translate}}</button>
|
||||
<button mat-raised-button color="primary" *ngIf="project"
|
||||
[routerLink]="'/project/' + project.id + '/perms'">{{"project.perms" | translate}}</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -1,3 +1,7 @@
|
||||
button {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
mat-panel-title > mat-icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
@ -7,16 +7,22 @@
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel *ngFor="let project of projects">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title><span style="width: 3em">{{project.id}}</span>{{project.name}}
|
||||
<mat-panel-title>
|
||||
<mat-icon *ngIf="project.public">public</mat-icon>
|
||||
<mat-icon *ngIf="!project.public">lock</mat-icon>
|
||||
<span style="width: 3em">{{project.id}}</span>{{project.name}}
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>{{project.motd}}</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<pre>{{project | json}}</pre>
|
||||
<div>
|
||||
<button mat-raised-button [routerLink]="'/project/' + project.id">
|
||||
<button mat-raised-button color="primary" [routerLink]="'/project/' + project.id">
|
||||
<mat-icon>timeline</mat-icon>{{"projects.dashboard" | translate}}</button>
|
||||
<button mat-raised-button [routerLink]="'/project/' + project.id + '/update'">
|
||||
<button mat-raised-button color="primary" [routerLink]="'/project/' + project.id + '/update'">
|
||||
<mat-icon>build</mat-icon>{{"project.update" | translate}}</button>
|
||||
<button mat-raised-button color="primary" [routerLink]="'/project/' + project.id + '/perms'">
|
||||
<mat-icon>perm_identity</mat-icon>
|
||||
{{"project.perms" | translate}}</button>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
<span *ngIf="projects && projects.length == 0">
|
||||
|
@ -0,0 +1,15 @@
|
||||
.unauthorized {
|
||||
text-align: center;
|
||||
color: #616161;
|
||||
min-height: 3em;
|
||||
margin-top: 2em !important;
|
||||
}
|
||||
|
||||
.text-mono {
|
||||
font-family: Hack, Courier, "Courier New", monospace;
|
||||
color: #ff4081;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 15px;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<div class="container">
|
||||
<mat-card class="mat-elevation-z8">
|
||||
<button mat-button [title]="'perms.refresh' | translate" style="float:right"
|
||||
(click)="refresh()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{"perms.title" | translate}}</mat-card-title>
|
||||
<mat-card-subtitle>{{"perms.subtitle" | translate}}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<mat-list *ngIf="!unauthorized">
|
||||
<mat-list-item *ngFor="let w of requests">
|
||||
<mat-icon mat-list-icon>person_add</mat-icon>
|
||||
<h4 mat-line>{{w.alias}}</h4>
|
||||
<div mat-line>
|
||||
Id=<span class="text-mono">{{w.id}}</span>, {{"perms.created" | translate}}
|
||||
<span class="text-mono">{{moment.unix(w.created).format("YYYY-MM-DD HH:mm:ss UTC")}}</span>
|
||||
</div>
|
||||
<span style="flex: 1 1 auto;"></span>
|
||||
<button mat-raised-button color="primary" [title]="'perms.grant' | translate">
|
||||
<mat-icon>check</mat-icon>
|
||||
</button>
|
||||
<button mat-raised-button color="warn" [title]="'perms.reject' | translate">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
<p *ngIf="unauthorized" class="unauthorized">
|
||||
<mat-icon>block</mat-icon>
|
||||
{{"perms.unauthorized" | translate}}
|
||||
</p>
|
||||
</mat-card-content>
|
||||
|
||||
</mat-card>
|
||||
</div>
|
60
web/angular/src/app/project-perms/project-perms.component.ts
Normal file
60
web/angular/src/app/project-perms/project-perms.component.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import {Worker} from "../models/worker"
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {ApiService} from "../api.service";
|
||||
import {Project} from "../models/project";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {MessengerService} from "../messenger.service";
|
||||
import {TranslateService} from "@ngx-translate/core";
|
||||
|
||||
import * as moment from "moment"
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-perms',
|
||||
templateUrl: './project-perms.component.html',
|
||||
styleUrls: ['./project-perms.component.css']
|
||||
})
|
||||
export class ProjectPermsComponent implements OnInit {
|
||||
|
||||
constructor(private apiService: ApiService,
|
||||
private route: ActivatedRoute,
|
||||
private messengerService: MessengerService,
|
||||
private translate: TranslateService,
|
||||
private router: Router) {
|
||||
}
|
||||
|
||||
project: Project;
|
||||
private projectId: number;
|
||||
requests: Worker[];
|
||||
unauthorized: boolean = false;
|
||||
moment = moment;
|
||||
|
||||
ngOnInit() {
|
||||
this.route.params.subscribe(params => {
|
||||
this.projectId = params["id"];
|
||||
this.getProject();
|
||||
this.getProjectRequests();
|
||||
})
|
||||
}
|
||||
|
||||
private getProject() {
|
||||
this.apiService.getProject(this.projectId).subscribe(data => {
|
||||
this.project = data["project"]
|
||||
})
|
||||
}
|
||||
|
||||
private getProjectRequests() {
|
||||
this.apiService.getProjectAccessRequests(this.projectId).subscribe(
|
||||
data => {
|
||||
this.requests = data["requests"]
|
||||
},
|
||||
error => {
|
||||
if (error && (error.status == 401 || error.status == 403)) {
|
||||
this.unauthorized = true;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this.getProjectRequests()
|
||||
}
|
||||
}
|
@ -29,16 +29,7 @@ export class UpdateProjectComponent implements OnInit {
|
||||
|
||||
private getProject() {
|
||||
this.apiService.getProject(this.projectId).subscribe(data => {
|
||||
this.project = <Project>{
|
||||
id: data["project"]["id"],
|
||||
name: data["project"]["name"],
|
||||
clone_url: data["project"]["clone_url"],
|
||||
git_repo: data["project"]["git_repo"],
|
||||
motd: data["project"]["motd"],
|
||||
priority: data["project"]["priority"],
|
||||
version: data["project"]["version"],
|
||||
public: data["project"]["public"],
|
||||
}
|
||||
this.project = data["project"]
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,8 @@
|
||||
"create": "Create",
|
||||
"git_repo": "Git repository name",
|
||||
"motd": "Message of the day",
|
||||
"update": "Edit"
|
||||
"update": "Edit",
|
||||
"perms": "Permissions"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard for",
|
||||
@ -86,5 +87,18 @@
|
||||
"workers": {
|
||||
"title": "Completed tasks per worker",
|
||||
"subtitle": "Real-time data for all projects"
|
||||
},
|
||||
"perms": {
|
||||
"title": "Project permissions",
|
||||
"subtitle": "Workers need to request access to work on private projects",
|
||||
"unauthorized": "You need ROLE_MANAGE_ACCESS on this project to access this page",
|
||||
"created": "Created on",
|
||||
"grant": "Accept request",
|
||||
"reject": "Deny request",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"messenger": {
|
||||
"close": "Close",
|
||||
"unauthorized": "Unauthorized"
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,8 @@
|
||||
"public": "Publique",
|
||||
"create": "Créer",
|
||||
"motd": "Message du jour",
|
||||
"update": "Mettre à jour"
|
||||
"update": "Mettre à jour",
|
||||
"perms": "Permissions"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Tableau de bord pour ",
|
||||
@ -88,6 +89,19 @@
|
||||
"workers": {
|
||||
"title": "Tâches complétés par worker",
|
||||
"subtitle": "Données en temps réél pour tous les projets"
|
||||
},
|
||||
"perms": {
|
||||
"title": "Permissions du projet",
|
||||
"subtitle": "Les Workers doivent faire un requête d'acces pour pouvoir travailler sur les projets privés",
|
||||
"unauthorized": "Vous devez avoir ROLE_GESTION_ACCES sur ce project pour accéder à cette page",
|
||||
"created": "Créé le",
|
||||
"grant": "Accepter la requête",
|
||||
"reject": "Rejeter la requête",
|
||||
"refresh": "Refraichir"
|
||||
},
|
||||
"messenger": {
|
||||
"close": "Fermer",
|
||||
"unauthorized": "Non autorisé"
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user