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:
Jordan Knott
2020-09-19 20:20:36 -05:00
parent 28a53f14ad
commit 4277b7b2a8
20 changed files with 327 additions and 178 deletions

View File

@ -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.unmarshalOUUID2githubᚗ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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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!
}

View File

@ -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

View File

@ -97,7 +97,7 @@ type Project {
id: ID!
createdAt: Time!
name: String!
team: Team!
team: Team
taskGroups: [TaskGroup!]!
members: [Member!]!
labels: [ProjectLabel!]!

View File

@ -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!

View File

@ -7,8 +7,7 @@ extend type Mutation {
}
input NewProject {
userID: UUID!
teamID: UUID!
teamID: UUID
name: String!
}