feat: add personal projects
personal projects are projects that have no team. they can only seen by the project members (one of which is whoever first creates the project).
This commit is contained in:
@ -2571,7 +2571,7 @@ type Project {
|
||||
id: ID!
|
||||
createdAt: Time!
|
||||
name: String!
|
||||
team: Team!
|
||||
team: Team
|
||||
taskGroups: [TaskGroup!]!
|
||||
members: [Member!]!
|
||||
labels: [ProjectLabel!]!
|
||||
@ -2659,7 +2659,8 @@ type Query {
|
||||
organizations: [Organization!]!
|
||||
users: [UserAccount!]!
|
||||
findUser(input: FindUser!): UserAccount!
|
||||
findProject(input: FindProject!): Project!
|
||||
findProject(input: FindProject!):
|
||||
Project! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
||||
findTask(input: FindTask!): Task!
|
||||
projects(input: ProjectsFilter): [Project!]!
|
||||
findTeam(input: FindTeam!): Team!
|
||||
@ -2753,8 +2754,7 @@ extend type Mutation {
|
||||
}
|
||||
|
||||
input NewProject {
|
||||
userID: UUID!
|
||||
teamID: UUID!
|
||||
teamID: UUID
|
||||
name: String!
|
||||
}
|
||||
|
||||
@ -10193,14 +10193,11 @@ func (ec *executionContext) _Project_team(ctx context.Context, field graphql.Col
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*db.Team)
|
||||
fc.Result = res
|
||||
return ec.marshalNTeam2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx, field.Selections, res)
|
||||
return ec.marshalOTeam2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Project_taskGroups(ctx context.Context, field graphql.CollectedField, obj *db.Project) (ret graphql.Marshaler) {
|
||||
@ -10638,8 +10635,40 @@ func (ec *executionContext) _Query_findProject(ctx context.Context, field graphq
|
||||
}
|
||||
fc.Args = args
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Query().FindProject(rctx, args["input"].(FindProject))
|
||||
directive0 := func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Query().FindProject(rctx, args["input"].(FindProject))
|
||||
}
|
||||
directive1 := func(ctx context.Context) (interface{}, error) {
|
||||
roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN", "MEMBER"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "PROJECT")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "PROJECT")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ec.directives.HasRole == nil {
|
||||
return nil, errors.New("directive hasRole is not implemented")
|
||||
}
|
||||
return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg)
|
||||
}
|
||||
|
||||
tmp, err := directive1(rctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tmp == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if data, ok := tmp.(*db.Project); ok {
|
||||
return data, nil
|
||||
}
|
||||
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/db.Project`, tmp)
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
@ -15121,15 +15150,9 @@ func (ec *executionContext) unmarshalInputNewProject(ctx context.Context, obj in
|
||||
|
||||
for k, v := range asMap {
|
||||
switch k {
|
||||
case "userID":
|
||||
var err error
|
||||
it.UserID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "teamID":
|
||||
var err error
|
||||
it.TeamID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
|
||||
it.TeamID, err = ec.unmarshalOUUID2ᚖgithubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
@ -17286,9 +17309,6 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet,
|
||||
}
|
||||
}()
|
||||
res = ec._Project_team(ctx, field, obj)
|
||||
if res == graphql.Null {
|
||||
atomic.AddUint32(&invalids, 1)
|
||||
}
|
||||
return res
|
||||
})
|
||||
case "taskGroups":
|
||||
@ -20927,6 +20947,17 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as
|
||||
return ec.marshalOString2string(ctx, sel, *v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalOTeam2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx context.Context, sel ast.SelectionSet, v db.Team) graphql.Marshaler {
|
||||
return ec._Team(ctx, sel, &v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalOTeam2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTeam(ctx context.Context, sel ast.SelectionSet, v *db.Team) graphql.Marshaler {
|
||||
if v == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
return ec._Team(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalOTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) {
|
||||
return graphql.UnmarshalTime(v)
|
||||
}
|
||||
|
@ -2,10 +2,12 @@ package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
@ -19,6 +21,7 @@ import (
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
"github.com/jordanknott/taskcafe/internal/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
)
|
||||
|
||||
// NewHandler returns a new graphql endpoint handler.
|
||||
@ -66,6 +69,11 @@ func NewHandler(repo db.Repository) http.Handler {
|
||||
log.Error("subject field name does not exist on input type")
|
||||
return nil, errors.New("subject field name does not exist on input type")
|
||||
}
|
||||
if fieldName == "TeamID" && subjectField.IsNil() {
|
||||
// Is a personal project, no check
|
||||
// TODO: add config setting to disable personal projects
|
||||
return next(ctx)
|
||||
}
|
||||
subjectID, ok = subjectField.Interface().(uuid.UUID)
|
||||
if !ok {
|
||||
log.Error("error while casting subject UUID")
|
||||
@ -93,16 +101,30 @@ func NewHandler(repo db.Repository) http.Handler {
|
||||
}
|
||||
projectRoles, err := GetProjectRoles(ctx, repo, subjectID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, &gqlerror.Error{
|
||||
Message: "not authorized",
|
||||
Extensions: map[string]interface{}{
|
||||
"code": "401",
|
||||
},
|
||||
}
|
||||
}
|
||||
log.WithError(err).Error("error while getting project roles")
|
||||
return nil, err
|
||||
}
|
||||
for _, validRole := range roles {
|
||||
if GetRoleLevel(projectRoles.TeamRole) == validRole || GetRoleLevel(projectRoles.ProjectRole) == validRole {
|
||||
log.WithFields(log.Fields{"validRole": validRole}).Info("checking role")
|
||||
if CompareRoleLevel(projectRoles.TeamRole, validRole) || CompareRoleLevel(projectRoles.ProjectRole, validRole) {
|
||||
log.WithFields(log.Fields{"teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("is team or project role")
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
return nil, errors.New("must be a team or project admin")
|
||||
return nil, &gqlerror.Error{
|
||||
Message: "not authorized",
|
||||
Extensions: map[string]interface{}{
|
||||
"code": "401",
|
||||
},
|
||||
}
|
||||
} else if level == ActionLevelTeam {
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
@ -114,14 +136,24 @@ func NewHandler(repo db.Repository) http.Handler {
|
||||
return nil, err
|
||||
}
|
||||
for _, validRole := range roles {
|
||||
if GetRoleLevel(role.RoleCode) == validRole || GetRoleLevel(role.RoleCode) == validRole {
|
||||
if CompareRoleLevel(role.RoleCode, validRole) || CompareRoleLevel(role.RoleCode, validRole) {
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
return nil, errors.New("must be a team admin")
|
||||
return nil, &gqlerror.Error{
|
||||
Message: "not authorized",
|
||||
Extensions: map[string]interface{}{
|
||||
"code": "401",
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
return nil, errors.New("invalid path")
|
||||
return nil, &gqlerror.Error{
|
||||
Message: "bad path",
|
||||
Extensions: map[string]interface{}{
|
||||
"code": "500",
|
||||
},
|
||||
}
|
||||
}
|
||||
srv := handler.New(NewExecutableSchema(c))
|
||||
srv.AddTransport(transport.Websocket{
|
||||
@ -184,12 +216,12 @@ func GetProjectRoles(ctx context.Context, r db.Repository, projectID uuid.UUID)
|
||||
return r.GetUserRolesForProject(ctx, db.GetUserRolesForProjectParams{UserID: userID, ProjectID: projectID})
|
||||
}
|
||||
|
||||
// GetRoleLevel converts a role level string to a RoleLevel type
|
||||
func GetRoleLevel(r string) RoleLevel {
|
||||
if r == RoleLevelAdmin.String() {
|
||||
return RoleLevelAdmin
|
||||
// CompareRoleLevel compares a string against a role level
|
||||
func CompareRoleLevel(a string, b RoleLevel) bool {
|
||||
if strings.ToLower(a) == strings.ToLower(b.String()) {
|
||||
return true
|
||||
}
|
||||
return RoleLevelMember
|
||||
return false
|
||||
}
|
||||
|
||||
// ConvertToRoleCode converts a role code string to a RoleCode type
|
||||
|
@ -213,9 +213,8 @@ type MemberList struct {
|
||||
}
|
||||
|
||||
type NewProject struct {
|
||||
UserID uuid.UUID `json:"userID"`
|
||||
TeamID uuid.UUID `json:"teamID"`
|
||||
Name string `json:"name"`
|
||||
TeamID *uuid.UUID `json:"teamID"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type NewProjectLabel struct {
|
||||
|
@ -97,7 +97,7 @@ type Project {
|
||||
id: ID!
|
||||
createdAt: Time!
|
||||
name: String!
|
||||
team: Team!
|
||||
team: Team
|
||||
taskGroups: [TaskGroup!]!
|
||||
members: [Member!]!
|
||||
labels: [ProjectLabel!]!
|
||||
@ -185,7 +185,8 @@ type Query {
|
||||
organizations: [Organization!]!
|
||||
users: [UserAccount!]!
|
||||
findUser(input: FindUser!): UserAccount!
|
||||
findProject(input: FindProject!): Project!
|
||||
findProject(input: FindProject!):
|
||||
Project! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
||||
findTask(input: FindTask!): Task!
|
||||
projects(input: ProjectsFilter): [Project!]!
|
||||
findTeam(input: FindTeam!): Team!
|
||||
@ -279,8 +280,7 @@ extend type Mutation {
|
||||
}
|
||||
|
||||
input NewProject {
|
||||
userID: UUID!
|
||||
teamID: UUID!
|
||||
teamID: UUID
|
||||
name: String!
|
||||
}
|
||||
|
||||
|
@ -23,10 +23,41 @@ func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.U
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*db.Project, error) {
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
return &db.Project{}, errors.New("user id is missing")
|
||||
}
|
||||
createdAt := time.Now().UTC()
|
||||
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
|
||||
log.WithFields(log.Fields{"name": input.Name, "teamID": input.TeamID}).Info("creating new project")
|
||||
var project db.Project
|
||||
var err error
|
||||
if input.TeamID == nil {
|
||||
project, err = r.Repository.CreatePersonalProject(ctx, db.CreatePersonalProjectParams{
|
||||
CreatedAt: createdAt,
|
||||
Name: input.Name,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating project")
|
||||
return &db.Project{}, err
|
||||
}
|
||||
log.WithFields(log.Fields{"userID": userID, "projectID": project.ProjectID}).Info("creating personal project link")
|
||||
} else {
|
||||
project, err = r.Repository.CreateTeamProject(ctx, db.CreateTeamProjectParams{
|
||||
CreatedAt: createdAt,
|
||||
Name: input.Name,
|
||||
TeamID: *input.TeamID,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating project")
|
||||
return &db.Project{}, err
|
||||
}
|
||||
}
|
||||
_, err = r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: project.ProjectID, UserID: userID, AddedAt: createdAt, RoleCode: "admin"})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating initial project member")
|
||||
return &db.Project{}, err
|
||||
}
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteProject(ctx context.Context, input DeleteProject) (*DeleteProjectPayload, error) {
|
||||
@ -880,6 +911,9 @@ 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)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
log.WithFields(log.Fields{"teamID": obj.TeamID, "projectID": obj.ProjectID}).WithError(err).Error("issue while getting team for project")
|
||||
return &team, err
|
||||
}
|
||||
@ -982,25 +1016,6 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db
|
||||
},
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -1021,12 +1036,14 @@ func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]
|
||||
return r.Repository.GetAllProjectsForTeam(ctx, *input.TeamID)
|
||||
}
|
||||
|
||||
var teams []db.Team
|
||||
var err error
|
||||
if orgRole == "admin" {
|
||||
log.Info("showing all projects for admin")
|
||||
return r.Repository.GetAllProjects(ctx)
|
||||
teams, err = r.Repository.GetAllTeams(ctx)
|
||||
} else {
|
||||
teams, err = r.Repository.GetTeamsForUserIDWhereAdmin(ctx, userID)
|
||||
}
|
||||
|
||||
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")
|
||||
@ -1078,12 +1095,14 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
||||
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 {
|
||||
log.WithError(err).Error("error while getting teams for user ID")
|
||||
return []db.Team{}, err
|
||||
}
|
||||
|
||||
@ -1093,7 +1112,7 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
||||
|
||||
visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID)
|
||||
if err != nil {
|
||||
log.WithField("userID", userID).Info("error while getting visible projects")
|
||||
log.WithField("userID", userID).WithError(err).Error("error while getting visible projects for user ID")
|
||||
return []db.Team{}, err
|
||||
}
|
||||
for _, project := range visibleProjects {
|
||||
@ -1102,7 +1121,10 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) {
|
||||
log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project")
|
||||
team, err := r.Repository.GetTeamByID(ctx, project.TeamID)
|
||||
if err != nil {
|
||||
log.WithField("teamID", project.TeamID).Info("error getting team by id")
|
||||
if err == sql.ErrNoRows {
|
||||
continue
|
||||
}
|
||||
log.WithField("teamID", project.TeamID).WithError(err).Error("error getting team by id")
|
||||
return []db.Team{}, err
|
||||
}
|
||||
teams[project.TeamID.String()] = team
|
||||
|
@ -97,7 +97,7 @@ type Project {
|
||||
id: ID!
|
||||
createdAt: Time!
|
||||
name: String!
|
||||
team: Team!
|
||||
team: Team
|
||||
taskGroups: [TaskGroup!]!
|
||||
members: [Member!]!
|
||||
labels: [ProjectLabel!]!
|
||||
|
@ -25,7 +25,8 @@ type Query {
|
||||
organizations: [Organization!]!
|
||||
users: [UserAccount!]!
|
||||
findUser(input: FindUser!): UserAccount!
|
||||
findProject(input: FindProject!): Project!
|
||||
findProject(input: FindProject!):
|
||||
Project! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT)
|
||||
findTask(input: FindTask!): Task!
|
||||
projects(input: ProjectsFilter): [Project!]!
|
||||
findTeam(input: FindTeam!): Team!
|
||||
|
@ -7,8 +7,7 @@ extend type Mutation {
|
||||
}
|
||||
|
||||
input NewProject {
|
||||
userID: UUID!
|
||||
teamID: UUID!
|
||||
teamID: UUID
|
||||
name: String!
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user