From 262f9cbddae48eea0943d8765caf629df0dca6ea Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Wed, 14 Oct 2020 16:52:32 -0500 Subject: [PATCH] feat: redesign project invite popup --- frontend/src/Projects/Project/index.tsx | 55 +++-- frontend/src/shared/generated/graphql.tsx | 69 +++--- .../graphql/project/inviteProjectMember.ts | 25 -- .../graphql/project/inviteProjectMembers.ts | 25 ++ internal/commands/web.go | 3 + internal/db/querier.go | 2 +- internal/db/query/user_accounts.sql | 4 +- internal/db/user_accounts.sql.go | 32 +-- internal/graph/generated.go | 221 +++++++++++++----- internal/graph/graph.go | 19 +- internal/graph/models_gen.go | 19 +- internal/graph/schema.graphqls | 18 +- internal/graph/schema.resolvers.go | 201 ++++++++-------- internal/graph/schema/project_member.gql | 18 +- internal/logger/logger.go | 90 +------ internal/logger/route_logger.go | 89 +++++++ internal/route/middleware.go | 2 + internal/utils/context.go | 2 + 18 files changed, 530 insertions(+), 364 deletions(-) delete mode 100644 frontend/src/shared/graphql/project/inviteProjectMember.ts create mode 100644 frontend/src/shared/graphql/project/inviteProjectMembers.ts create mode 100644 internal/logger/route_logger.go diff --git a/frontend/src/Projects/Project/index.tsx b/frontend/src/Projects/Project/index.tsx index 544353f..7f96c63 100644 --- a/frontend/src/Projects/Project/index.tsx +++ b/frontend/src/Projects/Project/index.tsx @@ -16,7 +16,7 @@ import { } from 'react-router-dom'; import { useUpdateProjectMemberRoleMutation, - useInviteProjectMemberMutation, + useInviteProjectMembersMutation, useDeleteProjectMemberMutation, useToggleTaskLabelMutation, useUpdateProjectNameMutation, @@ -84,9 +84,10 @@ type InviteUserData = { suerID?: string; }; type UserManagementPopupProps = { + projectID: string; users: Array; projectMembers: Array; - onInviteProjectMember: (data: InviteUserData) => void; + onInviteProjectMembers: (data: Array) => void; }; const VisibiltyPrivateIcon = styled(Lock)` @@ -131,14 +132,14 @@ type MemberFilterOptions = { organization?: boolean; }; -const fetchMembers = async (client: any, options: MemberFilterOptions, input: string, cb: any) => { +const fetchMembers = async (client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) => { if (input && input.trim().length < 3) { return []; } const res = await client.query({ query: gql` query { - searchMembers(input: {SearchFilter:"${input}"}) { + searchMembers(input: {SearchFilter:"${input}", projectID:"${projectID}"}) { similarity confirmed joined @@ -165,7 +166,7 @@ const fetchMembers = async (client: any, options: MemberFilterOptions, input: st emails.push(m.user.email); return { label: m.user.fullName, - value: { id: m.id, type: 0, profileIcon: m.user.profileIcon }, + value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon }, }; }), ]; @@ -214,6 +215,7 @@ const OptionContent = styled.div` `; const UserOption: React.FC = ({ isDisabled, isFocused, innerProps, label, data }) => { + console.log(data); return !isDisabled ? ( = ({ users, projectMembers, onInviteProjectMember }) => { +const UserManagementPopup: React.FC = ({ + projectID, + users, + projectMembers, + onInviteProjectMembers, +}) => { const client = useApolloClient(); const [invitedUsers, setInvitedUsers] = useState | null>(null); return ( option.value.id} placeholder="Email address or username" noOptionsMessage={() => null} - onChange={(e: any) => setInvitedUsers(e ? e.value : null)} + onChange={(e: any) => { + setInvitedUsers(e); + }} isMulti autoFocus cacheOptions @@ -301,13 +311,25 @@ const UserManagementPopup: React.FC = ({ users, projec IndicatorSeparator: null, DropdownIndicator: null, }} - loadOptions={(i, cb) => fetchMembers(client, {}, i, cb)} + loadOptions={(i, cb) => fetchMembers(client, projectID, {}, i, cb)} /> { - // FUCK, gotta rewrite invite member to be MULTIPLE. SHIT! - // onInviteProjectMember(); + if (invitedUsers) { + onInviteProjectMembers( + invitedUsers.map(user => { + if (user.value.type === 0) { + return { + userID: user.value.id, + }; + } + return { + email: user.value.id, + }; + }), + ); + } }} disabled={invitedUsers === null} hoverVariant="none" @@ -398,14 +420,17 @@ const Project = () => { }, }); - const [inviteProjectMember] = useInviteProjectMemberMutation({ + const [inviteProjectMembers] = useInviteProjectMembersMutation({ update: (client, response) => { updateApolloCache( client, FindProjectDocument, cache => produce(cache, draftCache => { - draftCache.findProject.members.push({ ...response.data.inviteProjectMember.member }); + draftCache.findProject.members = [ + ...cache.findProject.members, + ...response.data.inviteProjectMembers.members, + ]; }), { projectID }, ); @@ -472,8 +497,10 @@ const Project = () => { showPopup( $target, { - // /inviteProjectMember({ variables: { userID, projectID } }); + projectID={projectID} + onInviteProjectMembers={members => { + inviteProjectMembers({ variables: { projectID, members } }); + hidePopup(); }} users={data.users} projectMembers={data.findProject.members} diff --git a/frontend/src/shared/generated/graphql.tsx b/frontend/src/shared/generated/graphql.tsx index b470c4c..501e62d 100644 --- a/frontend/src/shared/generated/graphql.tsx +++ b/frontend/src/shared/generated/graphql.tsx @@ -289,7 +289,7 @@ export type Mutation = { deleteTeamMember: DeleteTeamMemberPayload; deleteUserAccount: DeleteUserAccountPayload; duplicateTaskGroup: DuplicateTaskGroupPayload; - inviteProjectMember: InviteProjectMemberPayload; + inviteProjectMembers: InviteProjectMembersPayload; logoutUser: Scalars['Boolean']; removeTaskLabel: Task; setTaskChecklistItemComplete: TaskChecklistItem; @@ -439,8 +439,8 @@ export type MutationDuplicateTaskGroupArgs = { }; -export type MutationInviteProjectMemberArgs = { - input: InviteProjectMember; +export type MutationInviteProjectMembersArgs = { + input: InviteProjectMembers; }; @@ -694,16 +694,21 @@ export type UpdateProjectLabelColor = { labelColorID: Scalars['UUID']; }; -export type InviteProjectMember = { - projectID: Scalars['UUID']; +export type MemberInvite = { userID?: Maybe; email?: Maybe; }; -export type InviteProjectMemberPayload = { - __typename?: 'InviteProjectMemberPayload'; +export type InviteProjectMembers = { + projectID: Scalars['UUID']; + members: Array; +}; + +export type InviteProjectMembersPayload = { + __typename?: 'InviteProjectMembersPayload'; ok: Scalars['Boolean']; - member: Member; + projectID: Scalars['UUID']; + members: Array; }; export type DeleteProjectMember = { @@ -1446,19 +1451,18 @@ export type DeleteProjectMemberMutation = ( ) } ); -export type InviteProjectMemberMutationVariables = { +export type InviteProjectMembersMutationVariables = { projectID: Scalars['UUID']; - userID?: Maybe; - email?: Maybe; + members: Array; }; -export type InviteProjectMemberMutation = ( +export type InviteProjectMembersMutation = ( { __typename?: 'Mutation' } - & { inviteProjectMember: ( - { __typename?: 'InviteProjectMemberPayload' } - & Pick - & { member: ( + & { inviteProjectMembers: ( + { __typename?: 'InviteProjectMembersPayload' } + & Pick + & { members: Array<( { __typename?: 'Member' } & Pick & { profileIcon: ( @@ -1468,7 +1472,7 @@ export type InviteProjectMemberMutation = ( { __typename?: 'Role' } & Pick ) } - ) } + )> } ) } ); @@ -2978,11 +2982,11 @@ export function useDeleteProjectMemberMutation(baseOptions?: ApolloReactHooks.Mu export type DeleteProjectMemberMutationHookResult = ReturnType; export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult; export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions; -export const InviteProjectMemberDocument = gql` - mutation inviteProjectMember($projectID: UUID!, $userID: UUID, $email: String) { - inviteProjectMember(input: {projectID: $projectID, userID: $userID, email: $email}) { +export const InviteProjectMembersDocument = gql` + mutation inviteProjectMembers($projectID: UUID!, $members: [MemberInvite!]!) { + inviteProjectMembers(input: {projectID: $projectID, members: $members}) { ok - member { + members { id fullName profileIcon { @@ -2999,33 +3003,32 @@ export const InviteProjectMemberDocument = gql` } } `; -export type InviteProjectMemberMutationFn = ApolloReactCommon.MutationFunction; +export type InviteProjectMembersMutationFn = ApolloReactCommon.MutationFunction; /** - * __useInviteProjectMemberMutation__ + * __useInviteProjectMembersMutation__ * - * To run a mutation, you first call `useInviteProjectMemberMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useInviteProjectMemberMutation` returns a tuple that includes: + * To run a mutation, you first call `useInviteProjectMembersMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useInviteProjectMembersMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example - * const [inviteProjectMemberMutation, { data, loading, error }] = useInviteProjectMemberMutation({ + * const [inviteProjectMembersMutation, { data, loading, error }] = useInviteProjectMembersMutation({ * variables: { * projectID: // value for 'projectID' - * userID: // value for 'userID' - * email: // value for 'email' + * members: // value for 'members' * }, * }); */ -export function useInviteProjectMemberMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { - return ApolloReactHooks.useMutation(InviteProjectMemberDocument, baseOptions); +export function useInviteProjectMembersMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(InviteProjectMembersDocument, baseOptions); } -export type InviteProjectMemberMutationHookResult = ReturnType; -export type InviteProjectMemberMutationResult = ApolloReactCommon.MutationResult; -export type InviteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions; +export type InviteProjectMembersMutationHookResult = ReturnType; +export type InviteProjectMembersMutationResult = ApolloReactCommon.MutationResult; +export type InviteProjectMembersMutationOptions = ApolloReactCommon.BaseMutationOptions; export const UpdateProjectMemberRoleDocument = gql` mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) { updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) { diff --git a/frontend/src/shared/graphql/project/inviteProjectMember.ts b/frontend/src/shared/graphql/project/inviteProjectMember.ts deleted file mode 100644 index bc00225..0000000 --- a/frontend/src/shared/graphql/project/inviteProjectMember.ts +++ /dev/null @@ -1,25 +0,0 @@ -import gql from 'graphql-tag'; - -export const INVITE_PROJECT_MEMBER_MUTATION = gql` - mutation inviteProjectMember($projectID: UUID!, $userID: UUID, $email: String) { - inviteProjectMember(input: { projectID: $projectID, userID: $userID, email: $email }) { - ok - member { - id - fullName - profileIcon { - url - initials - bgColor - } - username - role { - code - name - } - } - } - } -`; - -export default INVITE_PROJECT_MEMBER_MUTATION; diff --git a/frontend/src/shared/graphql/project/inviteProjectMembers.ts b/frontend/src/shared/graphql/project/inviteProjectMembers.ts new file mode 100644 index 0000000..e13b7cc --- /dev/null +++ b/frontend/src/shared/graphql/project/inviteProjectMembers.ts @@ -0,0 +1,25 @@ +import gql from 'graphql-tag'; + +export const INVITE_PROJECT_MEMBERS_MUTATION = gql` + mutation inviteProjectMembers($projectID: UUID!, $members: [MemberInvite!]!) { + inviteProjectMembers(input: { projectID: $projectID, members: $members }) { + ok + members { + id + fullName + profileIcon { + url + initials + bgColor + } + username + role { + code + name + } + } + } + } +`; + +export default INVITE_PROJECT_MEMBERS_MUTATION; diff --git a/internal/commands/web.go b/internal/commands/web.go index b752ef8..d51da7c 100644 --- a/internal/commands/web.go +++ b/internal/commands/web.go @@ -78,8 +78,11 @@ func newWebCmd() *cobra.Command { return http.ListenAndServe(viper.GetString("server.hostname"), r) }, } + cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server") + viper.BindPFlag("migrate", cc.Flags().Lookup("migrate")) + viper.SetDefault("migrate", false) return cc } diff --git a/internal/db/querier.go b/internal/db/querier.go index 34f3a1d..7ffe0f0 100644 --- a/internal/db/querier.go +++ b/internal/db/querier.go @@ -61,7 +61,7 @@ type Querier interface { GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error) GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error) GetLabelColors(ctx context.Context) ([]LabelColor, error) - GetMemberData(ctx context.Context) ([]GetMemberDataRow, error) + GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error) GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error) diff --git a/internal/db/query/user_accounts.sql b/internal/db/query/user_accounts.sql index f98fa29..3b5c4da 100644 --- a/internal/db/query/user_accounts.sql +++ b/internal/db/query/user_accounts.sql @@ -16,7 +16,9 @@ UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1 RETURNING *; -- name: GetMemberData :many -SELECT username, full_name, email, user_id FROM user_account; +SELECT * FROM user_account + WHERE username != 'system' + AND user_id NOT IN (SELECT user_id FROM project_member WHERE project_id = $1); -- name: UpdateUserAccountInfo :one UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5 diff --git a/internal/db/user_accounts.sql.go b/internal/db/user_accounts.sql.go index ee62982..ca2fb77 100644 --- a/internal/db/user_accounts.sql.go +++ b/internal/db/user_accounts.sql.go @@ -102,30 +102,32 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) } const getMemberData = `-- name: GetMemberData :many -SELECT username, full_name, email, user_id FROM user_account +SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account + WHERE username != 'system' + AND user_id NOT IN (SELECT user_id FROM project_member WHERE project_id = $1) ` -type GetMemberDataRow struct { - Username string `json:"username"` - FullName string `json:"full_name"` - Email string `json:"email"` - UserID uuid.UUID `json:"user_id"` -} - -func (q *Queries) GetMemberData(ctx context.Context) ([]GetMemberDataRow, error) { - rows, err := q.db.QueryContext(ctx, getMemberData) +func (q *Queries) GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error) { + rows, err := q.db.QueryContext(ctx, getMemberData, projectID) if err != nil { return nil, err } defer rows.Close() - var items []GetMemberDataRow + var items []UserAccount for rows.Next() { - var i GetMemberDataRow + var i UserAccount if err := rows.Scan( - &i.Username, - &i.FullName, - &i.Email, &i.UserID, + &i.CreatedAt, + &i.Email, + &i.Username, + &i.PasswordHash, + &i.ProfileBgColor, + &i.FullName, + &i.Initials, + &i.ProfileAvatarUrl, + &i.RoleCode, + &i.Bio, ); err != nil { return nil, err } diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 05a7795..a0c1acb 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -127,9 +127,10 @@ type ComplexityRoot struct { TaskGroup func(childComplexity int) int } - InviteProjectMemberPayload struct { - Member func(childComplexity int) int - Ok func(childComplexity int) int + InviteProjectMembersPayload struct { + Members func(childComplexity int) int + Ok func(childComplexity int) int + ProjectID func(childComplexity int) int } LabelColor struct { @@ -194,7 +195,7 @@ type ComplexityRoot struct { DeleteTeamMember func(childComplexity int, input DeleteTeamMember) int DeleteUserAccount func(childComplexity int, input DeleteUserAccount) int DuplicateTaskGroup func(childComplexity int, input DuplicateTaskGroup) int - InviteProjectMember func(childComplexity int, input InviteProjectMember) int + InviteProjectMembers func(childComplexity int, input InviteProjectMembers) int LogoutUser func(childComplexity int, input LogoutUser) int RemoveTaskLabel func(childComplexity int, input *RemoveTaskLabelInput) int SetTaskChecklistItemComplete func(childComplexity int, input SetTaskChecklistItemComplete) int @@ -454,7 +455,7 @@ type MutationResolver interface { UpdateProjectLabel(ctx context.Context, input UpdateProjectLabel) (*db.ProjectLabel, error) UpdateProjectLabelName(ctx context.Context, input UpdateProjectLabelName) (*db.ProjectLabel, error) UpdateProjectLabelColor(ctx context.Context, input UpdateProjectLabelColor) (*db.ProjectLabel, error) - InviteProjectMember(ctx context.Context, input InviteProjectMember) (*InviteProjectMemberPayload, error) + InviteProjectMembers(ctx context.Context, input InviteProjectMembers) (*InviteProjectMembersPayload, error) DeleteProjectMember(ctx context.Context, input DeleteProjectMember) (*DeleteProjectMemberPayload, error) UpdateProjectMemberRole(ctx context.Context, input UpdateProjectMemberRole) (*UpdateProjectMemberRolePayload, error) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) @@ -801,19 +802,26 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.DuplicateTaskGroupPayload.TaskGroup(childComplexity), true - case "InviteProjectMemberPayload.member": - if e.complexity.InviteProjectMemberPayload.Member == nil { + case "InviteProjectMembersPayload.members": + if e.complexity.InviteProjectMembersPayload.Members == nil { break } - return e.complexity.InviteProjectMemberPayload.Member(childComplexity), true + return e.complexity.InviteProjectMembersPayload.Members(childComplexity), true - case "InviteProjectMemberPayload.ok": - if e.complexity.InviteProjectMemberPayload.Ok == nil { + case "InviteProjectMembersPayload.ok": + if e.complexity.InviteProjectMembersPayload.Ok == nil { break } - return e.complexity.InviteProjectMemberPayload.Ok(childComplexity), true + return e.complexity.InviteProjectMembersPayload.Ok(childComplexity), true + + case "InviteProjectMembersPayload.projectID": + if e.complexity.InviteProjectMembersPayload.ProjectID == nil { + break + } + + return e.complexity.InviteProjectMembersPayload.ProjectID(childComplexity), true case "LabelColor.colorHex": if e.complexity.LabelColor.ColorHex == nil { @@ -1257,17 +1265,17 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DuplicateTaskGroup(childComplexity, args["input"].(DuplicateTaskGroup)), true - case "Mutation.inviteProjectMember": - if e.complexity.Mutation.InviteProjectMember == nil { + case "Mutation.inviteProjectMembers": + if e.complexity.Mutation.InviteProjectMembers == nil { break } - args, err := ec.field_Mutation_inviteProjectMember_args(context.TODO(), rawArgs) + args, err := ec.field_Mutation_inviteProjectMembers_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.InviteProjectMember(childComplexity, args["input"].(InviteProjectMember)), true + return e.complexity.Mutation.InviteProjectMembers(childComplexity, args["input"].(InviteProjectMembers)), true case "Mutation.logoutUser": if e.complexity.Mutation.LogoutUser == nil { @@ -2869,24 +2877,28 @@ input UpdateProjectLabelColor { } extend type Mutation { - # TODO: rename to inviteProjectMember - inviteProjectMember(input: InviteProjectMember!): - InviteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + inviteProjectMembers(input: InviteProjectMembers!): + InviteProjectMembersPayload! @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 InviteProjectMember { - projectID: UUID! +input MemberInvite { userID: UUID email: String } -type InviteProjectMemberPayload { +input InviteProjectMembers { + projectID: UUID! + members: [MemberInvite!]! +} + +type InviteProjectMembersPayload { ok: Boolean! - member: Member! + projectID: UUID! + members: [Member!]! } input DeleteProjectMember { @@ -3715,12 +3727,12 @@ func (ec *executionContext) field_Mutation_duplicateTaskGroup_args(ctx context.C return args, nil } -func (ec *executionContext) field_Mutation_inviteProjectMember_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { +func (ec *executionContext) field_Mutation_inviteProjectMembers_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - var arg0 InviteProjectMember + var arg0 InviteProjectMembers if tmp, ok := rawArgs["input"]; ok { - arg0, err = ec.unmarshalNInviteProjectMember2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMember(ctx, tmp) + arg0, err = ec.unmarshalNInviteProjectMembers2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMembers(ctx, tmp) if err != nil { return nil, err } @@ -5179,7 +5191,7 @@ func (ec *executionContext) _DuplicateTaskGroupPayload_taskGroup(ctx context.Con return ec.marshalNTaskGroup2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskGroup(ctx, field.Selections, res) } -func (ec *executionContext) _InviteProjectMemberPayload_ok(ctx context.Context, field graphql.CollectedField, obj *InviteProjectMemberPayload) (ret graphql.Marshaler) { +func (ec *executionContext) _InviteProjectMembersPayload_ok(ctx context.Context, field graphql.CollectedField, obj *InviteProjectMembersPayload) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -5187,7 +5199,7 @@ func (ec *executionContext) _InviteProjectMemberPayload_ok(ctx context.Context, } }() fc := &graphql.FieldContext{ - Object: "InviteProjectMemberPayload", + Object: "InviteProjectMembersPayload", Field: field, Args: nil, IsMethod: false, @@ -5213,7 +5225,7 @@ func (ec *executionContext) _InviteProjectMemberPayload_ok(ctx context.Context, return ec.marshalNBoolean2bool(ctx, field.Selections, res) } -func (ec *executionContext) _InviteProjectMemberPayload_member(ctx context.Context, field graphql.CollectedField, obj *InviteProjectMemberPayload) (ret graphql.Marshaler) { +func (ec *executionContext) _InviteProjectMembersPayload_projectID(ctx context.Context, field graphql.CollectedField, obj *InviteProjectMembersPayload) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -5221,7 +5233,7 @@ func (ec *executionContext) _InviteProjectMemberPayload_member(ctx context.Conte } }() fc := &graphql.FieldContext{ - Object: "InviteProjectMemberPayload", + Object: "InviteProjectMembersPayload", Field: field, Args: nil, IsMethod: false, @@ -5230,7 +5242,7 @@ func (ec *executionContext) _InviteProjectMemberPayload_member(ctx context.Conte ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Member, nil + return obj.ProjectID, nil }) if err != nil { ec.Error(ctx, err) @@ -5242,9 +5254,43 @@ func (ec *executionContext) _InviteProjectMemberPayload_member(ctx context.Conte } return graphql.Null } - res := resTmp.(*Member) + res := resTmp.(uuid.UUID) fc.Result = res - return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res) + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) _InviteProjectMembersPayload_members(ctx context.Context, field graphql.CollectedField, obj *InviteProjectMembersPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "InviteProjectMembersPayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Members, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]Member) + fc.Result = res + return ec.marshalNMember2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberᚄ(ctx, field.Selections, res) } func (ec *executionContext) _LabelColor_id(ctx context.Context, field graphql.CollectedField, obj *db.LabelColor) (ret graphql.Marshaler) { @@ -6545,7 +6591,7 @@ func (ec *executionContext) _Mutation_updateProjectLabelColor(ctx context.Contex return ec.marshalNProjectLabel2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐProjectLabel(ctx, field.Selections, res) } -func (ec *executionContext) _Mutation_inviteProjectMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_inviteProjectMembers(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -6561,7 +6607,7 @@ func (ec *executionContext) _Mutation_inviteProjectMember(ctx context.Context, f ctx = graphql.WithFieldContext(ctx, fc) rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Mutation_inviteProjectMember_args(ctx, rawArgs) + args, err := ec.field_Mutation_inviteProjectMembers_args(ctx, rawArgs) if err != nil { ec.Error(ctx, err) return graphql.Null @@ -6570,7 +6616,7 @@ func (ec *executionContext) _Mutation_inviteProjectMember(ctx context.Context, f resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().InviteProjectMember(rctx, args["input"].(InviteProjectMember)) + return ec.resolvers.Mutation().InviteProjectMembers(rctx, args["input"].(InviteProjectMembers)) } directive1 := func(ctx context.Context) (interface{}, error) { roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) @@ -6598,10 +6644,10 @@ func (ec *executionContext) _Mutation_inviteProjectMember(ctx context.Context, f if tmp == nil { return nil, nil } - if data, ok := tmp.(*InviteProjectMemberPayload); ok { + if data, ok := tmp.(*InviteProjectMembersPayload); ok { return data, nil } - return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.InviteProjectMemberPayload`, tmp) + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.InviteProjectMembersPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6613,9 +6659,9 @@ func (ec *executionContext) _Mutation_inviteProjectMember(ctx context.Context, f } return graphql.Null } - res := resTmp.(*InviteProjectMemberPayload) + res := resTmp.(*InviteProjectMembersPayload) fc.Result = res - return ec.marshalNInviteProjectMemberPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMemberPayload(ctx, field.Selections, res) + return ec.marshalNInviteProjectMembersPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMembersPayload(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_deleteProjectMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -15403,8 +15449,8 @@ func (ec *executionContext) unmarshalInputFindUser(ctx context.Context, obj inte return it, nil } -func (ec *executionContext) unmarshalInputInviteProjectMember(ctx context.Context, obj interface{}) (InviteProjectMember, error) { - var it InviteProjectMember +func (ec *executionContext) unmarshalInputInviteProjectMembers(ctx context.Context, obj interface{}) (InviteProjectMembers, error) { + var it InviteProjectMembers var asMap = obj.(map[string]interface{}) for k, v := range asMap { @@ -15415,15 +15461,9 @@ func (ec *executionContext) unmarshalInputInviteProjectMember(ctx context.Contex if err != nil { return it, err } - case "userID": + case "members": var err error - it.UserID, err = ec.unmarshalOUUID2ᚖgithubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) - if err != nil { - return it, err - } - case "email": - var err error - it.Email, err = ec.unmarshalOString2ᚖstring(ctx, v) + it.Members, err = ec.unmarshalNMemberInvite2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberInviteᚄ(ctx, v) if err != nil { return it, err } @@ -15451,6 +15491,30 @@ func (ec *executionContext) unmarshalInputLogoutUser(ctx context.Context, obj in return it, nil } +func (ec *executionContext) unmarshalInputMemberInvite(ctx context.Context, obj interface{}) (MemberInvite, error) { + var it MemberInvite + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "userID": + var err error + it.UserID, err = ec.unmarshalOUUID2ᚖgithubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "email": + var err error + it.Email, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputMemberSearchFilter(ctx context.Context, obj interface{}) (MemberSearchFilter, error) { var it MemberSearchFilter var asMap = obj.(map[string]interface{}) @@ -16797,24 +16861,29 @@ func (ec *executionContext) _DuplicateTaskGroupPayload(ctx context.Context, sel return out } -var inviteProjectMemberPayloadImplementors = []string{"InviteProjectMemberPayload"} +var inviteProjectMembersPayloadImplementors = []string{"InviteProjectMembersPayload"} -func (ec *executionContext) _InviteProjectMemberPayload(ctx context.Context, sel ast.SelectionSet, obj *InviteProjectMemberPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, inviteProjectMemberPayloadImplementors) +func (ec *executionContext) _InviteProjectMembersPayload(ctx context.Context, sel ast.SelectionSet, obj *InviteProjectMembersPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, inviteProjectMembersPayloadImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("InviteProjectMemberPayload") + out.Values[i] = graphql.MarshalString("InviteProjectMembersPayload") case "ok": - out.Values[i] = ec._InviteProjectMemberPayload_ok(ctx, field, obj) + out.Values[i] = ec._InviteProjectMembersPayload_ok(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } - case "member": - out.Values[i] = ec._InviteProjectMemberPayload_member(ctx, field, obj) + case "projectID": + out.Values[i] = ec._InviteProjectMembersPayload_projectID(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "members": + out.Values[i] = ec._InviteProjectMembersPayload_members(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } @@ -17108,8 +17177,8 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } - case "inviteProjectMember": - out.Values[i] = ec._Mutation_inviteProjectMember(ctx, field) + case "inviteProjectMembers": + out.Values[i] = ec._Mutation_inviteProjectMembers(ctx, field) if out.Values[i] == graphql.Null { invalids++ } @@ -19692,22 +19761,22 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } -func (ec *executionContext) unmarshalNInviteProjectMember2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMember(ctx context.Context, v interface{}) (InviteProjectMember, error) { - return ec.unmarshalInputInviteProjectMember(ctx, v) +func (ec *executionContext) unmarshalNInviteProjectMembers2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMembers(ctx context.Context, v interface{}) (InviteProjectMembers, error) { + return ec.unmarshalInputInviteProjectMembers(ctx, v) } -func (ec *executionContext) marshalNInviteProjectMemberPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMemberPayload(ctx context.Context, sel ast.SelectionSet, v InviteProjectMemberPayload) graphql.Marshaler { - return ec._InviteProjectMemberPayload(ctx, sel, &v) +func (ec *executionContext) marshalNInviteProjectMembersPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMembersPayload(ctx context.Context, sel ast.SelectionSet, v InviteProjectMembersPayload) graphql.Marshaler { + return ec._InviteProjectMembersPayload(ctx, sel, &v) } -func (ec *executionContext) marshalNInviteProjectMemberPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMemberPayload(ctx context.Context, sel ast.SelectionSet, v *InviteProjectMemberPayload) graphql.Marshaler { +func (ec *executionContext) marshalNInviteProjectMembersPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMembersPayload(ctx context.Context, sel ast.SelectionSet, v *InviteProjectMembersPayload) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "must not be null") } return graphql.Null } - return ec._InviteProjectMemberPayload(ctx, sel, v) + return ec._InviteProjectMembersPayload(ctx, sel, v) } func (ec *executionContext) marshalNLabelColor2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐLabelColor(ctx context.Context, sel ast.SelectionSet, v db.LabelColor) graphql.Marshaler { @@ -19830,6 +19899,30 @@ func (ec *executionContext) marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskc return ec._Member(ctx, sel, v) } +func (ec *executionContext) unmarshalNMemberInvite2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberInvite(ctx context.Context, v interface{}) (MemberInvite, error) { + return ec.unmarshalInputMemberInvite(ctx, v) +} + +func (ec *executionContext) unmarshalNMemberInvite2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberInviteᚄ(ctx context.Context, v interface{}) ([]MemberInvite, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]MemberInvite, len(vSlice)) + for i := range vSlice { + res[i], err = ec.unmarshalNMemberInvite2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberInvite(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + func (ec *executionContext) marshalNMemberList2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberList(ctx context.Context, sel ast.SelectionSet, v MemberList) graphql.Marshaler { return ec._MemberList(ctx, sel, &v) } diff --git a/internal/graph/graph.go b/internal/graph/graph.go index cccec02..50316ea 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -19,6 +19,7 @@ import ( "github.com/google/uuid" "github.com/jordanknott/taskcafe/internal/auth" "github.com/jordanknott/taskcafe/internal/db" + "github.com/jordanknott/taskcafe/internal/logger" "github.com/jordanknott/taskcafe/internal/utils" log "github.com/sirupsen/logrus" "github.com/vektah/gqlparser/v2/gqlerror" @@ -63,10 +64,10 @@ func NewHandler(repo db.Repository) http.Handler { default: fieldName = "ProjectID" } - log.WithFields(log.Fields{"typeArg": typeArg, "fieldName": fieldName}).Info("getting field by name") + logger.New(ctx).WithFields(log.Fields{"typeArg": typeArg, "fieldName": fieldName}).Info("getting field by name") subjectField := val.FieldByName(fieldName) if !subjectField.IsValid() { - log.Error("subject field name does not exist on input type") + logger.New(ctx).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() { @@ -76,13 +77,13 @@ func NewHandler(repo db.Repository) http.Handler { } subjectID, ok = subjectField.Interface().(uuid.UUID) if !ok { - log.Error("error while casting subject UUID") + logger.New(ctx).Error("error while casting subject UUID") return nil, errors.New("error while casting subject uuid") } var err error if level == ActionLevelProject { - log.WithFields(log.Fields{"subjectID": subjectID}).Info("fetching subject ID by typeArg") + logger.New(ctx).WithFields(log.Fields{"subjectID": subjectID}).Info("fetching subject ID by typeArg") if typeArg == ObjectTypeTask { subjectID, err = repo.GetProjectIDForTask(ctx, subjectID) } @@ -96,7 +97,7 @@ func NewHandler(repo db.Repository) http.Handler { subjectID, err = repo.GetProjectIDForTaskChecklistItem(ctx, subjectID) } if err != nil { - log.WithError(err).Error("error while getting subject ID") + logger.New(ctx).WithError(err).Error("error while getting subject ID") return nil, err } projectRoles, err := GetProjectRoles(ctx, repo, subjectID) @@ -109,13 +110,13 @@ func NewHandler(repo db.Repository) http.Handler { }, } } - log.WithError(err).Error("error while getting project roles") + logger.New(ctx).WithError(err).Error("error while getting project roles") return nil, err } for _, validRole := range roles { - log.WithFields(log.Fields{"validRole": validRole}).Info("checking role") + logger.New(ctx).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") + logger.New(ctx).WithFields(log.Fields{"teamRole": projectRoles.TeamRole, "projectRole": projectRoles.ProjectRole}).Info("is team or project role") return next(ctx) } } @@ -132,7 +133,7 @@ func NewHandler(repo db.Repository) http.Handler { } role, err := repo.GetTeamRoleForUserID(ctx, db.GetTeamRoleForUserIDParams{UserID: userID, TeamID: subjectID}) if err != nil { - log.WithError(err).Error("error while getting team roles for user ID") + logger.New(ctx).WithError(err).Error("error while getting team roles for user ID") return nil, err } for _, validRole := range roles { diff --git a/internal/graph/models_gen.go b/internal/graph/models_gen.go index 271b584..2a293ce 100644 --- a/internal/graph/models_gen.go +++ b/internal/graph/models_gen.go @@ -177,15 +177,15 @@ type FindUser struct { UserID uuid.UUID `json:"userID"` } -type InviteProjectMember struct { - ProjectID uuid.UUID `json:"projectID"` - UserID *uuid.UUID `json:"userID"` - Email *string `json:"email"` +type InviteProjectMembers struct { + ProjectID uuid.UUID `json:"projectID"` + Members []MemberInvite `json:"members"` } -type InviteProjectMemberPayload struct { - Ok bool `json:"ok"` - Member *Member `json:"member"` +type InviteProjectMembersPayload struct { + Ok bool `json:"ok"` + ProjectID uuid.UUID `json:"projectID"` + Members []Member `json:"members"` } type LogoutUser struct { @@ -208,6 +208,11 @@ type Member struct { Member *MemberList `json:"member"` } +type MemberInvite struct { + UserID *uuid.UUID `json:"userID"` + Email *string `json:"email"` +} + type MemberList struct { Teams []db.Team `json:"teams"` Projects []db.Project `json:"projects"` diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls index 1ef322e..ddd5757 100644 --- a/internal/graph/schema.graphqls +++ b/internal/graph/schema.graphqls @@ -338,24 +338,28 @@ input UpdateProjectLabelColor { } extend type Mutation { - # TODO: rename to inviteProjectMember - inviteProjectMember(input: InviteProjectMember!): - InviteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + inviteProjectMembers(input: InviteProjectMembers!): + InviteProjectMembersPayload! @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 InviteProjectMember { - projectID: UUID! +input MemberInvite { userID: UUID email: String } -type InviteProjectMemberPayload { +input InviteProjectMembers { + projectID: UUID! + members: [MemberInvite!]! +} + +type InviteProjectMembersPayload { ok: Boolean! - member: Member! + projectID: UUID! + members: [Member!]! } input DeleteProjectMember { diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index f705435..72b32ba 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/jordanknott/taskcafe/internal/auth" "github.com/jordanknott/taskcafe/internal/db" + "github.com/jordanknott/taskcafe/internal/logger" "github.com/lithammer/fuzzysearch/fuzzy" log "github.com/sirupsen/logrus" "github.com/vektah/gqlparser/v2/gqlerror" @@ -29,7 +30,7 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) return &db.Project{}, errors.New("user id is missing") } createdAt := time.Now().UTC() - log.WithFields(log.Fields{"name": input.Name, "teamID": input.TeamID}).Info("creating new project") + logger.New(ctx).WithFields(log.Fields{"name": input.Name, "teamID": input.TeamID}).Info("creating new project") var project db.Project var err error if input.TeamID == nil { @@ -38,10 +39,10 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) Name: input.Name, }) if err != nil { - log.WithError(err).Error("error while creating project") + logger.New(ctx).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") + logger.New(ctx).WithField("projectID", project.ProjectID).Info("creating personal project link") } else { project, err = r.Repository.CreateTeamProject(ctx, db.CreateTeamProjectParams{ CreatedAt: createdAt, @@ -49,13 +50,13 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) TeamID: *input.TeamID, }) if err != nil { - log.WithError(err).Error("error while creating project") + logger.New(ctx).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") + logger.New(ctx).WithError(err).Error("error while creating initial project member") return &db.Project{}, err } return &project, nil @@ -124,53 +125,55 @@ func (r *mutationResolver) UpdateProjectLabelColor(ctx context.Context, input Up return &label, err } -func (r *mutationResolver) InviteProjectMember(ctx context.Context, input InviteProjectMember) (*InviteProjectMemberPayload, error) { - if input.Email != nil && input.UserID != nil { - return &InviteProjectMemberPayload{Ok: false}, &gqlerror.Error{ - Message: "Both email and userID can not be used to invite a project member", - Extensions: map[string]interface{}{ - "code": "403", - }, +func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input InviteProjectMembers) (*InviteProjectMembersPayload, error) { + members := []Member{} + for _, invitedMember := range input.Members { + if invitedMember.Email != nil && invitedMember.UserID != nil { + return &InviteProjectMembersPayload{Ok: false}, &gqlerror.Error{ + Message: "Both email and userID can not be used to invite a project member", + Extensions: map[string]interface{}{ + "code": "403", + }, + } + } else if invitedMember.Email == nil && invitedMember.UserID == nil { + return &InviteProjectMembersPayload{Ok: false}, &gqlerror.Error{ + Message: "Either email or userID must be set to invite a project member", + Extensions: map[string]interface{}{ + "code": "403", + }, + } } - } else if input.Email == nil && input.UserID == nil { - return &InviteProjectMemberPayload{Ok: false}, &gqlerror.Error{ - Message: "Either email or userID must be set to invite a project member", - Extensions: map[string]interface{}{ - "code": "403", - }, + if invitedMember.UserID != nil { + addedAt := time.Now().UTC() + _, err := r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: input.ProjectID, UserID: *invitedMember.UserID, AddedAt: addedAt, RoleCode: "member"}) + if err != nil { + return &InviteProjectMembersPayload{Ok: false}, err + } + user, err := r.Repository.GetUserAccountByID(ctx, *invitedMember.UserID) + if err != nil && err != sql.ErrNoRows { + return &InviteProjectMembersPayload{Ok: false}, 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: *invitedMember.UserID, ProjectID: input.ProjectID}) + if err != nil { + return &InviteProjectMembersPayload{Ok: false}, err + } + members = append(members, Member{ + ID: *invitedMember.UserID, + FullName: user.FullName, + Username: user.Username, + ProfileIcon: profileIcon, + Role: &db.Role{Code: role.Code, Name: role.Name}, + }) } } - if input.UserID != nil { - addedAt := time.Now().UTC() - _, err := r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: input.ProjectID, UserID: *input.UserID, AddedAt: addedAt, RoleCode: "member"}) - if err != nil { - return &InviteProjectMemberPayload{Ok: false}, err - } - user, err := r.Repository.GetUserAccountByID(ctx, *input.UserID) - if err != nil && err != sql.ErrNoRows { - return &InviteProjectMemberPayload{Ok: false}, 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: *input.UserID, ProjectID: input.ProjectID}) - if err != nil { - return &InviteProjectMemberPayload{Ok: false}, err - } - return &InviteProjectMemberPayload{Ok: true, Member: &Member{ - ID: *input.UserID, - FullName: user.FullName, - Username: user.Username, - ProfileIcon: profileIcon, - Role: &db.Role{Code: role.Code, Name: role.Name}, - }}, nil - } - // invite user - return &InviteProjectMemberPayload{Ok: false}, errors.New("not implemented") + return &InviteProjectMembersPayload{Ok: false, ProjectID: input.ProjectID, Members: members}, nil } func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input DeleteProjectMember) (*DeleteProjectMemberPayload, error) { @@ -202,18 +205,18 @@ func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input Delete func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input UpdateProjectMemberRole) (*UpdateProjectMemberRolePayload, error) { user, err := r.Repository.GetUserAccountByID(ctx, input.UserID) if err != nil { - log.WithError(err).Error("get user account") + logger.New(ctx).WithError(err).Error("get user account") return &UpdateProjectMemberRolePayload{Ok: false}, err } _, err = r.Repository.UpdateProjectMemberRole(ctx, db.UpdateProjectMemberRoleParams{ProjectID: input.ProjectID, UserID: input.UserID, RoleCode: input.RoleCode.String()}) if err != nil { - log.WithError(err).Error("update project member role") + logger.New(ctx).WithError(err).Error("update project member role") return &UpdateProjectMemberRolePayload{Ok: false}, err } role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: user.UserID, ProjectID: input.ProjectID}) if err != nil { - log.WithError(err).Error("get role for project member") + logger.New(ctx).WithError(err).Error("get role for project member") return &UpdateProjectMemberRolePayload{Ok: false}, err } var url *string @@ -232,17 +235,17 @@ func (r *mutationResolver) UpdateProjectMemberRole(ctx context.Context, input Up func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) { createdAt := time.Now().UTC() - log.WithFields(log.Fields{"positon": input.Position, "taskGroupID": input.TaskGroupID}).Info("creating task") + logger.New(ctx).WithFields(log.Fields{"positon": input.Position, "taskGroupID": input.TaskGroupID}).Info("creating task") task, err := r.Repository.CreateTask(ctx, db.CreateTaskParams{input.TaskGroupID, createdAt, input.Name, input.Position}) if err != nil { - log.WithError(err).Error("issue while creating task") + logger.New(ctx).WithError(err).Error("issue while creating task") return &db.Task{}, err } return &task, nil } func (r *mutationResolver) DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error) { - log.WithFields(log.Fields{ + logger.New(ctx).WithFields(log.Fields{ "taskID": input.TaskID, }).Info("deleting task") err := r.Repository.DeleteTaskByID(ctx, input.TaskID) @@ -299,8 +302,8 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInput) (*db.Task, error) { assignedDate := time.Now().UTC() assignedTask, err := r.Repository.CreateTaskAssigned(ctx, db.CreateTaskAssignedParams{input.TaskID, input.UserID, assignedDate}) - log.WithFields(log.Fields{ - "userID": assignedTask.UserID, + logger.New(ctx).WithFields(log.Fields{ + "assignedUserID": assignedTask.UserID, "taskID": assignedTask.TaskID, "assignedTaskID": assignedTask.TaskAssignedID, }).Info("assigned task") @@ -610,7 +613,7 @@ func (r *mutationResolver) ToggleTaskLabel(ctx context.Context, input ToggleTask createdAt := time.Now().UTC() if err == sql.ErrNoRows { - log.WithFields(log.Fields{"err": err}).Warning("no rows") + logger.New(ctx).WithFields(log.Fields{"err": err}).Warning("no rows") _, err := r.Repository.CreateTaskLabelForTask(ctx, db.CreateTaskLabelForTaskParams{ TaskID: input.TaskID, ProjectLabelID: input.ProjectLabelID, @@ -643,17 +646,17 @@ func (r *mutationResolver) ToggleTaskLabel(ctx context.Context, input ToggleTask func (r *mutationResolver) DeleteTeam(ctx context.Context, input DeleteTeam) (*DeleteTeamPayload, error) { team, err := r.Repository.GetTeamByID(ctx, input.TeamID) if err != nil { - log.Error(err) + logger.New(ctx).Error(err) return &DeleteTeamPayload{Ok: false}, err } projects, err := r.Repository.GetAllProjectsForTeam(ctx, input.TeamID) if err != nil { - log.Error(err) + logger.New(ctx).Error(err) return &DeleteTeamPayload{Ok: false}, err } err = r.Repository.DeleteTeamByID(ctx, input.TeamID) if err != nil { - log.Error(err) + logger.New(ctx).Error(err) return &DeleteTeamPayload{Ok: false}, err } @@ -708,18 +711,18 @@ func (r *mutationResolver) CreateTeamMember(ctx context.Context, input CreateTea func (r *mutationResolver) UpdateTeamMemberRole(ctx context.Context, input UpdateTeamMemberRole) (*UpdateTeamMemberRolePayload, error) { user, err := r.Repository.GetUserAccountByID(ctx, input.UserID) if err != nil { - log.WithError(err).Error("get user account") + logger.New(ctx).WithError(err).Error("get user account") return &UpdateTeamMemberRolePayload{Ok: false}, err } _, err = r.Repository.UpdateTeamMemberRole(ctx, db.UpdateTeamMemberRoleParams{TeamID: input.TeamID, UserID: input.UserID, RoleCode: input.RoleCode.String()}) if err != nil { - log.WithError(err).Error("update project member role") + logger.New(ctx).WithError(err).Error("update project member role") return &UpdateTeamMemberRolePayload{Ok: false}, err } role, err := r.Repository.GetRoleForTeamMember(ctx, db.GetRoleForTeamMemberParams{UserID: user.UserID, TeamID: input.TeamID}) if err != nil { - log.WithError(err).Error("get role for project member") + logger.New(ctx).WithError(err).Error("get role for project member") return &UpdateTeamMemberRolePayload{Ok: false}, err } var url *string @@ -871,9 +874,9 @@ func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uu } func (r *notificationResolver) Entity(ctx context.Context, obj *db.Notification) (*NotificationEntity, error) { - log.WithFields(log.Fields{"notificationID": obj.NotificationID}).Info("fetching entity for notification") + logger.New(ctx).WithFields(log.Fields{"notificationID": obj.NotificationID}).Info("fetching entity for notification") entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID) - log.WithFields(log.Fields{"entityID": entity.EntityID}).Info("fetched entity") + logger.New(ctx).WithFields(log.Fields{"entityID": entity.EntityID}).Info("fetched entity") if err != nil { return &NotificationEntity{}, err } @@ -905,7 +908,7 @@ func (r *notificationResolver) Actor(ctx context.Context, obj *db.Notification) if err != nil { return &NotificationActor{}, err } - log.WithFields(log.Fields{"entityID": entity.ActorID}).Info("fetching actor") + logger.New(ctx).WithFields(log.Fields{"entityID": entity.ActorID}).Info("fetching actor") user, err := r.Repository.GetUserAccountByID(ctx, entity.ActorID) if err != nil { return &NotificationActor{}, err @@ -935,7 +938,7 @@ func (r *projectResolver) Team(ctx context.Context, obj *db.Project) (*db.Team, 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") + logger.New(ctx).WithFields(log.Fields{"teamID": obj.TeamID, "projectID": obj.ProjectID}).WithError(err).Error("issue while getting team for project") return &team, err } return &team, nil @@ -949,14 +952,14 @@ func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Membe members := []Member{} projectMembers, err := r.Repository.GetProjectMembersForProjectID(ctx, obj.ProjectID) if err != nil { - log.WithError(err).Error("get project members for project id") + logger.New(ctx).WithError(err).Error("get project members for project id") return members, err } for _, projectMember := range projectMembers { user, err := r.Repository.GetUserAccountByID(ctx, projectMember.UserID) if err != nil { - log.WithError(err).Error("get user account by ID") + logger.New(ctx).WithError(err).Error("get user account by ID") return members, err } var url *string @@ -965,7 +968,7 @@ func (r *projectResolver) Members(ctx context.Context, obj *db.Project) ([]Membe } role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: user.UserID, ProjectID: obj.ProjectID}) if err != nil { - log.WithError(err).Error("get role for projet member by user ID") + logger.New(ctx).WithError(err).Error("get role for projet member by user ID") return members, err } profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor} @@ -1023,11 +1026,7 @@ func (r *queryResolver) FindUser(ctx context.Context, input FindUser) (*db.UserA } func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db.Project, error) { - userID, role, ok := GetUser(ctx) - log.WithFields(log.Fields{"userID": userID, "role": role}).Info("find project user") - if !ok { - return &db.Project{}, nil - } + logger.New(ctx).Info("finding project user") project, err := r.Repository.GetProjectByID(ctx, input.ProjectID) if err == sql.ErrNoRows { return &db.Project{}, &gqlerror.Error{ @@ -1048,10 +1047,10 @@ 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") + logger.New(ctx).Info("user id was not found from middleware") return []db.Project{}, nil } - log.WithFields(log.Fields{"userID": userID}).Info("fetching projects") + logger.New(ctx).Info("fetching projects") if input != nil { return r.Repository.GetAllProjectsForTeam(ctx, *input.TeamID) @@ -1067,37 +1066,36 @@ func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([] projects := make(map[string]db.Project) for _, team := range teams { - log.WithFields(log.Fields{"teamID": team.TeamID}).Info("found team") + logger.New(ctx).WithField("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") + logger.New(ctx).WithField("projectID", project.ProjectID).Info("adding team project") projects[project.ProjectID.String()] = project } } visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID) if err != nil { - log.WithField("userID", userID).Info("error getting visible projects for user") + logger.New(ctx).Info("error getting visible projects for user") return []db.Project{}, nil } for _, project := range visibleProjects { - log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("found visible project") + logger.New(ctx).WithField("projectID", project.ProjectID).Info("found visible project") if _, ok := projects[project.ProjectID.String()]; !ok { - log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project") + logger.New(ctx).WithField("projectID", project.ProjectID).Info("adding visible project") projects[project.ProjectID.String()] = project } } - log.WithFields(log.Fields{"projectLength": len(projects)}).Info("making projects") + logger.New(ctx).WithField("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") + logger.New(ctx).WithField("projectID", project.ProjectID).Info("adding project to final list") allProjects = append(allProjects, project) } - log.Info(allProjects) return allProjects, nil } @@ -1112,7 +1110,7 @@ func (r *queryResolver) FindTeam(ctx context.Context, input FindTeam) (*db.Team, func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) { userID, orgRole, ok := GetUser(ctx) if !ok { - log.Error("userID or orgRole does not exist!") + logger.New(ctx).Error("userID or org role does not exist") return []db.Team{}, errors.New("internal error") } if orgRole == "admin" { @@ -1123,7 +1121,7 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) { 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") + logger.New(ctx).WithError(err).Error("error while getting teams for user ID") return []db.Team{}, err } @@ -1133,19 +1131,19 @@ func (r *queryResolver) Teams(ctx context.Context) ([]db.Team, error) { visibleProjects, err := r.Repository.GetAllVisibleProjectsForUserID(ctx, userID) if err != nil { - log.WithField("userID", userID).WithError(err).Error("error while getting visible projects for user ID") + logger.New(ctx).WithError(err).Error("error while getting visible projects for user ID") return []db.Team{}, err } for _, project := range visibleProjects { - log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("found visible project") + logger.New(ctx).WithField("projectID", project.ProjectID).Info("found visible project") if _, ok := teams[project.ProjectID.String()]; !ok { - log.WithFields(log.Fields{"projectID": project.ProjectID.String()}).Info("adding visible project") + logger.New(ctx).WithField("projectID", project.ProjectID).Info("adding visible project") team, err := r.Repository.GetTeamByID(ctx, project.TeamID) if err != nil { if err == sql.ErrNoRows { continue } - log.WithField("teamID", project.TeamID).WithError(err).Error("error getting team by id") + logger.New(ctx).WithField("teamID", project.TeamID).WithError(err).Error("error getting team by id") return []db.Team{}, err } teams[project.TeamID.String()] = team @@ -1173,7 +1171,7 @@ func (r *queryResolver) Me(ctx context.Context) (*MePayload, 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") + logger.New(ctx).Warning("can not find user for me query") return &MePayload{}, nil } else if err != nil { return &MePayload{}, err @@ -1201,7 +1199,7 @@ func (r *queryResolver) Me(ctx context.Context) (*MePayload, error) { func (r *queryResolver) Notifications(ctx context.Context) ([]db.Notification, error) { userID, ok := GetUserID(ctx) - log.WithFields(log.Fields{"userID": userID}).Info("fetching notifications") + logger.New(ctx).Info("fetching notifications") if !ok { return []db.Notification{}, errors.New("user id is missing") } @@ -1215,7 +1213,7 @@ func (r *queryResolver) Notifications(ctx context.Context) ([]db.Notification, e } func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFilter) ([]MemberSearchResult, error) { - availableMembers, err := r.Repository.GetMemberData(ctx) + availableMembers, err := r.Repository.GetMemberData(ctx, *input.ProjectID) if err != nil { return []MemberSearchResult{}, err } @@ -1233,7 +1231,7 @@ func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFil memberList := map[uuid.UUID]bool{} for _, rank := range rankedList { if _, ok := memberList[masterList[rank.Target]]; !ok { - log.WithFields(log.Fields{"source": rank.Source, "target": rank.Target}).Info("searching") + logger.New(ctx).WithFields(log.Fields{"source": rank.Source, "target": rank.Target}).Info("searching") userID := masterList[rank.Target] user, err := r.Repository.GetUserAccountByID(ctx, userID) if err != nil { @@ -1313,7 +1311,7 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *db.Task) ([]Member, er if err == sql.ErrNoRows { role = db.Role{Code: "owner", Name: "Owner"} } else { - log.WithFields(log.Fields{"userID": user.UserID}).WithError(err).Error("get role for project member") + logger.New(ctx).WithError(err).Error("get role for project member") return taskMembers, err } } @@ -1407,14 +1405,14 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err teamMembers, err := r.Repository.GetTeamMembersForTeamID(ctx, obj.TeamID) if err != nil { - log.WithError(err).Error("get project members for project id") + logger.New(ctx).Error("get project members for project id") return members, err } for _, teamMember := range teamMembers { user, err := r.Repository.GetUserAccountByID(ctx, teamMember.UserID) if err != nil { - log.WithError(err).Error("get user account by ID") + logger.New(ctx).WithError(err).Error("get user account by ID") return members, err } var url *string @@ -1423,7 +1421,7 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err } role, err := r.Repository.GetRoleForTeamMember(ctx, db.GetRoleForTeamMemberParams{UserID: user.UserID, TeamID: obj.TeamID}) if err != nil { - log.WithError(err).Error("get role for projet member by user ID") + logger.New(ctx).WithError(err).Error("get role for projet member by user ID") return members, err } @@ -1451,8 +1449,7 @@ func (r *userAccountResolver) ID(ctx context.Context, obj *db.UserAccount) (uuid func (r *userAccountResolver) Role(ctx context.Context, obj *db.UserAccount) (*db.Role, error) { role, err := r.Repository.GetRoleForUserID(ctx, obj.UserID) if err != nil { - log.Info("beep!") - log.WithError(err).Error("get role for user id") + logger.New(ctx).WithError(err).Error("get role for user id") return &db.Role{}, err } return &db.Role{Code: role.Code, Name: role.Name}, nil diff --git a/internal/graph/schema/project_member.gql b/internal/graph/schema/project_member.gql index 030ae62..29df16b 100644 --- a/internal/graph/schema/project_member.gql +++ b/internal/graph/schema/project_member.gql @@ -1,22 +1,26 @@ extend type Mutation { - # TODO: rename to inviteProjectMember - inviteProjectMember(input: InviteProjectMember!): - InviteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + inviteProjectMembers(input: InviteProjectMembers!): + InviteProjectMembersPayload! @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 InviteProjectMember { - projectID: UUID! +input MemberInvite { userID: UUID email: String } -type InviteProjectMemberPayload { +input InviteProjectMembers { + projectID: UUID! + members: [MemberInvite!]! +} + +type InviteProjectMembersPayload { ok: Boolean! - member: Member! + projectID: UUID! + members: [Member!]! } input DeleteProjectMember { diff --git a/internal/logger/logger.go b/internal/logger/logger.go index adf231a..4d5ea0f 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -1,89 +1,21 @@ package logger import ( - "fmt" - "net/http" - "time" + "context" - "github.com/go-chi/chi/middleware" - "github.com/sirupsen/logrus" + "github.com/google/uuid" + "github.com/jordanknott/taskcafe/internal/utils" + log "github.com/sirupsen/logrus" ) -// NewStructuredLogger creates a new logger for chi router -func NewStructuredLogger(logger *logrus.Logger) func(next http.Handler) http.Handler { - return middleware.RequestLogger(&StructuredLogger{logger}) -} - -// StructuredLogger is a logger for chi router -type StructuredLogger struct { - Logger *logrus.Logger -} - -// NewLogEntry creates a new log entry for the given HTTP request -func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry { - entry := &StructuredLoggerEntry{Logger: logrus.NewEntry(l.Logger)} - logFields := logrus.Fields{} - - if reqID := middleware.GetReqID(r.Context()); reqID != "" { - logFields["req_id"] = reqID +// New returns a log entry with the reqID and userID fields populated if they exist +func New(ctx context.Context) *log.Entry { + entry := log.NewEntry(log.StandardLogger()) + if reqID, ok := ctx.Value(utils.ReqIDKey).(uuid.UUID); ok { + entry = entry.WithField("reqID", reqID) } - - scheme := "http" - if r.TLS != nil { - scheme = "https" + if userID, ok := ctx.Value(utils.UserIDKey).(uuid.UUID); ok { + entry = entry.WithField("userID", userID) } - logFields["http_scheme"] = scheme - logFields["http_proto"] = r.Proto - logFields["http_method"] = r.Method - - logFields["remote_addr"] = r.RemoteAddr - logFields["user_agent"] = r.UserAgent() - - logFields["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI) - - entry.Logger = entry.Logger.WithFields(logFields) - return entry } - -// StructuredLoggerEntry is a log entry will all relevant information about a specific http request -type StructuredLoggerEntry struct { - Logger logrus.FieldLogger -} - -// Write logs information about http request response body -func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration) { - l.Logger = l.Logger.WithFields(logrus.Fields{ - "resp_status": status, "resp_bytes_length": bytes, - "resp_elapsed_ms": float64(elapsed.Nanoseconds()) / 1000000.0, - }) - l.Logger.Debugln("request complete") -} - -// Panic logs if the request panics -func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) { - l.Logger = l.Logger.WithFields(logrus.Fields{ - "stack": string(stack), - "panic": fmt.Sprintf("%+v", v), - }) -} - -// GetLogEntry helper function for getting log entry for request -func GetLogEntry(r *http.Request) logrus.FieldLogger { - entry := middleware.GetLogEntry(r).(*StructuredLoggerEntry) - return entry.Logger -} - -// LogEntrySetField sets a key's value -func LogEntrySetField(r *http.Request, key string, value interface{}) { - if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok { - entry.Logger = entry.Logger.WithField(key, value) - } -} - -// LogEntrySetFields sets the log entry's fields -func LogEntrySetFields(r *http.Request, fields map[string]interface{}) { - if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok { - entry.Logger = entry.Logger.WithFields(fields) - } -} diff --git a/internal/logger/route_logger.go b/internal/logger/route_logger.go new file mode 100644 index 0000000..adf231a --- /dev/null +++ b/internal/logger/route_logger.go @@ -0,0 +1,89 @@ +package logger + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi/middleware" + "github.com/sirupsen/logrus" +) + +// NewStructuredLogger creates a new logger for chi router +func NewStructuredLogger(logger *logrus.Logger) func(next http.Handler) http.Handler { + return middleware.RequestLogger(&StructuredLogger{logger}) +} + +// StructuredLogger is a logger for chi router +type StructuredLogger struct { + Logger *logrus.Logger +} + +// NewLogEntry creates a new log entry for the given HTTP request +func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry { + entry := &StructuredLoggerEntry{Logger: logrus.NewEntry(l.Logger)} + logFields := logrus.Fields{} + + if reqID := middleware.GetReqID(r.Context()); reqID != "" { + logFields["req_id"] = reqID + } + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + logFields["http_scheme"] = scheme + logFields["http_proto"] = r.Proto + logFields["http_method"] = r.Method + + logFields["remote_addr"] = r.RemoteAddr + logFields["user_agent"] = r.UserAgent() + + logFields["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI) + + entry.Logger = entry.Logger.WithFields(logFields) + + return entry +} + +// StructuredLoggerEntry is a log entry will all relevant information about a specific http request +type StructuredLoggerEntry struct { + Logger logrus.FieldLogger +} + +// Write logs information about http request response body +func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration) { + l.Logger = l.Logger.WithFields(logrus.Fields{ + "resp_status": status, "resp_bytes_length": bytes, + "resp_elapsed_ms": float64(elapsed.Nanoseconds()) / 1000000.0, + }) + l.Logger.Debugln("request complete") +} + +// Panic logs if the request panics +func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) { + l.Logger = l.Logger.WithFields(logrus.Fields{ + "stack": string(stack), + "panic": fmt.Sprintf("%+v", v), + }) +} + +// GetLogEntry helper function for getting log entry for request +func GetLogEntry(r *http.Request) logrus.FieldLogger { + entry := middleware.GetLogEntry(r).(*StructuredLoggerEntry) + return entry.Logger +} + +// LogEntrySetField sets a key's value +func LogEntrySetField(r *http.Request, key string, value interface{}) { + if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok { + entry.Logger = entry.Logger.WithField(key, value) + } +} + +// LogEntrySetFields sets the log entry's fields +func LogEntrySetFields(r *http.Request, fields map[string]interface{}) { + if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok { + entry.Logger = entry.Logger.WithFields(fields) + } +} diff --git a/internal/route/middleware.go b/internal/route/middleware.go index 4a1336f..9442e6d 100644 --- a/internal/route/middleware.go +++ b/internal/route/middleware.go @@ -19,6 +19,7 @@ type AuthenticationMiddleware struct { // Middleware returns the middleware handler func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestID := uuid.New() bearerTokenRaw := r.Header.Get("Authorization") splitToken := strings.Split(bearerTokenRaw, "Bearer") if len(splitToken) != 2 { @@ -61,6 +62,7 @@ func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler { ctx := context.WithValue(r.Context(), utils.UserIDKey, userID) ctx = context.WithValue(ctx, utils.RestrictedModeKey, accessClaims.Restricted) ctx = context.WithValue(ctx, utils.OrgRoleKey, accessClaims.OrgRole) + ctx = context.WithValue(ctx, utils.ReqIDKey, requestID) next.ServeHTTP(w, r.WithContext(ctx)) }) diff --git a/internal/utils/context.go b/internal/utils/context.go index f221fc7..1bf2f8c 100644 --- a/internal/utils/context.go +++ b/internal/utils/context.go @@ -6,6 +6,8 @@ type ContextKey string const ( // UserIDKey is the key for the user id of the authenticated user UserIDKey ContextKey = "userID" + // ReqIDKey is the unique ID key for current request + ReqIDKey ContextKey = "reqID" //RestrictedModeKey is the key for whether the authenticated user only has access to install route RestrictedModeKey ContextKey = "restricted_mode" // OrgRoleKey is the key for the organization role code of the authenticated user