mirror of
https://github.com/semaphoreui/semaphore.git
synced 2024-11-25 06:15:56 +01:00
commit
36570ae15c
@ -150,10 +150,10 @@ var pathSubPatterns = []func() string{
|
||||
func() string { return strconv.Itoa(userProject.ID) },
|
||||
func() string { return strconv.Itoa(userPathTestUser.ID) },
|
||||
func() string { return strconv.Itoa(userKey.ID) },
|
||||
func() string { return strconv.Itoa(int(repoID)) },
|
||||
func() string { return strconv.Itoa(int(inventoryID)) },
|
||||
func() string { return strconv.Itoa(int(environmentID)) },
|
||||
func() string { return strconv.Itoa(int(templateID)) },
|
||||
func() string { return strconv.Itoa(repoID) },
|
||||
func() string { return strconv.Itoa(inventoryID) },
|
||||
func() string { return strconv.Itoa(environmentID) },
|
||||
func() string { return strconv.Itoa(templateID) },
|
||||
func() string { return strconv.Itoa(task.ID) },
|
||||
func() string { return strconv.Itoa(schedule.ID) },
|
||||
func() string { return strconv.Itoa(view.ID) },
|
||||
|
@ -104,6 +104,7 @@ func main() {
|
||||
h.Before("project > /api/project/{project_id}/tasks/{task_id} > Get a single task > 200 > application/json", capabilityWrapper("task"))
|
||||
h.Before("project > /api/project/{project_id}/tasks/{task_id} > Deletes task (including output) > 204 > application/json", capabilityWrapper("task"))
|
||||
h.Before("project > /api/project/{project_id}/tasks/{task_id}/output > Get task output > 200 > application/json", capabilityWrapper("task"))
|
||||
h.Before("project > /api/project/{project_id}/tasks/{task_id}/stop > Stop a job > 204 > application/json", capabilityWrapper("task"))
|
||||
|
||||
h.Before("schedule > /api/project/{project_id}/schedules/{schedule_id} > Get schedule > 200 > application/json", capabilityWrapper("schedule"))
|
||||
h.Before("schedule > /api/project/{project_id}/schedules/{schedule_id} > Updates schedule > 204 > application/json", capabilityWrapper("schedule"))
|
||||
|
44
api-docs.yml
44
api-docs.yml
@ -928,6 +928,28 @@ paths:
|
||||
204:
|
||||
description: Project deleted
|
||||
|
||||
|
||||
/project/{project_id}/role:
|
||||
parameters:
|
||||
- $ref: "#/parameters/project_id"
|
||||
get:
|
||||
tags:
|
||||
- project
|
||||
summary: Fetch permissions of the current user for project
|
||||
responses:
|
||||
200:
|
||||
description: Permissions
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
role:
|
||||
type: string
|
||||
example: owner
|
||||
permissions:
|
||||
type: number
|
||||
example: 0
|
||||
|
||||
|
||||
/project/{project_id}/events:
|
||||
parameters:
|
||||
- $ref: '#/parameters/project_id'
|
||||
@ -1487,7 +1509,6 @@ paths:
|
||||
description: view removed
|
||||
|
||||
|
||||
|
||||
# tasks
|
||||
/project/{project_id}/tasks:
|
||||
parameters:
|
||||
@ -1533,6 +1554,8 @@ paths:
|
||||
description: Task queued
|
||||
schema:
|
||||
$ref: "#/definitions/Task"
|
||||
|
||||
|
||||
/project/{project_id}/tasks/last:
|
||||
parameters:
|
||||
- $ref: "#/parameters/project_id"
|
||||
@ -1547,6 +1570,22 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Task'
|
||||
|
||||
|
||||
/project/{project_id}/tasks/{task_id}/stop:
|
||||
parameters:
|
||||
- $ref: "#/parameters/project_id"
|
||||
- $ref: '#/parameters/task_id'
|
||||
post:
|
||||
tags:
|
||||
- project
|
||||
summary: Stop a job
|
||||
responses:
|
||||
204:
|
||||
description: Task queued
|
||||
|
||||
|
||||
|
||||
/project/{project_id}/tasks/{task_id}:
|
||||
parameters:
|
||||
- $ref: "#/parameters/project_id"
|
||||
@ -1567,6 +1606,9 @@ paths:
|
||||
responses:
|
||||
204:
|
||||
description: task deleted
|
||||
|
||||
|
||||
|
||||
/project/{project_id}/tasks/{task_id}/output:
|
||||
parameters:
|
||||
- $ref: '#/parameters/project_id'
|
||||
|
@ -24,7 +24,7 @@ func ProjectMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// check if user in project's team
|
||||
_, err = helpers.Store(r).GetProjectUser(projectID, user.ID)
|
||||
projectUser, err := helpers.Store(r).GetProjectUser(projectID, user.ID)
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
@ -38,36 +38,22 @@ func ProjectMiddleware(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
context.Set(r, "projectUserRole", projectUser.Role)
|
||||
context.Set(r, "project", project)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// GetMustCanMiddlewareFor ensures that the user has administrator rights
|
||||
func GetMustCanMiddlewareFor(permissions db.ProjectUserPermission) mux.MiddlewareFunc {
|
||||
// GetMustCanMiddleware ensures that the user has administrator rights
|
||||
func GetMustCanMiddleware(permissions db.ProjectUserPermission) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
projectUserRole := context.Get(r, "projectUserRole").(db.ProjectUserRole)
|
||||
|
||||
if !user.Admin {
|
||||
// check if user in project's team
|
||||
projectUser, err := helpers.Store(r).GetProjectUser(project.ID, user.ID)
|
||||
|
||||
if err == db.ErrNotFound {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "GET" && r.Method != "HEAD" && !projectUser.Can(permissions) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !user.Admin && r.Method != "GET" && r.Method != "HEAD" && !projectUserRole.Can(permissions) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
@ -80,6 +66,16 @@ func GetProject(w http.ResponseWriter, r *http.Request) {
|
||||
helpers.WriteJSON(w, http.StatusOK, context.Get(r, "project"))
|
||||
}
|
||||
|
||||
func GetUserRole(w http.ResponseWriter, r *http.Request) {
|
||||
var permissions struct {
|
||||
Role db.ProjectUserRole `json:"role"`
|
||||
Permissions db.ProjectUserPermission `json:"permissions"`
|
||||
}
|
||||
permissions.Role = context.Get(r, "projectUserRole").(db.ProjectUserRole)
|
||||
permissions.Permissions = permissions.Role.GetPermissions()
|
||||
helpers.WriteJSON(w, http.StatusOK, permissions)
|
||||
}
|
||||
|
||||
// UpdateProject saves updated project details to the database
|
||||
func UpdateProject(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
@ -128,17 +128,19 @@ func Route() *mux.Router {
|
||||
//
|
||||
// Start and Stop tasks
|
||||
projectTaskStart := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter()
|
||||
projectTaskStart.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks))
|
||||
projectTaskStart.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanRunProjectTasks))
|
||||
projectTaskStart.Path("/tasks").HandlerFunc(projects.AddTask).Methods("POST")
|
||||
|
||||
projectTaskStop := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter()
|
||||
projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks))
|
||||
projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddleware(db.CanRunProjectTasks))
|
||||
projectTaskStop.HandleFunc("/tasks/{task_id}/stop", projects.StopTask).Methods("POST")
|
||||
|
||||
//
|
||||
// Project resources CRUD
|
||||
projectUserAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter()
|
||||
projectUserAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanManageProjectResources))
|
||||
projectUserAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanManageProjectResources))
|
||||
|
||||
projectUserAPI.Path("/role").HandlerFunc(projects.GetUserRole).Methods("GET", "HEAD")
|
||||
|
||||
projectUserAPI.Path("/events").HandlerFunc(getAllEvents).Methods("GET", "HEAD")
|
||||
projectUserAPI.HandleFunc("/events/last", getLastEvents).Methods("GET", "HEAD")
|
||||
@ -173,14 +175,14 @@ func Route() *mux.Router {
|
||||
//
|
||||
// Updating and deleting project
|
||||
projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter()
|
||||
projectAdminAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanUpdateProject))
|
||||
projectAdminAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanUpdateProject))
|
||||
projectAdminAPI.Methods("PUT").HandlerFunc(projects.UpdateProject)
|
||||
projectAdminAPI.Methods("DELETE").HandlerFunc(projects.DeleteProject)
|
||||
|
||||
//
|
||||
// Manage project users
|
||||
projectAdminUsersAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter()
|
||||
projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanManageProjectUsers))
|
||||
projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanManageProjectUsers))
|
||||
projectAdminUsersAPI.Path("/users").HandlerFunc(projects.AddUser).Methods("POST")
|
||||
|
||||
projectUserManagement := projectAdminUsersAPI.PathPrefix("/users").Subrouter()
|
||||
|
11
api/user.go
11
api/user.go
@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"github.com/gorilla/context"
|
||||
"github.com/gorilla/mux"
|
||||
"io"
|
||||
@ -18,7 +19,15 @@ func getUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, context.Get(r, "user"))
|
||||
var user struct {
|
||||
db.User
|
||||
CanCreateProject bool `json:"can_create_project"`
|
||||
}
|
||||
|
||||
user.User = *context.Get(r, "user").(*db.User)
|
||||
user.CanCreateProject = user.Admin || util.Config.NonAdminCanCreateProject
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, user)
|
||||
}
|
||||
|
||||
func getAPITokens(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -19,8 +19,8 @@ const (
|
||||
)
|
||||
|
||||
var rolePermissions = map[ProjectUserRole]ProjectUserPermission{
|
||||
ProjectOwner: CanRunProjectTasks | CanUpdateProject | CanManageProjectResources,
|
||||
ProjectManager: CanRunProjectTasks | CanManageProjectResources,
|
||||
ProjectOwner: CanRunProjectTasks | CanManageProjectResources | CanUpdateProject | CanManageProjectUsers,
|
||||
ProjectManager: CanRunProjectTasks | CanManageProjectResources | CanManageProjectUsers,
|
||||
ProjectTaskRunner: CanRunProjectTasks,
|
||||
ProjectGuest: 0,
|
||||
}
|
||||
@ -39,5 +39,13 @@ type ProjectUser struct {
|
||||
|
||||
func (u *ProjectUser) Can(permissions ProjectUserPermission) bool {
|
||||
userPermissions := rolePermissions[u.Role]
|
||||
return (userPermissions & userPermissions) == permissions
|
||||
return (userPermissions & permissions) == permissions
|
||||
}
|
||||
|
||||
func (r ProjectUserRole) Can(permissions ProjectUserPermission) bool {
|
||||
return (rolePermissions[r] & permissions) == permissions
|
||||
}
|
||||
|
||||
func (r ProjectUserRole) GetPermissions() ProjectUserPermission {
|
||||
return rolePermissions[r]
|
||||
}
|
||||
|
15
db/ProjectUser_test.go
Normal file
15
db/ProjectUser_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProjectUsers_RoleCan(t *testing.T) {
|
||||
if !ProjectManager.Can(CanManageProjectResources) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if ProjectManager.Can(CanUpdateProject) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
218
web/src/App.vue
218
web/src/App.vue
@ -1,56 +1,56 @@
|
||||
<template>
|
||||
<v-app v-if="state === 'success'" class="app">
|
||||
<EditDialog
|
||||
v-model="passwordDialog"
|
||||
save-button-text="Save"
|
||||
:title="$t('changePassword')"
|
||||
v-if="user"
|
||||
event-name="i-user"
|
||||
v-model="passwordDialog"
|
||||
save-button-text="Save"
|
||||
:title="$t('changePassword')"
|
||||
v-if="user"
|
||||
event-name="i-user"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<ChangePasswordForm
|
||||
:project-id="projectId"
|
||||
:item-id="user.id"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:project-id="projectId"
|
||||
:item-id="user.id"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<EditDialog
|
||||
v-model="userDialog"
|
||||
save-button-text="Save"
|
||||
:title="$t('editUser')"
|
||||
v-if="user"
|
||||
event-name="i-user"
|
||||
v-model="userDialog"
|
||||
save-button-text="Save"
|
||||
:title="$t('editUser')"
|
||||
v-if="user"
|
||||
event-name="i-user"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<UserForm
|
||||
:project-id="projectId"
|
||||
:item-id="user.id"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:project-id="projectId"
|
||||
:item-id="user.id"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<EditDialog
|
||||
v-model="taskLogDialog"
|
||||
save-button-text="Delete"
|
||||
:max-width="1000"
|
||||
:hide-buttons="true"
|
||||
@close="onTaskLogDialogClosed()"
|
||||
v-model="taskLogDialog"
|
||||
save-button-text="Delete"
|
||||
:max-width="1000"
|
||||
:hide-buttons="true"
|
||||
@close="onTaskLogDialogClosed()"
|
||||
>
|
||||
<template v-slot:title={}>
|
||||
<div class="text-truncate" style="max-width: calc(100% - 36px);">
|
||||
<router-link
|
||||
class="breadcrumbs__item breadcrumbs__item--link"
|
||||
:to="`/project/${projectId}/templates/${template ? template.id : null}`"
|
||||
@click="taskLogDialog = false"
|
||||
class="breadcrumbs__item breadcrumbs__item--link"
|
||||
:to="`/project/${projectId}/templates/${template ? template.id : null}`"
|
||||
@click="taskLogDialog = false"
|
||||
>{{ template ? template.name : null }}
|
||||
</router-link>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
@ -59,8 +59,8 @@
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
icon
|
||||
@click="taskLogDialog = false; onTaskLogDialogClosed()"
|
||||
icon
|
||||
@click="taskLogDialog = false; onTaskLogDialogClosed()"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
@ -71,61 +71,61 @@
|
||||
</EditDialog>
|
||||
|
||||
<EditDialog
|
||||
v-model="newProjectDialog"
|
||||
save-button-text="Create"
|
||||
:title="$t('newProject')"
|
||||
event-name="i-project"
|
||||
v-model="newProjectDialog"
|
||||
save-button-text="Create"
|
||||
:title="$t('newProject')"
|
||||
event-name="i-project"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<ProjectForm
|
||||
item-id="new"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
item-id="new"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:color="snackbarColor"
|
||||
:timeout="3000"
|
||||
top
|
||||
v-model="snackbar"
|
||||
:color="snackbarColor"
|
||||
:timeout="3000"
|
||||
top
|
||||
>
|
||||
{{ snackbarText }}
|
||||
<v-btn
|
||||
text
|
||||
@click="snackbar = false"
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
{{ $t('close') }}
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
|
||||
<v-navigation-drawer
|
||||
app
|
||||
dark
|
||||
:color="darkMode ? '#003236' : '#005057'"
|
||||
fixed
|
||||
width="260"
|
||||
v-model="drawer"
|
||||
mobile-breakpoint="960"
|
||||
v-if="$route.path.startsWith('/project/')"
|
||||
app
|
||||
dark
|
||||
:color="darkMode ? '#003236' : '#005057'"
|
||||
fixed
|
||||
width="260"
|
||||
v-model="drawer"
|
||||
mobile-breakpoint="960"
|
||||
v-if="$route.path.startsWith('/project/')"
|
||||
>
|
||||
<v-menu bottom max-width="235" max-height="100%" v-if="project">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-list class="pa-0 overflow-y-auto">
|
||||
<v-list-item
|
||||
key="project"
|
||||
class="app__project-selector"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
key="project"
|
||||
class="app__project-selector"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-avatar
|
||||
:color="getProjectColor(project)"
|
||||
size="24"
|
||||
style="font-size: 13px; font-weight: bold;"
|
||||
:color="getProjectColor(project)"
|
||||
size="24"
|
||||
style="font-size: 13px; font-weight: bold;"
|
||||
>
|
||||
<span class="white--text">{{ getProjectInitials(project) }}</span>
|
||||
</v-avatar>
|
||||
@ -135,6 +135,7 @@
|
||||
<v-list-item-title class="app__project-selector-title">
|
||||
{{ project.name }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ userRole.role }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-icon>
|
||||
@ -145,16 +146,16 @@
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(item, i) in projects"
|
||||
:key="i"
|
||||
:to="`/project/${item.id}`"
|
||||
@click="selectProject(item.id)"
|
||||
v-for="(item, i) in projects"
|
||||
:key="i"
|
||||
:to="`/project/${item.id}`"
|
||||
@click="selectProject(item.id)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-avatar
|
||||
:color="getProjectColor(item)"
|
||||
size="24"
|
||||
style="font-size: 13px; font-weight: bold;"
|
||||
:color="getProjectColor(item)"
|
||||
size="24"
|
||||
style="font-size: 13px; font-weight: bold;"
|
||||
>
|
||||
<span class="white--text">{{ getProjectInitials(item) }}</span>
|
||||
</v-avatar>
|
||||
@ -162,7 +163,7 @@
|
||||
<v-list-item-content>{{ item.name }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="newProjectDialog = true" v-if="user.admin">
|
||||
<v-list-item @click="newProjectDialog = true" v-if="user.can_create_project">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-list-item-icon>
|
||||
@ -283,9 +284,9 @@
|
||||
></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
key="project"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
key="project"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
@ -341,14 +342,14 @@
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item key="password" @click="passwordDialog = true">-->
|
||||
<!-- <v-list-item-icon>-->
|
||||
<!-- <v-icon>mdi-lock</v-icon>-->
|
||||
<!-- </v-list-item-icon>-->
|
||||
<!-- <v-list-item-icon>-->
|
||||
<!-- <v-icon>mdi-lock</v-icon>-->
|
||||
<!-- </v-list-item-icon>-->
|
||||
|
||||
<!-- <v-list-item-content>-->
|
||||
<!-- Change Password-->
|
||||
<!-- </v-list-item-content>-->
|
||||
<!-- </v-list-item>-->
|
||||
<!-- <v-list-item-content>-->
|
||||
<!-- Change Password-->
|
||||
<!-- </v-list-item-content>-->
|
||||
<!-- </v-list-item>-->
|
||||
|
||||
<v-list-item key="sign_out" @click="signOut()">
|
||||
<v-list-item-icon>
|
||||
@ -365,23 +366,27 @@
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-main>
|
||||
<router-view :projectId="projectId" :userId="user ? user.id : null"></router-view>
|
||||
<router-view
|
||||
:projectId="projectId"
|
||||
:userPermissions="userRole.permissions"
|
||||
:userId="user ? user.id : null"
|
||||
></router-view>
|
||||
</v-main>
|
||||
|
||||
</v-app>
|
||||
<v-app v-else-if="state === 'loading'">
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
class="pa-0"
|
||||
fluid
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
class="pa-0"
|
||||
>
|
||||
<v-progress-circular
|
||||
:size="70"
|
||||
color="primary"
|
||||
indeterminate
|
||||
:size="70"
|
||||
color="primary"
|
||||
indeterminate
|
||||
></v-progress-circular>
|
||||
</v-container>
|
||||
</v-main>
|
||||
@ -389,12 +394,12 @@
|
||||
<v-app v-else-if="state === 'error'">
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
flex-column
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
class="pa-0 text-center"
|
||||
fluid
|
||||
flex-column
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
class="pa-0 text-center"
|
||||
>
|
||||
<v-alert text color="error" class="d-inline-block">
|
||||
<h3 class="headline">
|
||||
@ -421,6 +426,7 @@
|
||||
.v-dialog > .v-card > .v-card__title {
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
& * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -489,7 +495,7 @@
|
||||
}
|
||||
|
||||
& > td:first-child {
|
||||
//font-weight: bold !important;
|
||||
//font-weight: bold !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -560,6 +566,7 @@ export default {
|
||||
return {
|
||||
drawer: null,
|
||||
user: null,
|
||||
userRole: 0,
|
||||
systemInfo: null,
|
||||
state: 'loading',
|
||||
snackbar: false,
|
||||
@ -580,8 +587,8 @@ export default {
|
||||
watch: {
|
||||
async projects(val) {
|
||||
if (val.length === 0
|
||||
&& this.$route.path.startsWith('/project/')
|
||||
&& this.$route.path !== '/project/new') {
|
||||
&& this.$route.path.startsWith('/project/')
|
||||
&& this.$route.path !== '/project/new') {
|
||||
await this.$router.push({ path: '/project/new' });
|
||||
}
|
||||
},
|
||||
@ -783,8 +790,8 @@ export default {
|
||||
|
||||
// try to find project and switch to it if URL not pointing to any project
|
||||
if (this.$route.path === '/'
|
||||
|| this.$route.path === '/project'
|
||||
|| (this.$route.path.startsWith('/project/'))) {
|
||||
|| this.$route.path === '/project'
|
||||
|| (this.$route.path.startsWith('/project/'))) {
|
||||
await this.trySelectMostSuitableProject();
|
||||
}
|
||||
|
||||
@ -812,7 +819,7 @@ export default {
|
||||
}
|
||||
|
||||
if ((projectId == null || !this.projects.some((p) => p.id === projectId))
|
||||
&& localStorage.getItem('projectId')) {
|
||||
&& localStorage.getItem('projectId')) {
|
||||
projectId = parseInt(localStorage.getItem('projectId'), 10);
|
||||
}
|
||||
|
||||
@ -826,10 +833,17 @@ export default {
|
||||
},
|
||||
|
||||
async selectProject(projectId) {
|
||||
this.userRole = (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${projectId}/role`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
localStorage.setItem('projectId', projectId);
|
||||
if (this.projectId === projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$router.push({ path: `/project/${projectId}` });
|
||||
},
|
||||
|
||||
@ -861,7 +875,7 @@ export default {
|
||||
|
||||
getProjectColor(projectData) {
|
||||
const projectIndex = this.projects.length
|
||||
- this.projects.findIndex((p) => p.id === projectData.id);
|
||||
- this.projects.findIndex((p) => p.id === projectData.id);
|
||||
return PROJECT_COLORS[projectIndex % PROJECT_COLORS.length];
|
||||
},
|
||||
|
||||
|
@ -5,6 +5,7 @@ import YesNoDialog from '@/components/YesNoDialog.vue';
|
||||
import ObjectRefsDialog from '@/components/ObjectRefsDialog.vue';
|
||||
|
||||
import { getErrorMessage } from '@/lib/error';
|
||||
import { USER_PERMISSIONS } from '@/lib/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -16,11 +17,16 @@ export default {
|
||||
props: {
|
||||
projectId: Number,
|
||||
userId: Number,
|
||||
userPermissions: Number,
|
||||
},
|
||||
|
||||
data() {
|
||||
const allowActions = this.allowActions();
|
||||
|
||||
const headers = this.getHeaders().filter((header) => allowActions || header.value !== 'actions');
|
||||
|
||||
return {
|
||||
headers: this.getHeaders(),
|
||||
headers,
|
||||
items: null,
|
||||
|
||||
itemId: null,
|
||||
@ -29,6 +35,8 @@ export default {
|
||||
|
||||
itemRefs: null,
|
||||
itemRefsDialog: null,
|
||||
|
||||
USER_PERMISSIONS,
|
||||
};
|
||||
},
|
||||
|
||||
@ -38,6 +46,15 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
allowActions() {
|
||||
return this.can(USER_PERMISSIONS.manageProjectResources);
|
||||
},
|
||||
|
||||
can(permission) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return (this.userPermissions & permission) === permission;
|
||||
},
|
||||
|
||||
// eslint-disable-next-line no-empty-function
|
||||
async beforeLoadItems() {
|
||||
},
|
||||
|
@ -22,15 +22,22 @@
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
|
||||
<v-checkbox
|
||||
v-model="item.admin"
|
||||
:label="$t('administrator')"
|
||||
></v-checkbox>
|
||||
<v-select
|
||||
v-model="item.role"
|
||||
:label="$t('role')"
|
||||
:items="USER_ROLES"
|
||||
item-value="slug"
|
||||
item-text="title"
|
||||
:rules="[v => !!v || $t('user_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
</v-form>
|
||||
</template>
|
||||
<script>
|
||||
import ItemFormBase from '@/components/ItemFormBase';
|
||||
import axios from 'axios';
|
||||
import { USER_ROLES } from '@/lib/constants';
|
||||
|
||||
export default {
|
||||
mixins: [ItemFormBase],
|
||||
@ -40,6 +47,7 @@ export default {
|
||||
users: null,
|
||||
userId: null,
|
||||
teamMembers: null,
|
||||
USER_ROLES,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -15,3 +15,24 @@ export const TEMPLATE_TYPE_ACTION_TITLES = {
|
||||
build: 'Build',
|
||||
deploy: 'Deploy',
|
||||
};
|
||||
|
||||
export const USER_PERMISSIONS = {
|
||||
runProjectTasks: 1,
|
||||
updateProject: 2,
|
||||
manageProjectResources: 4,
|
||||
manageProjectUsers: 8,
|
||||
};
|
||||
|
||||
export const USER_ROLES = [{
|
||||
slug: 'owner',
|
||||
title: 'Owner',
|
||||
}, {
|
||||
slug: 'manager',
|
||||
title: 'Manager',
|
||||
}, {
|
||||
slug: 'task_runner',
|
||||
title: 'Task Runner',
|
||||
}, {
|
||||
slug: 'guest',
|
||||
title: 'Guest',
|
||||
}];
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||
<div v-if="items">
|
||||
<v-toolbar flat >
|
||||
<v-toolbar flat>
|
||||
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||
<v-toolbar-title>{{ $t('dashboard') }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
@ -8,7 +8,12 @@
|
||||
<v-tabs centered>
|
||||
<v-tab key="history" :to="`/project/${projectId}/history`">{{ $t('history') }}</v-tab>
|
||||
<v-tab key="activity" :to="`/project/${projectId}/activity`">{{ $t('activity') }}</v-tab>
|
||||
<v-tab key="settings" :to="`/project/${projectId}/settings`">{{ $t('settings') }}</v-tab>
|
||||
<v-tab
|
||||
v-if="can(USER_PERMISSIONS.updateProject)"
|
||||
key="settings"
|
||||
:to="`/project/${projectId}/settings`"
|
||||
>{{ $t('settings') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</div>
|
||||
</v-toolbar>
|
||||
@ -27,8 +32,14 @@
|
||||
</template>
|
||||
<script>
|
||||
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||
import { USER_PERMISSIONS } from '@/lib/constants';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
USER_PERMISSIONS() {
|
||||
return USER_PERMISSIONS;
|
||||
},
|
||||
},
|
||||
mixins: [ItemListPageBase],
|
||||
|
||||
methods: {
|
||||
|
@ -40,6 +40,7 @@
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="editItem('new')"
|
||||
v-if="can(USER_PERMISSIONS.manageProjectResources)"
|
||||
>{{ $t('newEnvironment') }}
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
@ -23,7 +23,12 @@
|
||||
<v-tabs centered>
|
||||
<v-tab key="history" :to="`/project/${projectId}/history`">{{ $t('history') }}</v-tab>
|
||||
<v-tab key="activity" :to="`/project/${projectId}/activity`">{{ $t('activity') }}</v-tab>
|
||||
<v-tab key="settings" :to="`/project/${projectId}/settings`">{{ $t('settings') }}</v-tab>
|
||||
<v-tab
|
||||
v-if="can(USER_PERMISSIONS.updateProject)"
|
||||
key="settings"
|
||||
:to="`/project/${projectId}/settings`"
|
||||
>{{ $t('settings') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</div>
|
||||
</v-toolbar>
|
||||
|
@ -40,6 +40,7 @@
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="editItem('new')"
|
||||
v-if="can(USER_PERMISSIONS.manageProjectResources)"
|
||||
>{{ $t('newInventory') }}</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
|
@ -41,6 +41,7 @@
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="editItem('new')"
|
||||
v-if="can(USER_PERMISSIONS.manageProjectResources)"
|
||||
>{{ $t('newKey') }}</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
|
@ -39,6 +39,7 @@
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="editItem('new')"
|
||||
v-if="can(USER_PERMISSIONS.manageProjectResources)"
|
||||
>{{ $t('newRepository') }}</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
|
@ -32,7 +32,9 @@
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="editItem('new')"
|
||||
>{{ $t('newTeamMember') }}</v-btn>
|
||||
v-if="can(USER_PERMISSIONS.manageProjectUsers)"
|
||||
>{{ $t('newTeamMember') }}
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
@ -45,10 +47,11 @@
|
||||
<template v-slot:item.role="{ item }">
|
||||
<v-select
|
||||
v-model="item.role"
|
||||
:items="roles"
|
||||
:items="USER_ROLES"
|
||||
item-value="slug"
|
||||
item-text="title"
|
||||
:style="{width: '200px'}"
|
||||
:disabled="!can(USER_PERMISSIONS.manageProjectUsers)"
|
||||
@change="updateProjectUser(item)"
|
||||
/>
|
||||
</template>
|
||||
@ -58,6 +61,7 @@
|
||||
icon
|
||||
:disabled="!isUserAdmin()"
|
||||
@click="askDeleteItem(item.id)"
|
||||
v-if="can(USER_PERMISSIONS.manageProjectUsers)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
@ -70,25 +74,14 @@
|
||||
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||
import TeamMemberForm from '@/components/TeamMemberForm.vue';
|
||||
import axios from 'axios';
|
||||
import { USER_PERMISSIONS, USER_ROLES } from '@/lib/constants';
|
||||
|
||||
export default {
|
||||
components: { TeamMemberForm },
|
||||
mixins: [ItemListPageBase],
|
||||
data() {
|
||||
return {
|
||||
roles: [{
|
||||
slug: 'owner',
|
||||
title: 'Owner',
|
||||
}, {
|
||||
slug: 'manager',
|
||||
title: 'Manager',
|
||||
}, {
|
||||
slug: 'task_runner',
|
||||
title: 'Task Runner',
|
||||
}, {
|
||||
slug: 'guest',
|
||||
title: 'Guest',
|
||||
}],
|
||||
USER_ROLES,
|
||||
};
|
||||
},
|
||||
|
||||
@ -103,6 +96,10 @@ export default {
|
||||
await this.loadItems();
|
||||
},
|
||||
|
||||
allowActions() {
|
||||
return this.can(USER_PERMISSIONS.manageProjectUsers);
|
||||
},
|
||||
|
||||
getHeaders() {
|
||||
return [
|
||||
{
|
||||
@ -129,6 +126,7 @@ export default {
|
||||
sortable: false,
|
||||
}];
|
||||
},
|
||||
|
||||
getSingleItemUrl() {
|
||||
return `/api/project/${this.projectId}/users/${this.itemId}`;
|
||||
},
|
||||
|
@ -78,6 +78,7 @@
|
||||
color="primary"
|
||||
@click="editItem('new')"
|
||||
class="mr-1"
|
||||
v-if="can(USER_PERMISSIONS.manageProjectResources)"
|
||||
>{{ $t('newTemplate') }}
|
||||
</v-btn>
|
||||
|
||||
@ -97,7 +98,12 @@
|
||||
>{{ view.title }}
|
||||
</v-tab>
|
||||
|
||||
<v-btn icon class="mt-2 ml-4" @click="editViewsDialog = true">
|
||||
<v-btn
|
||||
icon
|
||||
class="mt-2 ml-4"
|
||||
@click="editViewsDialog = true"
|
||||
v-if="can(USER_PERMISSIONS.manageProjectResources)"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</v-tabs>
|
||||
@ -212,7 +218,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
@import '~vuetify/src/styles/settings/_variables';
|
||||
@import '~vuetify/src/styles/settings/variables';
|
||||
|
||||
.templates-table .text-start:first-child {
|
||||
padding-right: 0 !important;
|
||||
|
Loading…
Reference in New Issue
Block a user