taskcafe/internal/graph/user.resolvers.go

303 lines
10 KiB
Go

package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"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"
"golang.org/x/crypto/bcrypt"
)
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*db.UserAccount, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &db.UserAccount{}, nil
}
role, err := r.Repository.GetRoleForUserID(ctx, userID)
if err != nil {
log.WithError(err).Error("while creating user account")
return &db.UserAccount{}, nil
}
if ConvertToRoleCode(role.Code) != RoleCodeAdmin {
return &db.UserAccount{}, &gqlerror.Error{
Message: "Must be an organization admin",
Extensions: map[string]interface{}{
"code": "0-400",
},
}
}
createdAt := time.Now().UTC()
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(input.Password), 14)
if err != nil {
return &db.UserAccount{}, err
}
userExists, err := r.Repository.DoesUserExist(ctx, db.DoesUserExistParams{Username: input.Username, Email: input.Email})
if err != nil {
return &db.UserAccount{}, err
}
if userExists {
return &db.UserAccount{}, &gqlerror.Error{
Message: "User with that username or email already exists",
Extensions: map[string]interface{}{
"code": "0-300",
},
}
}
userAccount, err := r.Repository.CreateUserAccount(ctx, db.CreateUserAccountParams{
FullName: input.FullName,
RoleCode: input.RoleCode,
Initials: input.Initials,
Email: input.Email,
Username: input.Username,
CreatedAt: createdAt,
Active: true,
PasswordHash: string(hashedPwd),
})
return &userAccount, err
}
func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUserAccount) (*DeleteUserAccountPayload, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &DeleteUserAccountPayload{Ok: false}, nil
}
role, err := r.Repository.GetRoleForUserID(ctx, userID)
if err != nil {
log.WithError(err).Error("while deleting user account")
return &DeleteUserAccountPayload{}, nil
}
if ConvertToRoleCode(role.Code) != RoleCodeAdmin {
return &DeleteUserAccountPayload{}, &gqlerror.Error{
Message: "Must be an organization admin",
Extensions: map[string]interface{}{
"code": "0-400",
},
}
}
user, err := r.Repository.GetUserAccountByID(ctx, input.UserID)
if err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
}
err = r.Repository.DeleteUserAccountByID(ctx, input.UserID)
if err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
}
return &DeleteUserAccountPayload{UserAccount: &user, Ok: true}, nil
}
func (r *mutationResolver) DeleteInvitedUserAccount(ctx context.Context, input DeleteInvitedUserAccount) (*DeleteInvitedUserAccountPayload, error) {
user, err := r.Repository.DeleteInvitedUserAccount(ctx, input.InvitedUserID)
if err != nil {
return &DeleteInvitedUserAccountPayload{}, err
}
err = r.Repository.DeleteConfirmTokenForEmail(ctx, user.Email)
if err != nil {
logger.New(ctx).WithError(err).Error("issue deleting confirm token")
return &DeleteInvitedUserAccountPayload{}, err
}
return &DeleteInvitedUserAccountPayload{
InvitedUser: &InvitedUserAccount{
Email: user.Email,
ID: user.UserAccountInvitedID,
InvitedOn: user.InvitedOn,
},
}, err
}
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
err := r.Repository.DeleteAuthTokenByUserID(ctx, input.UserID)
return true, err
}
func (r *mutationResolver) ClearProfileAvatar(ctx context.Context) (*db.UserAccount, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &db.UserAccount{}, fmt.Errorf("internal server error")
}
user, err := r.Repository.UpdateUserAccountProfileAvatarURL(ctx, db.UpdateUserAccountProfileAvatarURLParams{UserID: userID, ProfileAvatarUrl: sql.NullString{Valid: false, String: ""}})
if err != nil {
return &db.UserAccount{}, err
}
return &user, nil
}
func (r *mutationResolver) UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error) {
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(input.Password), 14)
if err != nil {
return &UpdateUserPasswordPayload{}, err
}
user, err := r.Repository.SetUserPassword(ctx, db.SetUserPasswordParams{UserID: input.UserID, PasswordHash: string(hashedPwd)})
if err != nil {
return &UpdateUserPasswordPayload{}, err
}
return &UpdateUserPasswordPayload{Ok: true, User: &user}, err
}
func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &UpdateUserRolePayload{}, nil
}
role, err := r.Repository.GetRoleForUserID(ctx, userID)
if err != nil {
log.WithError(err).Error("while updating user role")
return &UpdateUserRolePayload{}, nil
}
if ConvertToRoleCode(role.Code) != RoleCodeAdmin {
return &UpdateUserRolePayload{}, &gqlerror.Error{
Message: "Must be an organization admin",
Extensions: map[string]interface{}{
"code": "0-400",
},
}
}
user, err := r.Repository.UpdateUserRole(ctx, db.UpdateUserRoleParams{RoleCode: input.RoleCode.String(), UserID: input.UserID})
if err != nil {
return &UpdateUserRolePayload{}, err
}
return &UpdateUserRolePayload{User: &user}, nil
}
func (r *mutationResolver) UpdateUserInfo(ctx context.Context, input UpdateUserInfo) (*UpdateUserInfoPayload, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &UpdateUserInfoPayload{}, errors.New("invalid user ID")
}
user, err := r.Repository.UpdateUserAccountInfo(ctx, db.UpdateUserAccountInfoParams{
Bio: input.Bio, FullName: input.Name, Initials: input.Initials, Email: input.Email, UserID: userID,
})
return &UpdateUserInfoPayload{User: &user}, err
}
func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFilter) ([]MemberSearchResult, error) {
availableMembers, err := r.Repository.GetMemberData(ctx, *input.ProjectID)
if err != nil {
logger.New(ctx).WithField("projectID", input.ProjectID).WithError(err).Error("error while getting member data")
return []MemberSearchResult{}, err
}
invitedMembers, err := r.Repository.GetInvitedMembersForProjectID(ctx, *input.ProjectID)
if err != nil {
logger.New(ctx).WithField("projectID", input.ProjectID).WithError(err).Error("error while getting member data")
return []MemberSearchResult{}, err
}
sortList := []string{}
masterList := map[string]MasterEntry{}
for _, member := range availableMembers {
sortList = append(sortList, member.Username)
sortList = append(sortList, member.Email)
masterList[member.Username] = MasterEntry{ID: member.UserID, MemberType: MemberTypeJoined}
masterList[member.Email] = MasterEntry{ID: member.UserID, MemberType: MemberTypeJoined}
}
for _, member := range invitedMembers {
sortList = append(sortList, member.Email)
logger.New(ctx).WithField("Email", member.Email).Info("adding member")
masterList[member.Email] = MasterEntry{ID: member.UserAccountInvitedID, MemberType: MemberTypeInvited}
}
logger.New(ctx).WithField("searchFilter", input.SearchFilter).Info(sortList)
rankedList := fuzzy.RankFind(input.SearchFilter, sortList)
logger.New(ctx).Info(rankedList)
results := []MemberSearchResult{}
memberList := map[uuid.UUID]bool{}
for _, rank := range rankedList {
entry, _ := masterList[rank.Target]
_, ok := memberList[entry.ID]
logger.New(ctx).WithField("ok", ok).WithField("target", rank.Target).Info("checking rank")
if !ok {
if entry.MemberType == MemberTypeJoined {
logger.New(ctx).WithFields(log.Fields{"source": rank.Source, "target": rank.Target}).Info("searching")
entry := masterList[rank.Target]
user, err := r.Repository.GetUserAccountByID(ctx, entry.ID)
if err != nil {
if err == sql.ErrNoRows {
continue
}
return []MemberSearchResult{}, err
}
results = append(results, MemberSearchResult{ID: user.UserID.String(), User: &user, Status: ShareStatusJoined, Similarity: rank.Distance})
} else {
logger.New(ctx).WithField("id", rank.Target).Info("adding target")
results = append(results, MemberSearchResult{ID: rank.Target, Status: ShareStatusInvited, Similarity: rank.Distance})
}
memberList[entry.ID] = true
}
}
return results, nil
}
func (r *userAccountResolver) ID(ctx context.Context, obj *db.UserAccount) (uuid.UUID, error) {
return obj.UserID, nil
}
func (r *userAccountResolver) Role(ctx context.Context, obj *db.UserAccount) (*db.Role, error) {
role, err := r.Repository.GetRoleForUserID(ctx, obj.UserID)
if err != nil {
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
}
func (r *userAccountResolver) ProfileIcon(ctx context.Context, obj *db.UserAccount) (*ProfileIcon, error) {
var url *string
if obj.ProfileAvatarUrl.Valid {
url = &obj.ProfileAvatarUrl.String
}
profileIcon := &ProfileIcon{url, &obj.Initials, &obj.ProfileBgColor}
return profileIcon, nil
}
func (r *userAccountResolver) Owned(ctx context.Context, obj *db.UserAccount) (*OwnedList, error) {
return &OwnedList{}, nil // TODO(jordanknott)
}
func (r *userAccountResolver) Member(ctx context.Context, obj *db.UserAccount) (*MemberList, error) {
projectMemberIDs, err := r.Repository.GetMemberProjectIDsForUserID(ctx, obj.UserID)
if err != sql.ErrNoRows && err != nil {
return &MemberList{}, err
}
var projects []db.Project
for _, projectID := range projectMemberIDs {
project, err := r.Repository.GetProjectByID(ctx, projectID)
if err != nil {
return &MemberList{}, err
}
projects = append(projects, project)
}
teamMemberIDs, err := r.Repository.GetMemberTeamIDsForUserID(ctx, obj.UserID)
if err != sql.ErrNoRows && err != nil {
return &MemberList{}, err
}
var teams []db.Team
for _, teamID := range teamMemberIDs {
team, err := r.Repository.GetTeamByID(ctx, teamID)
if err != nil {
return &MemberList{}, err
}
teams = append(teams, team)
}
return &MemberList{Teams: teams, Projects: projects}, err
}
// UserAccount returns UserAccountResolver implementation.
func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolver{r} }
type userAccountResolver struct{ *Resolver }