feat: enforce user roles
enforces user admin role requirement for - creating / deleting / setting role for organization users - creating / deleting / setting role for project users - updating project name - deleting project hides action elements based on role for - admin console - team settings if team is only visible through project membership - add project tile if not team admin - project name text editor if not team / project admin - add redirect from team page if settings only visible through project membership - add redirect from admin console if not org admin role enforcement is handled on the api side through a custom GraphQL directive `hasRole`. on the client side, role information is fetched in the TopNavbar's `me` query and stored in the `UserContext`. there is a custom hook, `useCurrentUser`, that provides a user object with two functions, `isVisibile` & `isAdmin` which is used to check roles in order to render/hide relevant UI elements.
This commit is contained in:
		
				
					committed by
					
						
						Jordan Knott
					
				
			
			
				
	
			
			
			
						parent
						
							5dbdc20b36
						
					
				
				
					commit
					e64f6f8569
				
			
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -2,10 +2,13 @@ package graph
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/99designs/gqlgen/graphql"
 | 
			
		||||
	"github.com/99designs/gqlgen/graphql/handler"
 | 
			
		||||
	"github.com/99designs/gqlgen/graphql/handler/extension"
 | 
			
		||||
	"github.com/99designs/gqlgen/graphql/handler/lru"
 | 
			
		||||
@@ -15,16 +18,71 @@ import (
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/auth"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/config"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/db"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/vektah/gqlparser/v2/gqlerror"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// NewHandler returns a new graphql endpoint handler.
 | 
			
		||||
func NewHandler(config config.AppConfig, repo db.Repository) http.Handler {
 | 
			
		||||
	srv := handler.New(NewExecutableSchema(Config{
 | 
			
		||||
	c := Config{
 | 
			
		||||
		Resolvers: &Resolver{
 | 
			
		||||
			Config:     config,
 | 
			
		||||
			Repository: repo,
 | 
			
		||||
		},
 | 
			
		||||
	}))
 | 
			
		||||
	}
 | 
			
		||||
	c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
 | 
			
		||||
		role, ok := GetUserRole(ctx)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return nil, errors.New("user ID is missing")
 | 
			
		||||
		}
 | 
			
		||||
		if role == "admin" {
 | 
			
		||||
			return next(ctx)
 | 
			
		||||
		} else if level == ActionLevelOrg {
 | 
			
		||||
			return nil, errors.New("must be an org admin")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var subjectID uuid.UUID
 | 
			
		||||
		in := graphql.GetResolverContext(ctx).Args["input"]
 | 
			
		||||
		if typeArg == ObjectTypeProject || typeArg == ObjectTypeTeam {
 | 
			
		||||
			val := reflect.ValueOf(in) // could be any underlying type
 | 
			
		||||
			fieldName := "ProjectID"
 | 
			
		||||
			if typeArg == ObjectTypeTeam {
 | 
			
		||||
				fieldName = "TeamID"
 | 
			
		||||
			}
 | 
			
		||||
			subjectID, ok = val.FieldByName(fieldName).Interface().(uuid.UUID)
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return nil, errors.New("error while casting subject uuid")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if level == ActionLevelProject {
 | 
			
		||||
			roles, err := GetProjectRoles(ctx, repo, subjectID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			if roles.TeamRole == "admin" || roles.ProjectRole == "admin" {
 | 
			
		||||
				log.WithFields(log.Fields{"teamRole": roles.TeamRole, "projectRole": roles.ProjectRole}).Info("is team or project role")
 | 
			
		||||
				return next(ctx)
 | 
			
		||||
			}
 | 
			
		||||
			return nil, errors.New("must be a team or project admin")
 | 
			
		||||
		} else if level == ActionLevelTeam {
 | 
			
		||||
			userID, ok := GetUserID(ctx)
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return nil, errors.New("user id is missing")
 | 
			
		||||
			}
 | 
			
		||||
			role, err := repo.GetTeamRoleForUserID(ctx, db.GetTeamRoleForUserIDParams{UserID: userID, TeamID: subjectID})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			if role.RoleCode == "admin" {
 | 
			
		||||
				return next(ctx)
 | 
			
		||||
			}
 | 
			
		||||
			return nil, errors.New("must be a team admin")
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		return nil, errors.New("invalid path")
 | 
			
		||||
	}
 | 
			
		||||
	srv := handler.New(NewExecutableSchema(c))
 | 
			
		||||
	srv.AddTransport(transport.Websocket{
 | 
			
		||||
		KeepAlivePingInterval: 10 * time.Second,
 | 
			
		||||
	})
 | 
			
		||||
@@ -55,8 +113,80 @@ func GetUserID(ctx context.Context) (uuid.UUID, bool) {
 | 
			
		||||
	userID, ok := ctx.Value("userID").(uuid.UUID)
 | 
			
		||||
	return userID, ok
 | 
			
		||||
}
 | 
			
		||||
func GetUserRole(ctx context.Context) (auth.Role, bool) {
 | 
			
		||||
	role, ok := ctx.Value("org_role").(auth.Role)
 | 
			
		||||
	return role, ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetUser(ctx context.Context) (uuid.UUID, auth.Role, bool) {
 | 
			
		||||
	userID, userOK := GetUserID(ctx)
 | 
			
		||||
	role, roleOK := GetUserRole(ctx)
 | 
			
		||||
	return userID, role, userOK && roleOK
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetRestrictedMode(ctx context.Context) (auth.RestrictedMode, bool) {
 | 
			
		||||
	restricted, ok := ctx.Value("restricted_mode").(auth.RestrictedMode)
 | 
			
		||||
	return restricted, ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetProjectRoles(ctx context.Context, r db.Repository, projectID uuid.UUID) (db.GetUserRolesForProjectRow, error) {
 | 
			
		||||
	userID, ok := GetUserID(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return db.GetUserRolesForProjectRow{}, errors.New("user ID is not found")
 | 
			
		||||
	}
 | 
			
		||||
	return r.GetUserRolesForProject(ctx, db.GetUserRolesForProjectParams{UserID: userID, ProjectID: projectID})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ConvertToRoleCode(r string) RoleCode {
 | 
			
		||||
	if r == RoleCodeAdmin.String() {
 | 
			
		||||
		return RoleCodeAdmin
 | 
			
		||||
	}
 | 
			
		||||
	if r == RoleCodeMember.String() {
 | 
			
		||||
		return RoleCodeMember
 | 
			
		||||
	}
 | 
			
		||||
	return RoleCodeObserver
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RequireTeamAdmin(ctx context.Context, r db.Repository, teamID uuid.UUID) error {
 | 
			
		||||
	userID, role, ok := GetUser(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return errors.New("internal: user id is not set")
 | 
			
		||||
	}
 | 
			
		||||
	teamRole, err := r.GetTeamRoleForUserID(ctx, db.GetTeamRoleForUserIDParams{UserID: userID, TeamID: teamID})
 | 
			
		||||
	isAdmin := role == auth.RoleAdmin
 | 
			
		||||
	isTeamAdmin := err == nil && ConvertToRoleCode(teamRole.RoleCode) == RoleCodeAdmin
 | 
			
		||||
	if !(isAdmin || isTeamAdmin) {
 | 
			
		||||
		return &gqlerror.Error{
 | 
			
		||||
			Message: "organization or team admin role required",
 | 
			
		||||
			Extensions: map[string]interface{}{
 | 
			
		||||
				"code": "2-400",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RequireProjectOrTeamAdmin(ctx context.Context, r db.Repository, projectID uuid.UUID) error {
 | 
			
		||||
	role, ok := GetUserRole(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return errors.New("user ID is not set")
 | 
			
		||||
	}
 | 
			
		||||
	if role == auth.RoleAdmin {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	roles, err := GetProjectRoles(ctx, r, projectID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if !(roles.ProjectRole == "admin" || roles.TeamRole == "admin") {
 | 
			
		||||
		return &gqlerror.Error{
 | 
			
		||||
			Message: "You must be a team or project admin",
 | 
			
		||||
			Extensions: map[string]interface{}{
 | 
			
		||||
				"code": "4-400",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,15 +8,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*OwnedList, error) {
 | 
			
		||||
	ownedTeams, err := r.GetOwnedTeamsForUserID(ctx, user.UserID)
 | 
			
		||||
	if err != sql.ErrNoRows && err != nil {
 | 
			
		||||
		return &OwnedList{}, err
 | 
			
		||||
	}
 | 
			
		||||
	ownedProjects, err := r.GetOwnedProjectsForUserID(ctx, user.UserID)
 | 
			
		||||
	if err != sql.ErrNoRows && err != nil {
 | 
			
		||||
		return &OwnedList{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return &OwnedList{Teams: ownedTeams, Projects: ownedProjects}, nil
 | 
			
		||||
	return &OwnedList{}, nil
 | 
			
		||||
}
 | 
			
		||||
func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*MemberList, error) {
 | 
			
		||||
	projectMemberIDs, err := r.GetMemberProjectIDsForUserID(ctx, user.UserID)
 | 
			
		||||
 
 | 
			
		||||
@@ -152,7 +152,7 @@ type DeleteUserAccountPayload struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FindProject struct {
 | 
			
		||||
	ProjectID string `json:"projectId"`
 | 
			
		||||
	ProjectID uuid.UUID `json:"projectID"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FindTask struct {
 | 
			
		||||
@@ -171,6 +171,12 @@ type LogoutUser struct {
 | 
			
		||||
	UserID string `json:"userID"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MePayload struct {
 | 
			
		||||
	User         *db.UserAccount `json:"user"`
 | 
			
		||||
	TeamRoles    []TeamRole      `json:"teamRoles"`
 | 
			
		||||
	ProjectRoles []ProjectRole   `json:"projectRoles"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Member struct {
 | 
			
		||||
	ID          uuid.UUID    `json:"id"`
 | 
			
		||||
	Role        *db.Role     `json:"role"`
 | 
			
		||||
@@ -255,6 +261,11 @@ type ProfileIcon struct {
 | 
			
		||||
	BgColor  *string `json:"bgColor"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ProjectRole struct {
 | 
			
		||||
	ProjectID uuid.UUID `json:"projectID"`
 | 
			
		||||
	RoleCode  RoleCode  `json:"roleCode"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ProjectsFilter struct {
 | 
			
		||||
	TeamID *uuid.UUID `json:"teamID"`
 | 
			
		||||
}
 | 
			
		||||
@@ -263,17 +274,6 @@ type RemoveTaskLabelInput struct {
 | 
			
		||||
	TaskLabelID uuid.UUID `json:"taskLabelID"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SetProjectOwner struct {
 | 
			
		||||
	ProjectID uuid.UUID `json:"projectID"`
 | 
			
		||||
	OwnerID   uuid.UUID `json:"ownerID"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SetProjectOwnerPayload struct {
 | 
			
		||||
	Ok        bool    `json:"ok"`
 | 
			
		||||
	PrevOwner *Member `json:"prevOwner"`
 | 
			
		||||
	NewOwner  *Member `json:"newOwner"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SetTaskChecklistItemComplete struct {
 | 
			
		||||
	TaskChecklistItemID uuid.UUID `json:"taskChecklistItemID"`
 | 
			
		||||
	Complete            bool      `json:"complete"`
 | 
			
		||||
@@ -284,21 +284,15 @@ type SetTaskComplete struct {
 | 
			
		||||
	Complete bool      `json:"complete"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SetTeamOwner struct {
 | 
			
		||||
	TeamID uuid.UUID `json:"teamID"`
 | 
			
		||||
	UserID uuid.UUID `json:"userID"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SetTeamOwnerPayload struct {
 | 
			
		||||
	Ok        bool    `json:"ok"`
 | 
			
		||||
	PrevOwner *Member `json:"prevOwner"`
 | 
			
		||||
	NewOwner  *Member `json:"newOwner"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TaskBadges struct {
 | 
			
		||||
	Checklist *ChecklistBadge `json:"checklist"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TeamRole struct {
 | 
			
		||||
	TeamID   uuid.UUID `json:"teamID"`
 | 
			
		||||
	RoleCode RoleCode  `json:"roleCode"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ToggleTaskLabelInput struct {
 | 
			
		||||
	TaskID         uuid.UUID `json:"taskID"`
 | 
			
		||||
	ProjectLabelID uuid.UUID `json:"projectLabelID"`
 | 
			
		||||
@@ -409,8 +403,9 @@ type UpdateTeamMemberRole struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateTeamMemberRolePayload struct {
 | 
			
		||||
	Ok     bool    `json:"ok"`
 | 
			
		||||
	Member *Member `json:"member"`
 | 
			
		||||
	Ok     bool      `json:"ok"`
 | 
			
		||||
	TeamID uuid.UUID `json:"teamID"`
 | 
			
		||||
	Member *Member   `json:"member"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateUserPassword struct {
 | 
			
		||||
@@ -432,6 +427,94 @@ type UpdateUserRolePayload struct {
 | 
			
		||||
	User *db.UserAccount `json:"user"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ActionLevel string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ActionLevelOrg     ActionLevel = "ORG"
 | 
			
		||||
	ActionLevelTeam    ActionLevel = "TEAM"
 | 
			
		||||
	ActionLevelProject ActionLevel = "PROJECT"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var AllActionLevel = []ActionLevel{
 | 
			
		||||
	ActionLevelOrg,
 | 
			
		||||
	ActionLevelTeam,
 | 
			
		||||
	ActionLevelProject,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ActionLevel) IsValid() bool {
 | 
			
		||||
	switch e {
 | 
			
		||||
	case ActionLevelOrg, ActionLevelTeam, ActionLevelProject:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ActionLevel) String() string {
 | 
			
		||||
	return string(e)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *ActionLevel) UnmarshalGQL(v interface{}) error {
 | 
			
		||||
	str, ok := v.(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("enums must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*e = ActionLevel(str)
 | 
			
		||||
	if !e.IsValid() {
 | 
			
		||||
		return fmt.Errorf("%s is not a valid ActionLevel", str)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ActionLevel) MarshalGQL(w io.Writer) {
 | 
			
		||||
	fmt.Fprint(w, strconv.Quote(e.String()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ObjectType string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ObjectTypeOrg     ObjectType = "ORG"
 | 
			
		||||
	ObjectTypeTeam    ObjectType = "TEAM"
 | 
			
		||||
	ObjectTypeProject ObjectType = "PROJECT"
 | 
			
		||||
	ObjectTypeTask    ObjectType = "TASK"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var AllObjectType = []ObjectType{
 | 
			
		||||
	ObjectTypeOrg,
 | 
			
		||||
	ObjectTypeTeam,
 | 
			
		||||
	ObjectTypeProject,
 | 
			
		||||
	ObjectTypeTask,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ObjectType) IsValid() bool {
 | 
			
		||||
	switch e {
 | 
			
		||||
	case ObjectTypeOrg, ObjectTypeTeam, ObjectTypeProject, ObjectTypeTask:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ObjectType) String() string {
 | 
			
		||||
	return string(e)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *ObjectType) UnmarshalGQL(v interface{}) error {
 | 
			
		||||
	str, ok := v.(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("enums must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*e = ObjectType(str)
 | 
			
		||||
	if !e.IsValid() {
 | 
			
		||||
		return fmt.Errorf("%s is not a valid ObjectType", str)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ObjectType) MarshalGQL(w io.Writer) {
 | 
			
		||||
	fmt.Fprint(w, strconv.Quote(e.String()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RoleCode string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -476,3 +559,44 @@ func (e *RoleCode) UnmarshalGQL(v interface{}) error {
 | 
			
		||||
func (e RoleCode) MarshalGQL(w io.Writer) {
 | 
			
		||||
	fmt.Fprint(w, strconv.Quote(e.String()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RoleLevel string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	RoleLevelAdmin  RoleLevel = "ADMIN"
 | 
			
		||||
	RoleLevelMember RoleLevel = "MEMBER"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var AllRoleLevel = []RoleLevel{
 | 
			
		||||
	RoleLevelAdmin,
 | 
			
		||||
	RoleLevelMember,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e RoleLevel) IsValid() bool {
 | 
			
		||||
	switch e {
 | 
			
		||||
	case RoleLevelAdmin, RoleLevelMember:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e RoleLevel) String() string {
 | 
			
		||||
	return string(e)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *RoleLevel) UnmarshalGQL(v interface{}) error {
 | 
			
		||||
	str, ok := v.(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("enums must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*e = RoleLevel(str)
 | 
			
		||||
	if !e.IsValid() {
 | 
			
		||||
		return fmt.Errorf("%s is not a valid RoleLevel", str)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e RoleLevel) MarshalGQL(w io.Writer) {
 | 
			
		||||
	fmt.Fprint(w, strconv.Quote(e.String()))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -97,7 +97,6 @@ type Project {
 | 
			
		||||
  createdAt: Time!
 | 
			
		||||
  name: String!
 | 
			
		||||
  team: Team!
 | 
			
		||||
  owner: Member!
 | 
			
		||||
  taskGroups: [TaskGroup!]!
 | 
			
		||||
  members: [Member!]!
 | 
			
		||||
  labels: [ProjectLabel!]!
 | 
			
		||||
@@ -157,6 +156,26 @@ type TaskChecklist {
 | 
			
		||||
  items: [TaskChecklistItem!]!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum RoleLevel {
 | 
			
		||||
  ADMIN
 | 
			
		||||
  MEMBER
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ActionLevel {
 | 
			
		||||
  ORG
 | 
			
		||||
  TEAM
 | 
			
		||||
  PROJECT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ObjectType {
 | 
			
		||||
  ORG
 | 
			
		||||
  TEAM
 | 
			
		||||
  PROJECT
 | 
			
		||||
  TASK
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
 | 
			
		||||
 | 
			
		||||
type Query {
 | 
			
		||||
  organizations: [Organization!]!
 | 
			
		||||
  users: [UserAccount!]!
 | 
			
		||||
@@ -168,11 +187,27 @@ type Query {
 | 
			
		||||
  teams: [Team!]!
 | 
			
		||||
  labelColors: [LabelColor!]!
 | 
			
		||||
  taskGroups: [TaskGroup!]!
 | 
			
		||||
  me: UserAccount!
 | 
			
		||||
  me: MePayload! 
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Mutation
 | 
			
		||||
 | 
			
		||||
type TeamRole {
 | 
			
		||||
  teamID: UUID!
 | 
			
		||||
  roleCode: RoleCode!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ProjectRole {
 | 
			
		||||
  projectID: UUID!
 | 
			
		||||
  roleCode: RoleCode!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MePayload {
 | 
			
		||||
  user: UserAccount!
 | 
			
		||||
  teamRoles: [TeamRole!]!
 | 
			
		||||
  projectRoles: [ProjectRole!]!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input ProjectsFilter {
 | 
			
		||||
  teamID: UUID
 | 
			
		||||
}
 | 
			
		||||
@@ -182,7 +217,7 @@ input FindUser {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input FindProject {
 | 
			
		||||
  projectId: String!
 | 
			
		||||
  projectID: UUID!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input FindTask {
 | 
			
		||||
@@ -194,9 +229,11 @@ input FindTeam {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createProject(input: NewProject!): Project!
 | 
			
		||||
  deleteProject(input: DeleteProject!): DeleteProjectPayload!
 | 
			
		||||
  updateProjectName(input: UpdateProjectName): Project!
 | 
			
		||||
  createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
  deleteProject(input: DeleteProject!):
 | 
			
		||||
    DeleteProjectPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectName(input: UpdateProjectName):
 | 
			
		||||
    Project! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewProject {
 | 
			
		||||
@@ -221,11 +258,16 @@ type DeleteProjectPayload {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createProjectLabel(input: NewProjectLabel!): ProjectLabel!
 | 
			
		||||
  deleteProjectLabel(input: DeleteProjectLabel!): ProjectLabel!
 | 
			
		||||
  updateProjectLabel(input: UpdateProjectLabel!): ProjectLabel!
 | 
			
		||||
  updateProjectLabelName(input: UpdateProjectLabelName!): ProjectLabel!
 | 
			
		||||
  updateProjectLabelColor(input: UpdateProjectLabelColor!): ProjectLabel!
 | 
			
		||||
  createProjectLabel(input: NewProjectLabel!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  deleteProjectLabel(input: DeleteProjectLabel!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectLabel(input: UpdateProjectLabel!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectLabelName(input: UpdateProjectLabelName!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectLabelColor(input: UpdateProjectLabelColor!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewProjectLabel {
 | 
			
		||||
@@ -255,10 +297,12 @@ input UpdateProjectLabelColor {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createProjectMember(input: CreateProjectMember!): CreateProjectMemberPayload!
 | 
			
		||||
  deleteProjectMember(input: DeleteProjectMember!): DeleteProjectMemberPayload!
 | 
			
		||||
  updateProjectMemberRole(input: UpdateProjectMemberRole!): UpdateProjectMemberRolePayload!
 | 
			
		||||
  setProjectOwner(input: SetProjectOwner!): SetProjectOwnerPayload!
 | 
			
		||||
  createProjectMember(input: CreateProjectMember!):
 | 
			
		||||
    CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  deleteProjectMember(input: DeleteProjectMember!):
 | 
			
		||||
    DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectMemberRole(input: UpdateProjectMemberRole!):
 | 
			
		||||
    UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input CreateProjectMember {
 | 
			
		||||
@@ -293,16 +337,6 @@ type UpdateProjectMemberRolePayload {
 | 
			
		||||
  member: Member!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input SetProjectOwner {
 | 
			
		||||
  projectID: UUID!
 | 
			
		||||
  ownerID: UUID!
 | 
			
		||||
}
 | 
			
		||||
type SetProjectOwnerPayload {
 | 
			
		||||
  ok: Boolean!
 | 
			
		||||
  prevOwner: Member!
 | 
			
		||||
  newOwner: Member!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createTask(input: NewTask!): Task!
 | 
			
		||||
  deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
 | 
			
		||||
@@ -506,8 +540,10 @@ extend type Mutation {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  deleteTeam(input: DeleteTeam!): DeleteTeamPayload!
 | 
			
		||||
  createTeam(input: NewTeam!): Team!
 | 
			
		||||
  deleteTeam(input: DeleteTeam!):
 | 
			
		||||
    DeleteTeamPayload! @hasRole(roles:[ ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
  createTeam(input: NewTeam!):
 | 
			
		||||
    Team! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewTeam {
 | 
			
		||||
@@ -526,10 +562,11 @@ type DeleteTeamPayload {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  setTeamOwner(input: SetTeamOwner!): SetTeamOwnerPayload!
 | 
			
		||||
  createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload!
 | 
			
		||||
  updateTeamMemberRole(input: UpdateTeamMemberRole!): UpdateTeamMemberRolePayload!
 | 
			
		||||
  deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload!
 | 
			
		||||
  createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
  updateTeamMemberRole(input: UpdateTeamMemberRole!):
 | 
			
		||||
    UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
  deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input DeleteTeamMember {
 | 
			
		||||
@@ -562,29 +599,23 @@ input UpdateTeamMemberRole {
 | 
			
		||||
 | 
			
		||||
type UpdateTeamMemberRolePayload {
 | 
			
		||||
  ok: Boolean!
 | 
			
		||||
  member: Member!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input SetTeamOwner {
 | 
			
		||||
  teamID: UUID!
 | 
			
		||||
  userID: UUID!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SetTeamOwnerPayload {
 | 
			
		||||
  ok: Boolean!
 | 
			
		||||
  prevOwner: Member!
 | 
			
		||||
  newOwner: Member!
 | 
			
		||||
  member: Member!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createRefreshToken(input: NewRefreshToken!): RefreshToken!
 | 
			
		||||
  createUserAccount(input: NewUserAccount!): UserAccount!
 | 
			
		||||
  deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload!
 | 
			
		||||
  createUserAccount(input: NewUserAccount!):
 | 
			
		||||
    UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
  deleteUserAccount(input: DeleteUserAccount!):
 | 
			
		||||
    DeleteUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
 | 
			
		||||
  logoutUser(input: LogoutUser!): Boolean!
 | 
			
		||||
  clearProfileAvatar:  UserAccount!
 | 
			
		||||
 | 
			
		||||
  updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
 | 
			
		||||
  updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
 | 
			
		||||
  updateUserRole(input: UpdateUserRole!):
 | 
			
		||||
   UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input UpdateUserPassword {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/auth"
 | 
			
		||||
	"github.com/jordanknott/taskcafe/internal/db"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/vektah/gqlparser/v2/gqlerror"
 | 
			
		||||
@@ -23,7 +24,8 @@ func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.U
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*db.Project, error) {
 | 
			
		||||
	createdAt := time.Now().UTC()
 | 
			
		||||
	project, err := r.Repository.CreateProject(ctx, db.CreateProjectParams{input.UserID, input.TeamID, createdAt, input.Name})
 | 
			
		||||
	log.WithFields(log.Fields{"userID": input.UserID, "name": input.Name, "teamID": input.TeamID}).Info("creating new project")
 | 
			
		||||
	project, err := r.Repository.CreateProject(ctx, db.CreateProjectParams{input.TeamID, createdAt, input.Name})
 | 
			
		||||
	return &project, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -146,9 +148,6 @@ func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input Delete
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input UpdateProjectMemberRole) (*UpdateProjectMemberRolePayload, error) {
 | 
			
		||||
	if input.RoleCode == RoleCodeOwner {
 | 
			
		||||
		return &UpdateProjectMemberRolePayload{Ok: false}, errors.New("can not set project owner through this mutation")
 | 
			
		||||
	}
 | 
			
		||||
	user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("get user account")
 | 
			
		||||
@@ -179,64 +178,6 @@ func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input Up
 | 
			
		||||
	return &UpdateProjectMemberRolePayload{Ok: true, Member: &member}, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) SetProjectOwner(ctx context.Context, input SetProjectOwner) (*SetProjectOwnerPayload, error) {
 | 
			
		||||
	project, err := r.Repository.GetProjectByID(ctx, input.ProjectID)
 | 
			
		||||
	if project.Owner == input.OwnerID {
 | 
			
		||||
		return &SetProjectOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
 | 
			
		||||
	}
 | 
			
		||||
	_, err = r.Repository.SetProjectOwner(ctx, db.SetProjectOwnerParams{Owner: input.OwnerID, ProjectID: input.ProjectID})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &SetProjectOwnerPayload{Ok: false}, err
 | 
			
		||||
	}
 | 
			
		||||
	err = r.Repository.DeleteProjectMember(ctx, db.DeleteProjectMemberParams{ProjectID: input.ProjectID, UserID: input.OwnerID})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &SetProjectOwnerPayload{Ok: false}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addedAt := time.Now().UTC()
 | 
			
		||||
	_, err = r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: input.ProjectID,
 | 
			
		||||
		UserID: project.Owner, RoleCode: RoleCodeAdmin.String(), AddedAt: addedAt})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &SetProjectOwnerPayload{Ok: false}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oldUser, err := r.Repository.GetUserAccountByID(ctx, project.Owner)
 | 
			
		||||
	var url *string
 | 
			
		||||
	if oldUser.ProfileAvatarUrl.Valid {
 | 
			
		||||
		url = &oldUser.ProfileAvatarUrl.String
 | 
			
		||||
	}
 | 
			
		||||
	profileIcon := &ProfileIcon{url, &oldUser.Initials, &oldUser.ProfileBgColor}
 | 
			
		||||
	oldUserRole := db.Role{Code: "admin", Name: "Admin"}
 | 
			
		||||
	oldMember := &Member{
 | 
			
		||||
		ID:          oldUser.UserID,
 | 
			
		||||
		Username:    oldUser.Username,
 | 
			
		||||
		FullName:    oldUser.FullName,
 | 
			
		||||
		ProfileIcon: profileIcon,
 | 
			
		||||
		Role:        &oldUserRole,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newUser, err := r.Repository.GetUserAccountByID(ctx, input.OwnerID)
 | 
			
		||||
 | 
			
		||||
	if newUser.ProfileAvatarUrl.Valid {
 | 
			
		||||
		url = &newUser.ProfileAvatarUrl.String
 | 
			
		||||
	}
 | 
			
		||||
	profileIcon = &ProfileIcon{url, &newUser.Initials, &newUser.ProfileBgColor}
 | 
			
		||||
	newUserRole := db.Role{Code: "owner", Name: "Owner"}
 | 
			
		||||
	newMember := &Member{
 | 
			
		||||
		ID:          newUser.UserID,
 | 
			
		||||
		Username:    newUser.Username,
 | 
			
		||||
		FullName:    newUser.FullName,
 | 
			
		||||
		ProfileIcon: profileIcon,
 | 
			
		||||
		Role:        &newUserRole,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &SetProjectOwnerPayload{
 | 
			
		||||
		Ok:        true,
 | 
			
		||||
		PrevOwner: oldMember,
 | 
			
		||||
		NewOwner:  newMember,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) {
 | 
			
		||||
	taskGroupID, err := uuid.Parse(input.TaskGroupID)
 | 
			
		||||
	createdAt := time.Now().UTC()
 | 
			
		||||
@@ -574,71 +515,21 @@ func (r *mutationResolver) DeleteTeam(ctx context.Context, input DeleteTeam) (*D
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*db.Team, error) {
 | 
			
		||||
	userID, ok := GetUserID(ctx)
 | 
			
		||||
	_, role, ok := GetUser(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return &db.Team{}, fmt.Errorf("internal server error")
 | 
			
		||||
		return &db.Team{}, nil
 | 
			
		||||
	}
 | 
			
		||||
	createdAt := time.Now().UTC()
 | 
			
		||||
	team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name, Owner: userID})
 | 
			
		||||
	return &team, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) SetTeamOwner(ctx context.Context, input SetTeamOwner) (*SetTeamOwnerPayload, error) {
 | 
			
		||||
	team, err := r.Repository.GetTeamByID(ctx, input.TeamID)
 | 
			
		||||
	if team.Owner == input.UserID {
 | 
			
		||||
		return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
 | 
			
		||||
	if role == auth.RoleAdmin {
 | 
			
		||||
		createdAt := time.Now().UTC()
 | 
			
		||||
		team, err := r.Repository.CreateTeam(ctx, db.CreateTeamParams{OrganizationID: input.OrganizationID, CreatedAt: createdAt, Name: input.Name})
 | 
			
		||||
		return &team, err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = r.Repository.SetTeamOwner(ctx, db.SetTeamOwnerParams{Owner: input.UserID, TeamID: input.TeamID})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
 | 
			
		||||
	return &db.Team{}, &gqlerror.Error{
 | 
			
		||||
		Message: "You must be an organization admin to create new teams",
 | 
			
		||||
		Extensions: map[string]interface{}{
 | 
			
		||||
			"code": "1-400",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	err = r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{TeamID: input.TeamID, UserID: input.UserID})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addedAt := time.Now().UTC()
 | 
			
		||||
	_, err = r.Repository.CreateTeamMember(ctx, db.CreateTeamMemberParams{TeamID: input.TeamID,
 | 
			
		||||
		UserID: team.Owner, RoleCode: RoleCodeAdmin.String(), Addeddate: addedAt})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &SetTeamOwnerPayload{Ok: false}, errors.New("new project owner is already project owner")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oldUser, err := r.Repository.GetUserAccountByID(ctx, team.Owner)
 | 
			
		||||
	var url *string
 | 
			
		||||
	if oldUser.ProfileAvatarUrl.Valid {
 | 
			
		||||
		url = &oldUser.ProfileAvatarUrl.String
 | 
			
		||||
	}
 | 
			
		||||
	profileIcon := &ProfileIcon{url, &oldUser.Initials, &oldUser.ProfileBgColor}
 | 
			
		||||
	oldUserRole := db.Role{Code: "admin", Name: "Admin"}
 | 
			
		||||
	oldMember := &Member{
 | 
			
		||||
		ID:          oldUser.UserID,
 | 
			
		||||
		Username:    oldUser.Username,
 | 
			
		||||
		FullName:    oldUser.FullName,
 | 
			
		||||
		ProfileIcon: profileIcon,
 | 
			
		||||
		Role:        &oldUserRole,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newUser, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
 | 
			
		||||
 | 
			
		||||
	if newUser.ProfileAvatarUrl.Valid {
 | 
			
		||||
		url = &newUser.ProfileAvatarUrl.String
 | 
			
		||||
	}
 | 
			
		||||
	profileIcon = &ProfileIcon{url, &newUser.Initials, &newUser.ProfileBgColor}
 | 
			
		||||
	newUserRole := db.Role{Code: "owner", Name: "Owner"}
 | 
			
		||||
	newMember := &Member{
 | 
			
		||||
		ID:          newUser.UserID,
 | 
			
		||||
		Username:    newUser.Username,
 | 
			
		||||
		FullName:    newUser.FullName,
 | 
			
		||||
		ProfileIcon: profileIcon,
 | 
			
		||||
		Role:        &newUserRole,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &SetTeamOwnerPayload{
 | 
			
		||||
		Ok:        true,
 | 
			
		||||
		PrevOwner: oldMember,
 | 
			
		||||
		NewOwner:  newMember,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) CreateTeamMember(ctx context.Context, input CreateTeamMember) (*CreateTeamMemberPayload, error) {
 | 
			
		||||
@@ -669,9 +560,6 @@ func (r *mutationResolver) CreateTeamMember(ctx context.Context, input CreateTea
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) UpdateTeamMemberRole(ctx context.Context, input UpdateTeamMemberRole) (*UpdateTeamMemberRolePayload, error) {
 | 
			
		||||
	if input.RoleCode == RoleCodeOwner || input.RoleCode == RoleCodeObserver {
 | 
			
		||||
		return &UpdateTeamMemberRolePayload{Ok: false}, errors.New("can not set project owner through this mutation")
 | 
			
		||||
	}
 | 
			
		||||
	user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("get user account")
 | 
			
		||||
@@ -699,29 +587,12 @@ func (r *mutationResolver) UpdateTeamMemberRole(ctx context.Context, input Updat
 | 
			
		||||
	member := Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon,
 | 
			
		||||
		Role: &db.Role{Code: role.Code, Name: role.Name},
 | 
			
		||||
	}
 | 
			
		||||
	return &UpdateTeamMemberRolePayload{Ok: true, Member: &member}, err
 | 
			
		||||
	return &UpdateTeamMemberRolePayload{Ok: true, Member: &member, TeamID: input.TeamID}, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) DeleteTeamMember(ctx context.Context, input DeleteTeamMember) (*DeleteTeamMemberPayload, error) {
 | 
			
		||||
	ownedProjects, err := r.Repository.GetOwnedTeamProjectsForUserID(ctx, db.GetOwnedTeamProjectsForUserIDParams{TeamID: input.TeamID, Owner: input.UserID})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &DeleteTeamMemberPayload{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = r.Repository.GetTeamMemberByID(ctx, db.GetTeamMemberByIDParams{TeamID: input.TeamID, UserID: input.UserID})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &DeleteTeamMemberPayload{}, err
 | 
			
		||||
	}
 | 
			
		||||
	err = r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{TeamID: input.TeamID, UserID: input.UserID})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &DeleteTeamMemberPayload{}, err
 | 
			
		||||
	}
 | 
			
		||||
	if input.NewOwnerID != nil {
 | 
			
		||||
		for _, projectID := range ownedProjects {
 | 
			
		||||
			_, err = r.Repository.SetProjectOwner(ctx, db.SetProjectOwnerParams{ProjectID: projectID, Owner: *input.NewOwnerID})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, nil
 | 
			
		||||
	err := r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{TeamID: input.TeamID, UserID: input.UserID})
 | 
			
		||||
	return &DeleteTeamMemberPayload{TeamID: input.TeamID, UserID: input.UserID}, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefreshToken) (*db.RefreshToken, error) {
 | 
			
		||||
@@ -733,6 +604,18 @@ func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*db.UserAccount, error) {
 | 
			
		||||
	_, role, ok := GetUser(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return &db.UserAccount{}, nil
 | 
			
		||||
	}
 | 
			
		||||
	if role != auth.RoleAdmin {
 | 
			
		||||
		return &db.UserAccount{}, &gqlerror.Error{
 | 
			
		||||
			Message: "Must be an organization admin",
 | 
			
		||||
			Extensions: map[string]interface{}{
 | 
			
		||||
				"code": "0-400",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	createdAt := time.Now().UTC()
 | 
			
		||||
	hashedPwd, err := bcrypt.GenerateFromPassword([]byte(input.Password), 14)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -751,35 +634,25 @@ func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserA
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error) {
 | 
			
		||||
	_, role, ok := GetUser(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return &DeleteUserAccountPayload{Ok: false}, nil
 | 
			
		||||
	}
 | 
			
		||||
	if role != auth.RoleAdmin {
 | 
			
		||||
		return &DeleteUserAccountPayload{Ok: false}, &gqlerror.Error{
 | 
			
		||||
			Message: "User not found",
 | 
			
		||||
			Extensions: map[string]interface{}{
 | 
			
		||||
				"code": "0-401",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &DeleteUserAccountPayload{Ok: false}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var newOwnerID uuid.UUID
 | 
			
		||||
	if input.NewOwnerID == nil {
 | 
			
		||||
		sysUser, err := r.Repository.GetUserAccountByUsername(ctx, "system")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return &DeleteUserAccountPayload{Ok: false}, err
 | 
			
		||||
		}
 | 
			
		||||
		newOwnerID = sysUser.UserID
 | 
			
		||||
	} else {
 | 
			
		||||
		newOwnerID = *input.NewOwnerID
 | 
			
		||||
	}
 | 
			
		||||
	projectIDs, err := r.Repository.UpdateProjectOwnerByOwnerID(ctx, db.UpdateProjectOwnerByOwnerIDParams{Owner: user.UserID, Owner_2: newOwnerID})
 | 
			
		||||
	if err != sql.ErrNoRows && err != nil {
 | 
			
		||||
		return &DeleteUserAccountPayload{Ok: false}, err
 | 
			
		||||
	}
 | 
			
		||||
	for _, projectID := range projectIDs {
 | 
			
		||||
		r.Repository.DeleteProjectMember(ctx, db.DeleteProjectMemberParams{UserID: newOwnerID, ProjectID: projectID})
 | 
			
		||||
	}
 | 
			
		||||
	teamIDs, err := r.Repository.UpdateTeamOwnerByOwnerID(ctx, db.UpdateTeamOwnerByOwnerIDParams{Owner: user.UserID, Owner_2: newOwnerID})
 | 
			
		||||
	if err != sql.ErrNoRows && err != nil {
 | 
			
		||||
		return &DeleteUserAccountPayload{Ok: false}, err
 | 
			
		||||
	}
 | 
			
		||||
	for _, teamID := range teamIDs {
 | 
			
		||||
		r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{UserID: newOwnerID, TeamID: teamID})
 | 
			
		||||
	}
 | 
			
		||||
	// TODO(jordanknott) migrate admin ownership
 | 
			
		||||
 | 
			
		||||
	err = r.Repository.DeleteUserAccountByID(ctx, input.UserID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &DeleteUserAccountPayload{Ok: false}, err
 | 
			
		||||
@@ -822,6 +695,18 @@ func (r *mutationResolver) UpdateUserPassword(ctx context.Context, input UpdateU
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
 | 
			
		||||
	_, role, ok := GetUser(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return &UpdateUserRolePayload{}, nil
 | 
			
		||||
	}
 | 
			
		||||
	if role != auth.RoleAdmin {
 | 
			
		||||
		return &UpdateUserRolePayload{}, &gqlerror.Error{
 | 
			
		||||
			Message: "User not found",
 | 
			
		||||
			Extensions: map[string]interface{}{
 | 
			
		||||
				"code": "0-401",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	user, err := r.Repository.UpdateUserRole(ctx, db.UpdateUserRoleParams{RoleCode: input.RoleCode.String(), UserID: input.UserID})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &UpdateUserRolePayload{}, err
 | 
			
		||||
@@ -839,26 +724,11 @@ func (r *projectResolver) ID(ctx context.Context, obj *db.Project) (uuid.UUID, e
 | 
			
		||||
 | 
			
		||||
func (r *projectResolver) Team(ctx context.Context, obj *db.Project) (*db.Team, error) {
 | 
			
		||||
	team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
 | 
			
		||||
	return &team, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *projectResolver) Owner(ctx context.Context, obj *db.Project) (*Member, error) {
 | 
			
		||||
	user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &Member{}, err
 | 
			
		||||
		log.WithFields(log.Fields{"teamID": obj.TeamID, "projectID": obj.ProjectID}).WithError(err).Error("issue while getting team for project")
 | 
			
		||||
		return &team, err
 | 
			
		||||
	}
 | 
			
		||||
	var url *string
 | 
			
		||||
	if user.ProfileAvatarUrl.Valid {
 | 
			
		||||
		url = &user.ProfileAvatarUrl.String
 | 
			
		||||
	}
 | 
			
		||||
	profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
 | 
			
		||||
	role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: user.UserID, ProjectID: obj.ProjectID})
 | 
			
		||||
	if user.ProfileAvatarUrl.Valid {
 | 
			
		||||
		url = &user.ProfileAvatarUrl.String
 | 
			
		||||
	}
 | 
			
		||||
	return &Member{ID: obj.Owner, FullName: user.FullName, ProfileIcon: profileIcon,
 | 
			
		||||
		Role: &db.Role{Code: role.Code, Name: role.Name},
 | 
			
		||||
	}, nil
 | 
			
		||||
	return &team, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *projectResolver) TaskGroups(ctx context.Context, obj *db.Project) ([]db.TaskGroup, error) {
 | 
			
		||||
@@ -866,24 +736,7 @@ func (r *projectResolver) TaskGroups(ctx context.Context, obj *db.Project) ([]db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Member, error) {
 | 
			
		||||
	user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
 | 
			
		||||
	members := []Member{}
 | 
			
		||||
	if err == sql.ErrNoRows {
 | 
			
		||||
		return members, nil
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("get user account by ID")
 | 
			
		||||
		return members, err
 | 
			
		||||
	}
 | 
			
		||||
	var url *string
 | 
			
		||||
	if user.ProfileAvatarUrl.Valid {
 | 
			
		||||
		url = &user.ProfileAvatarUrl.String
 | 
			
		||||
	}
 | 
			
		||||
	profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
 | 
			
		||||
	members = append(members, Member{
 | 
			
		||||
		ID: obj.Owner, FullName: user.FullName, ProfileIcon: profileIcon, Username: user.Username,
 | 
			
		||||
		Role: &db.Role{Code: "owner", Name: "Owner"},
 | 
			
		||||
	})
 | 
			
		||||
	projectMembers, err := r.Repository.GetProjectMembersForProjectID(ctx, obj.ProjectID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("get project members for project id")
 | 
			
		||||
@@ -891,7 +744,7 @@ func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Membe
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, projectMember := range projectMembers {
 | 
			
		||||
		user, err = r.Repository.GetUserAccountByID(ctx, projectMember.UserID)
 | 
			
		||||
		user, err := r.Repository.GetUserAccountByID(ctx, projectMember.UserID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.WithError(err).Error("get user account by ID")
 | 
			
		||||
			return members, err
 | 
			
		||||
@@ -964,11 +817,12 @@ func (r *queryResolver) FindUser(ctx context.Context, input FindUser) (*db.UserA
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db.Project, error) {
 | 
			
		||||
	projectID, err := uuid.Parse(input.ProjectID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &db.Project{}, err
 | 
			
		||||
	userID, role, ok := GetUser(ctx)
 | 
			
		||||
	log.WithFields(log.Fields{"userID": userID, "role": role}).Info("find project user")
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return &db.Project{}, nil
 | 
			
		||||
	}
 | 
			
		||||
	project, err := r.Repository.GetProjectByID(ctx, projectID)
 | 
			
		||||
	project, err := r.Repository.GetProjectByID(ctx, input.ProjectID)
 | 
			
		||||
	if err == sql.ErrNoRows {
 | 
			
		||||
		return &db.Project{}, &gqlerror.Error{
 | 
			
		||||
			Message: "Project not found",
 | 
			
		||||
@@ -977,7 +831,26 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &project, err
 | 
			
		||||
	if role == auth.RoleAdmin {
 | 
			
		||||
		return &project, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	projectRoles, err := GetProjectRoles(ctx, r.Repository, input.ProjectID)
 | 
			
		||||
	log.WithFields(log.Fields{"projectID": input.ProjectID, "teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("get project roles ")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &project, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if projectRoles.TeamRole == "" && projectRoles.ProjectRole == "" {
 | 
			
		||||
		return &db.Project{}, &gqlerror.Error{
 | 
			
		||||
			Message: "project not accessible",
 | 
			
		||||
			Extensions: map[string]interface{}{
 | 
			
		||||
				"code": "11-400",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &project, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task, error) {
 | 
			
		||||
@@ -986,10 +859,57 @@ func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*db.Task,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]db.Project, error) {
 | 
			
		||||
	userID, orgRole, ok := GetUser(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		log.Info("user id was not found from middleware")
 | 
			
		||||
		return []db.Project{}, nil
 | 
			
		||||
	}
 | 
			
		||||
	log.WithFields(log.Fields{"userID": userID}).Info("fetching projects")
 | 
			
		||||
 | 
			
		||||
	if input != nil {
 | 
			
		||||
		return r.Repository.GetAllProjectsForTeam(ctx, *input.TeamID)
 | 
			
		||||
	}
 | 
			
		||||
	return r.Repository.GetAllProjects(ctx)
 | 
			
		||||
 | 
			
		||||
	if orgRole == "admin" {
 | 
			
		||||
		log.Info("showing all projects for admin")
 | 
			
		||||
		return r.Repository.GetAllProjects(ctx)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	teams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
 | 
			
		||||
	projects := make(map[string]db.Project)
 | 
			
		||||
	for _, team := range teams {
 | 
			
		||||
		log.WithFields(log.Fields{"teamID": team.TeamID}).Info("found team")
 | 
			
		||||
		teamProjects, err := r.Repository.GetAllProjectsForTeam(ctx, team.TeamID)
 | 
			
		||||
		if err != sql.ErrNoRows && err != nil {
 | 
			
		||||
			log.Info("issue getting team projects")
 | 
			
		||||
			return []db.Project{}, nil
 | 
			
		||||
		}
 | 
			
		||||
		for _, project := range teamProjects {
 | 
			
		||||
			log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding team project")
 | 
			
		||||
			projects[project.ProjectID.String()] = project
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Info("user id was not found from middleware")
 | 
			
		||||
		return []db.Project{}, nil
 | 
			
		||||
	}
 | 
			
		||||
	for _, project := range visibleProjects {
 | 
			
		||||
		log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("found visible project")
 | 
			
		||||
		if _, ok := projects[project.ProjectID.String()]; !ok {
 | 
			
		||||
			log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project")
 | 
			
		||||
			projects[project.ProjectID.String()] = project
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	log.WithFields(log.Fields{"projectLength": len(projects)}).Info("making projects")
 | 
			
		||||
	allProjects := make([]db.Project, 0, len(projects))
 | 
			
		||||
	for _, project := range projects {
 | 
			
		||||
		log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("add project to final list")
 | 
			
		||||
		allProjects = append(allProjects, project)
 | 
			
		||||
	}
 | 
			
		||||
	log.Info(allProjects)
 | 
			
		||||
	return allProjects, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *queryResolver) FindTeam(ctx context.Context, input FindTeam) (*db.Team, error) {
 | 
			
		||||
@@ -1001,7 +921,47 @@ func (r *queryResolver) FindTeam(ctx context.Context, input FindTeam) (*db.Team,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
 | 
			
		||||
	return r.Repository.GetAllTeams(ctx)
 | 
			
		||||
	userID, orgRole, ok := GetUser(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		log.Error("userID or orgRole does not exist!")
 | 
			
		||||
		return []db.Team{}, errors.New("internal error")
 | 
			
		||||
	}
 | 
			
		||||
	if orgRole == "admin" {
 | 
			
		||||
		return r.Repository.GetAllTeams(ctx)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	teams := make(map[string]db.Team)
 | 
			
		||||
	adminTeams, err := r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return []db.Team{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, team := range adminTeams {
 | 
			
		||||
		teams[team.TeamID.String()] = team
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Info("user id was not found from middleware")
 | 
			
		||||
		return []db.Team{}, err
 | 
			
		||||
	}
 | 
			
		||||
	for _, project := range visibleProjects {
 | 
			
		||||
		log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("found visible project")
 | 
			
		||||
		if _, ok := teams[project.ProjectID.String()]; !ok {
 | 
			
		||||
			log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project")
 | 
			
		||||
			team, err := r.Repository.GetTeamByID(ctx, project.TeamID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Info("user id was not found from middleware")
 | 
			
		||||
				return []db.Team{}, err
 | 
			
		||||
			}
 | 
			
		||||
			teams[project.TeamID.String()] = team
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	foundTeams := make([]db.Team, 0, len(teams))
 | 
			
		||||
	for _, team := range teams {
 | 
			
		||||
		foundTeams = append(foundTeams, team)
 | 
			
		||||
	}
 | 
			
		||||
	return foundTeams, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *queryResolver) LabelColors(ctx context.Context) ([]db.LabelColor, error) {
 | 
			
		||||
@@ -1012,19 +972,37 @@ func (r *queryResolver) TaskGroups(ctx context.Context) ([]db.TaskGroup, error)
 | 
			
		||||
	return r.Repository.GetAllTaskGroups(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *queryResolver) Me(ctx context.Context) (*db.UserAccount, error) {
 | 
			
		||||
func (r *queryResolver) Me(ctx context.Context) (*MePayload, error) {
 | 
			
		||||
	userID, ok := GetUserID(ctx)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return &db.UserAccount{}, fmt.Errorf("internal server error")
 | 
			
		||||
		return &MePayload{}, fmt.Errorf("internal server error")
 | 
			
		||||
	}
 | 
			
		||||
	user, err := r.Repository.GetUserAccountByID(ctx, userID)
 | 
			
		||||
	if err == sql.ErrNoRows {
 | 
			
		||||
		log.WithFields(log.Fields{"userID": userID}).Warning("can not find user for me query")
 | 
			
		||||
		return &db.UserAccount{}, nil
 | 
			
		||||
		return &MePayload{}, nil
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return &db.UserAccount{}, err
 | 
			
		||||
		return &MePayload{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return &user, err
 | 
			
		||||
	var projectRoles []ProjectRole
 | 
			
		||||
	projects, err := r.Repository.GetProjectRolesForUserID(ctx, userID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &MePayload{}, err
 | 
			
		||||
	}
 | 
			
		||||
	for _, project := range projects {
 | 
			
		||||
		projectRoles = append(projectRoles, ProjectRole{ProjectID: project.ProjectID, RoleCode: ConvertToRoleCode("admin")})
 | 
			
		||||
		// projectRoles = append(projectRoles, ProjectRole{ProjectID: project.ProjectID, RoleCode: ConvertToRoleCode(project.RoleCode)})
 | 
			
		||||
	}
 | 
			
		||||
	var teamRoles []TeamRole
 | 
			
		||||
	teams, err := r.Repository.GetTeamRolesForUserID(ctx, userID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &MePayload{}, err
 | 
			
		||||
	}
 | 
			
		||||
	for _, team := range teams {
 | 
			
		||||
		// teamRoles = append(teamRoles, TeamRole{TeamID: team.TeamID, RoleCode: ConvertToRoleCode(team.RoleCode)})
 | 
			
		||||
		teamRoles = append(teamRoles, TeamRole{TeamID: team.TeamID, RoleCode: ConvertToRoleCode("admin")})
 | 
			
		||||
	}
 | 
			
		||||
	return &MePayload{User: &user, TeamRoles: teamRoles, ProjectRoles: projectRoles}, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *refreshTokenResolver) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) {
 | 
			
		||||
@@ -1171,34 +1149,8 @@ func (r *teamResolver) ID(ctx context.Context, obj *db.Team) (uuid.UUID, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, error) {
 | 
			
		||||
	user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
 | 
			
		||||
	members := []Member{}
 | 
			
		||||
	log.WithFields(log.Fields{"teamID": obj.TeamID}).Info("getting members")
 | 
			
		||||
	if err == sql.ErrNoRows {
 | 
			
		||||
		return members, nil
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("get user account by ID")
 | 
			
		||||
		return members, err
 | 
			
		||||
	}
 | 
			
		||||
	ownedList, err := GetOwnedList(ctx, r.Repository, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return members, err
 | 
			
		||||
	}
 | 
			
		||||
	memberList, err := GetMemberList(ctx, r.Repository, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return members, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var url *string
 | 
			
		||||
	if user.ProfileAvatarUrl.Valid {
 | 
			
		||||
		url = &user.ProfileAvatarUrl.String
 | 
			
		||||
	}
 | 
			
		||||
	profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor}
 | 
			
		||||
	members = append(members, Member{
 | 
			
		||||
		ID: obj.Owner, FullName: user.FullName, ProfileIcon: profileIcon, Username: user.Username,
 | 
			
		||||
		Owned: ownedList, Member: memberList, Role: &db.Role{Code: "owner", Name: "Owner"},
 | 
			
		||||
	})
 | 
			
		||||
	teamMembers, err := r.Repository.GetTeamMembersForTeamID(ctx, obj.TeamID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("get project members for project id")
 | 
			
		||||
@@ -1206,7 +1158,7 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, teamMember := range teamMembers {
 | 
			
		||||
		user, err = r.Repository.GetUserAccountByID(ctx, teamMember.UserID)
 | 
			
		||||
		user, err := r.Repository.GetUserAccountByID(ctx, teamMember.UserID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.WithError(err).Error("get user account by ID")
 | 
			
		||||
			return members, err
 | 
			
		||||
@@ -1262,15 +1214,7 @@ func (r *userAccountResolver) ProfileIcon(ctx context.Context, obj *db.UserAccou
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *userAccountResolver) Owned(ctx context.Context, obj *db.UserAccount) (*OwnedList, error) {
 | 
			
		||||
	ownedTeams, err := r.Repository.GetOwnedTeamsForUserID(ctx, obj.UserID)
 | 
			
		||||
	if err != sql.ErrNoRows && err != nil {
 | 
			
		||||
		return &OwnedList{}, err
 | 
			
		||||
	}
 | 
			
		||||
	ownedProjects, err := r.Repository.GetOwnedProjectsForUserID(ctx, obj.UserID)
 | 
			
		||||
	if err != sql.ErrNoRows && err != nil {
 | 
			
		||||
		return &OwnedList{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return &OwnedList{Teams: ownedTeams, Projects: ownedProjects}, nil
 | 
			
		||||
	return &OwnedList{}, nil // TODO(jordanknott)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *userAccountResolver) Member(ctx context.Context, obj *db.UserAccount) (*MemberList, error) {
 | 
			
		||||
@@ -1330,7 +1274,9 @@ func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
 | 
			
		||||
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
 | 
			
		||||
 | 
			
		||||
// TaskChecklistItem returns TaskChecklistItemResolver implementation.
 | 
			
		||||
func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver { return &taskChecklistItemResolver{r} }
 | 
			
		||||
func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver {
 | 
			
		||||
	return &taskChecklistItemResolver{r}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TaskGroup returns TaskGroupResolver implementation.
 | 
			
		||||
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
 | 
			
		||||
 
 | 
			
		||||
@@ -97,7 +97,6 @@ type Project {
 | 
			
		||||
  createdAt: Time!
 | 
			
		||||
  name: String!
 | 
			
		||||
  team: Team!
 | 
			
		||||
  owner: Member!
 | 
			
		||||
  taskGroups: [TaskGroup!]!
 | 
			
		||||
  members: [Member!]!
 | 
			
		||||
  labels: [ProjectLabel!]!
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,23 @@
 | 
			
		||||
enum RoleLevel {
 | 
			
		||||
  ADMIN
 | 
			
		||||
  MEMBER
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ActionLevel {
 | 
			
		||||
  ORG
 | 
			
		||||
  TEAM
 | 
			
		||||
  PROJECT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ObjectType {
 | 
			
		||||
  ORG
 | 
			
		||||
  TEAM
 | 
			
		||||
  PROJECT
 | 
			
		||||
  TASK
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
directive @hasRole(roles: [RoleLevel!]!, level: ActionLevel!, type: ObjectType!) on FIELD_DEFINITION
 | 
			
		||||
 | 
			
		||||
type Query {
 | 
			
		||||
  organizations: [Organization!]!
 | 
			
		||||
  users: [UserAccount!]!
 | 
			
		||||
@@ -9,11 +29,27 @@ type Query {
 | 
			
		||||
  teams: [Team!]!
 | 
			
		||||
  labelColors: [LabelColor!]!
 | 
			
		||||
  taskGroups: [TaskGroup!]!
 | 
			
		||||
  me: UserAccount!
 | 
			
		||||
  me: MePayload! 
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Mutation
 | 
			
		||||
 | 
			
		||||
type TeamRole {
 | 
			
		||||
  teamID: UUID!
 | 
			
		||||
  roleCode: RoleCode!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ProjectRole {
 | 
			
		||||
  projectID: UUID!
 | 
			
		||||
  roleCode: RoleCode!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MePayload {
 | 
			
		||||
  user: UserAccount!
 | 
			
		||||
  teamRoles: [TeamRole!]!
 | 
			
		||||
  projectRoles: [ProjectRole!]!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input ProjectsFilter {
 | 
			
		||||
  teamID: UUID
 | 
			
		||||
}
 | 
			
		||||
@@ -23,7 +59,7 @@ input FindUser {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input FindProject {
 | 
			
		||||
  projectId: String!
 | 
			
		||||
  projectID: UUID!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input FindTask {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createProject(input: NewProject!): Project!
 | 
			
		||||
  deleteProject(input: DeleteProject!): DeleteProjectPayload!
 | 
			
		||||
  updateProjectName(input: UpdateProjectName): Project!
 | 
			
		||||
  createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
  deleteProject(input: DeleteProject!):
 | 
			
		||||
    DeleteProjectPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectName(input: UpdateProjectName):
 | 
			
		||||
    Project! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewProject {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,14 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createProjectLabel(input: NewProjectLabel!): ProjectLabel!
 | 
			
		||||
  deleteProjectLabel(input: DeleteProjectLabel!): ProjectLabel!
 | 
			
		||||
  updateProjectLabel(input: UpdateProjectLabel!): ProjectLabel!
 | 
			
		||||
  updateProjectLabelName(input: UpdateProjectLabelName!): ProjectLabel!
 | 
			
		||||
  updateProjectLabelColor(input: UpdateProjectLabelColor!): ProjectLabel!
 | 
			
		||||
  createProjectLabel(input: NewProjectLabel!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  deleteProjectLabel(input: DeleteProjectLabel!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectLabel(input: UpdateProjectLabel!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectLabelName(input: UpdateProjectLabelName!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectLabelColor(input: UpdateProjectLabelColor!):
 | 
			
		||||
    ProjectLabel! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewProjectLabel {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createProjectMember(input: CreateProjectMember!): CreateProjectMemberPayload!
 | 
			
		||||
  deleteProjectMember(input: DeleteProjectMember!): DeleteProjectMemberPayload!
 | 
			
		||||
  updateProjectMemberRole(input: UpdateProjectMemberRole!): UpdateProjectMemberRolePayload!
 | 
			
		||||
  setProjectOwner(input: SetProjectOwner!): SetProjectOwnerPayload!
 | 
			
		||||
  createProjectMember(input: CreateProjectMember!):
 | 
			
		||||
    CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  deleteProjectMember(input: DeleteProjectMember!):
 | 
			
		||||
    DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateProjectMemberRole(input: UpdateProjectMemberRole!):
 | 
			
		||||
    UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input CreateProjectMember {
 | 
			
		||||
@@ -36,13 +38,3 @@ type UpdateProjectMemberRolePayload {
 | 
			
		||||
  ok: Boolean!
 | 
			
		||||
  member: Member!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input SetProjectOwner {
 | 
			
		||||
  projectID: UUID!
 | 
			
		||||
  ownerID: UUID!
 | 
			
		||||
}
 | 
			
		||||
type SetProjectOwnerPayload {
 | 
			
		||||
  ok: Boolean!
 | 
			
		||||
  prevOwner: Member!
 | 
			
		||||
  newOwner: Member!
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,24 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createTask(input: NewTask!): Task!
 | 
			
		||||
  deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
 | 
			
		||||
  createTask(input: NewTask!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  deleteTask(input: DeleteTaskInput!):
 | 
			
		||||
    DeleteTaskPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
 | 
			
		||||
  updateTaskDescription(input: UpdateTaskDescriptionInput!): Task!
 | 
			
		||||
  updateTaskLocation(input: NewTaskLocation!): UpdateTaskLocationPayload!
 | 
			
		||||
  updateTaskName(input: UpdateTaskName!): Task!
 | 
			
		||||
  setTaskComplete(input: SetTaskComplete!): Task!
 | 
			
		||||
  updateTaskDueDate(input: UpdateTaskDueDate!): Task!
 | 
			
		||||
  updateTaskDescription(input: UpdateTaskDescriptionInput!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateTaskLocation(input: NewTaskLocation!):
 | 
			
		||||
    UpdateTaskLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateTaskName(input: UpdateTaskName!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  setTaskComplete(input: SetTaskComplete!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateTaskDueDate(input: UpdateTaskDueDate!):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
 | 
			
		||||
  assignTask(input: AssignTaskInput): Task!
 | 
			
		||||
  unassignTask(input: UnassignTaskInput): Task!
 | 
			
		||||
  assignTask(input: AssignTaskInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  unassignTask(input: UnassignTaskInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewTask {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,23 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createTaskChecklist(input: CreateTaskChecklist!): TaskChecklist!
 | 
			
		||||
  deleteTaskChecklist(input: DeleteTaskChecklist!): DeleteTaskChecklistPayload!
 | 
			
		||||
  updateTaskChecklistName(input: UpdateTaskChecklistName!): TaskChecklist!
 | 
			
		||||
  createTaskChecklistItem(input: CreateTaskChecklistItem!): TaskChecklistItem!
 | 
			
		||||
  updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!): TaskChecklistItem!
 | 
			
		||||
  setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!): TaskChecklistItem!
 | 
			
		||||
  deleteTaskChecklistItem(input: DeleteTaskChecklistItem!): DeleteTaskChecklistItemPayload!
 | 
			
		||||
  createTaskChecklist(input: CreateTaskChecklist!):
 | 
			
		||||
    TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  deleteTaskChecklist(input: DeleteTaskChecklist!):
 | 
			
		||||
    DeleteTaskChecklistPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateTaskChecklistName(input: UpdateTaskChecklistName!):
 | 
			
		||||
    TaskChecklist! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  createTaskChecklistItem(input: CreateTaskChecklistItem!):
 | 
			
		||||
    TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateTaskChecklistItemName(input: UpdateTaskChecklistItemName!):
 | 
			
		||||
    TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  setTaskChecklistItemComplete(input: SetTaskChecklistItemComplete!):
 | 
			
		||||
    TaskChecklistItem! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  deleteTaskChecklistItem(input: DeleteTaskChecklistItem!):
 | 
			
		||||
    DeleteTaskChecklistItemPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!):
 | 
			
		||||
    UpdateTaskChecklistLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!):
 | 
			
		||||
    UpdateTaskChecklistItemLocationPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
 | 
			
		||||
  updateTaskChecklistLocation(input: UpdateTaskChecklistLocation!): UpdateTaskChecklistLocationPayload!
 | 
			
		||||
  updateTaskChecklistItemLocation(input: UpdateTaskChecklistItemLocation!): UpdateTaskChecklistItemLocationPayload!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input UpdateTaskChecklistItemLocation {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,12 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createTaskGroup(input: NewTaskGroup!): TaskGroup!
 | 
			
		||||
  updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
 | 
			
		||||
  updateTaskGroupName(input: UpdateTaskGroupName!): TaskGroup!
 | 
			
		||||
  deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload!
 | 
			
		||||
  createTaskGroup(input: NewTaskGroup!):
 | 
			
		||||
    TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateTaskGroupLocation(input: NewTaskGroupLocation!):
 | 
			
		||||
    TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  updateTaskGroupName(input: UpdateTaskGroupName!):
 | 
			
		||||
    TaskGroup! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  deleteTaskGroup(input: DeleteTaskGroupInput!):
 | 
			
		||||
    DeleteTaskGroupPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewTaskGroupLocation {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,11 @@ type ToggleTaskLabelPayload {
 | 
			
		||||
  task: Task!
 | 
			
		||||
}
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  addTaskLabel(input: AddTaskLabelInput): Task!
 | 
			
		||||
  removeTaskLabel(input: RemoveTaskLabelInput): Task!
 | 
			
		||||
  toggleTaskLabel(input: ToggleTaskLabelInput!): ToggleTaskLabelPayload!
 | 
			
		||||
  addTaskLabel(input: AddTaskLabelInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  removeTaskLabel(input: RemoveTaskLabelInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
  toggleTaskLabel(input: ToggleTaskLabelInput!):
 | 
			
		||||
    ToggleTaskLabelPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  deleteTeam(input: DeleteTeam!): DeleteTeamPayload!
 | 
			
		||||
  createTeam(input: NewTeam!): Team!
 | 
			
		||||
  deleteTeam(input: DeleteTeam!):
 | 
			
		||||
    DeleteTeamPayload! @hasRole(roles:[ ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
  createTeam(input: NewTeam!):
 | 
			
		||||
    Team! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewTeam {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  setTeamOwner(input: SetTeamOwner!): SetTeamOwnerPayload!
 | 
			
		||||
  createTeamMember(input: CreateTeamMember!): CreateTeamMemberPayload!
 | 
			
		||||
  updateTeamMemberRole(input: UpdateTeamMemberRole!): UpdateTeamMemberRolePayload!
 | 
			
		||||
  deleteTeamMember(input: DeleteTeamMember!): DeleteTeamMemberPayload!
 | 
			
		||||
  createTeamMember(input: CreateTeamMember!):
 | 
			
		||||
    CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
  updateTeamMemberRole(input: UpdateTeamMemberRole!):
 | 
			
		||||
    UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
  deleteTeamMember(input: DeleteTeamMember!):
 | 
			
		||||
    DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input DeleteTeamMember {
 | 
			
		||||
@@ -35,16 +38,6 @@ input UpdateTeamMemberRole {
 | 
			
		||||
 | 
			
		||||
type UpdateTeamMemberRolePayload {
 | 
			
		||||
  ok: Boolean!
 | 
			
		||||
  teamID: UUID!
 | 
			
		||||
  member: Member!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input SetTeamOwner {
 | 
			
		||||
  teamID: UUID!
 | 
			
		||||
  userID: UUID!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SetTeamOwnerPayload {
 | 
			
		||||
  ok: Boolean!
 | 
			
		||||
  prevOwner: Member!
 | 
			
		||||
  newOwner: Member!
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,16 @@
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createRefreshToken(input: NewRefreshToken!): RefreshToken!
 | 
			
		||||
  createUserAccount(input: NewUserAccount!): UserAccount!
 | 
			
		||||
  deleteUserAccount(input: DeleteUserAccount!): DeleteUserAccountPayload!
 | 
			
		||||
  createUserAccount(input: NewUserAccount!):
 | 
			
		||||
    UserAccount! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
  deleteUserAccount(input: DeleteUserAccount!):
 | 
			
		||||
    DeleteUserAccountPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
 | 
			
		||||
  logoutUser(input: LogoutUser!): Boolean!
 | 
			
		||||
  clearProfileAvatar:  UserAccount!
 | 
			
		||||
 | 
			
		||||
  updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
 | 
			
		||||
  updateUserRole(input: UpdateUserRole!): UpdateUserRolePayload!
 | 
			
		||||
  updateUserRole(input: UpdateUserRole!):
 | 
			
		||||
   UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input UpdateUserPassword {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user