From 4f5277a1e8c8405dfa3bca67e7fbb051b9afad87 Mon Sep 17 00:00:00 2001 From: Peter Sanchez Date: Fri, 31 May 2024 06:52:02 -0600 Subject: [PATCH] Add org with admin write permissions to user org listing --- api/graph/generated.go | 100 +++++++++++++++++- api/graph/model/models_gen.go | 7 ++ api/graph/schema.graphqls | 9 +- api/graph/schema.resolvers.go | 35 ++++-- core/routes.go | 11 +- .../email_add_member_invitation_html.html | 4 +- .../email_add_member_invitation_text.txt | 4 +- templates/org_list.html | 9 ++ 8 files changed, 163 insertions(+), 16 deletions(-) diff --git a/api/graph/generated.go b/api/graph/generated.go index dd8ffde..240d977 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -374,7 +374,7 @@ type ComplexityRoot struct { GetOrgLinks func(childComplexity int, input *model.GetLinkInput) int GetOrgMembers func(childComplexity int, orgSlug string) int GetOrganization func(childComplexity int, id int) int - GetOrganizations func(childComplexity int) int + GetOrganizations func(childComplexity int, input *model.GetOrganizationsInput) int GetPaymentHistory func(childComplexity int, input *model.GetPaymentInput) int GetPopularLinks func(childComplexity int, input *model.PopularLinksInput) int GetQRDetail func(childComplexity int, hashID string, orgSlug *string) int @@ -475,7 +475,7 @@ type QueryResolver interface { Me(ctx context.Context) (*models.User, error) GetUsers(ctx context.Context, input *model.GetUserInput) (*model.UserCursor, error) GetUser(ctx context.Context, id int) (*models.User, error) - GetOrganizations(ctx context.Context) ([]*models.Organization, error) + GetOrganizations(ctx context.Context, input *model.GetOrganizationsInput) ([]*models.Organization, error) GetOrganization(ctx context.Context, id int) (*models.Organization, error) GetPaymentHistory(ctx context.Context, input *model.GetPaymentInput) (*model.PaymentCursor, error) GetPopularLinks(ctx context.Context, input *model.PopularLinksInput) ([]*models.BaseURL, error) @@ -2289,7 +2289,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in break } - return e.complexity.Query.GetOrganizations(childComplexity), true + args, err := ec.field_Query_getOrganizations_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.GetOrganizations(childComplexity, args["input"].(*model.GetOrganizationsInput)), true case "Query.getPaymentHistory": if e.complexity.Query.GetPaymentHistory == nil { @@ -2561,6 +2566,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputGetLinkShortInput, ec.unmarshalInputGetListingDetailInput, ec.unmarshalInputGetListingInput, + ec.unmarshalInputGetOrganizationsInput, ec.unmarshalInputGetPaymentInput, ec.unmarshalInputGetUserInput, ec.unmarshalInputLinkInput, @@ -3476,6 +3482,21 @@ func (ec *executionContext) field_Query_getOrganization_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Query_getOrganizations_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 *model.GetOrganizationsInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalOGetOrganizationsInput2ᚖlinksᚋapiᚋgraphᚋmodelᚐGetOrganizationsInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_getPaymentHistory_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -15988,7 +16009,7 @@ func (ec *executionContext) _Query_getOrganizations(ctx context.Context, field g 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.Query().GetOrganizations(rctx) + return ec.resolvers.Query().GetOrganizations(rctx, fc.Args["input"].(*model.GetOrganizationsInput)) } directive1 := func(ctx context.Context) (interface{}, error) { scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "ORGS") @@ -16068,6 +16089,17 @@ func (ec *executionContext) fieldContext_Query_getOrganizations(ctx context.Cont return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name) }, } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_getOrganizations_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return + } return fc, nil } @@ -22243,6 +22275,58 @@ func (ec *executionContext) unmarshalInputGetListingInput(ctx context.Context, o return it, nil } +func (ec *executionContext) unmarshalInputGetOrganizationsInput(ctx context.Context, obj interface{}) (model.GetOrganizationsInput, error) { + var it model.GetOrganizationsInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"limit", "after", "before", "search"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "limit": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit")) + it.Limit, err = ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + case "after": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + it.After, err = ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + if err != nil { + return it, err + } + case "before": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) + it.Before, err = ec.unmarshalOCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐCursor(ctx, v) + if err != nil { + return it, err + } + case "search": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("search")) + it.Search, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputGetPaymentInput(ctx context.Context, obj interface{}) (model.GetPaymentInput, error) { var it model.GetPaymentInput asMap := map[string]interface{}{} @@ -28281,6 +28365,14 @@ func (ec *executionContext) unmarshalOGetListingInput2ᚖlinksᚋapiᚋgraphᚋm return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalOGetOrganizationsInput2ᚖlinksᚋapiᚋgraphᚋmodelᚐGetOrganizationsInput(ctx context.Context, v interface{}) (*model.GetOrganizationsInput, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputGetOrganizationsInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalOGetPaymentInput2ᚖlinksᚋapiᚋgraphᚋmodelᚐGetPaymentInput(ctx context.Context, v interface{}) (*model.GetPaymentInput, error) { if v == nil { return nil, nil diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go index 2f1fde4..7fe6ec4 100644 --- a/api/graph/model/models_gen.go +++ b/api/graph/model/models_gen.go @@ -197,6 +197,13 @@ type GetListingInput struct { ExcludeTag *string `json:"excludeTag,omitempty"` } +type GetOrganizationsInput struct { + Limit *int `json:"limit,omitempty"` + After *Cursor `json:"after,omitempty"` + Before *Cursor `json:"before,omitempty"` + Search *string `json:"search,omitempty"` +} + type GetPaymentInput struct { OrgSlug *string `json:"orgSlug,omitempty"` Limit *int `json:"limit,omitempty"` diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index d7c2285..fad9ce0 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -424,6 +424,13 @@ input GetUserInput { search: String } +input GetOrganizationsInput { + limit: Int + after: Cursor + before: Cursor + search: String +} + input AdminBillingInput { orgSlug: String interval: Int @@ -679,7 +686,7 @@ type Query { getUser(id: Int!): User! @access(scope: PROFILE, kind: RW) "Returns an array of organizations" - getOrganizations: [Organization!]! @access(scope: ORGS, kind: RO) + getOrganizations(input: GetOrganizationsInput): [Organization!]! @access(scope: ORGS, kind: RO) "Returns a specific organization" getOrganization(id: Int!): Organization @access(scope: ORGS, kind: RO) diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 9841c9c..88a5868 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -41,6 +41,7 @@ import ( "netlandish.com/x/gobwebs" oauth2 "netlandish.com/x/gobwebs-oauth2" gaccounts "netlandish.com/x/gobwebs/accounts" + "netlandish.com/x/gobwebs/crypto" "netlandish.com/x/gobwebs/database" "netlandish.com/x/gobwebs/email" "netlandish.com/x/gobwebs/server" @@ -739,7 +740,8 @@ func (r *mutationResolver) AddMember(ctx context.Context, input *model.MemberInp return nil, valid.ErrAuthorization } currentUser := tokenUser.User.(*models.User) - lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), currentUser) + c := server.EchoForContext(ctx) + lang := links.GetLangFromRequest(c.Request(), currentUser) lt := localizer.GetLocalizer(lang) emailRegex, err := regexp.Compile(links.EmailPattern) @@ -813,7 +815,7 @@ func (r *mutationResolver) AddMember(ctx context.Context, input *model.MemberInp // New member to be added opts = &database.FilterOptions{ - Filter: sq.Eq{"u.email": input.Email}, + Filter: sq.Eq{"u.email": strings.ToLower(input.Email)}, Limit: 1, } @@ -829,7 +831,7 @@ func (r *mutationResolver) AddMember(ctx context.Context, input *model.MemberInp if len(users) == 0 { // We create a new user user = models.NewUser() - insecurePass := fmt.Sprintf("%s-%s", input.Email, time.Now()) + insecurePass := crypto.GenerateKey(20, true) user.Email = input.Email user.SetPassword(base64.StdEncoding.EncodeToString([]byte(insecurePass))) if err := user.Store(ctx); err != nil { @@ -864,18 +866,21 @@ func (r *mutationResolver) AddMember(ctx context.Context, input *model.MemberInp if err := conf.Store(ctx); err != nil { return nil, err } + + confURL := links.GetLinksDomainURL(c) q := url.Values{} q.Set("key", conf.Key) if isNewUser { q.Set("redirect", "true") } - confURL := fmt.Sprintf("/member/confirm?%s", q.Encode()) + confURL.RawQuery = q.Encode() + data := gobwebs.Map{ "title": lt.Translate("Link Taco: Invitation to join organization"), "currentUser": currentUser.Name, "user": user.Name, "org": org.Name, - "confURL": confURL, + "confURL": confURL.String(), } helper := email.NewHelper(srv.Email, tmap, tmpl) err = helper.Send( @@ -4251,16 +4256,32 @@ func (r *queryResolver) GetUser(ctx context.Context, id int) (*models.User, erro } // GetOrganizations is the resolver for the getOrganizations field. -func (r *queryResolver) GetOrganizations(ctx context.Context) ([]*models.Organization, error) { +func (r *queryResolver) GetOrganizations(ctx context.Context, input *model.GetOrganizationsInput) ([]*models.Organization, error) { tokenUser := oauth2.ForContext(ctx) if tokenUser == nil { return nil, valid.ErrAuthorization } user := tokenUser.User.(*models.User) opts := &database.FilterOptions{ - Filter: sq.Eq{"o.owner_id": user.ID}, + Filter: sq.Or{ + sq.Eq{"o.owner_id": user.ID}, + sq.And{ + sq.Eq{"ou.user_id": user.ID}, + sq.GtOrEq{"ou.permission": models.OrgUserPermissionAdminWrite}, + sq.Eq{"ou.is_active": true}, + }, + }, OrderBy: "o.created_on ASC", } + if input.Search != nil && *input.Search != "" { + s := links.ParseSearch(*input.Search) + opts.Filter = sq.And{ + opts.Filter, + sq.Expr(`to_tsvector('simple', o.name || ' ' || o.slug ) + @@ to_tsquery('simple', ?)`, s), + } + } + orgs, err := models.GetOrganizations(ctx, opts) if err != nil { return nil, err diff --git a/core/routes.go b/core/routes.go index a788b7e..19f3636 100644 --- a/core/routes.go +++ b/core/routes.go @@ -638,13 +638,14 @@ func (s *Service) DomainDelete(c echo.Context) error { func (s *Service) OrgList(c echo.Context) error { gctx := c.(*server.Context) + query := c.QueryParam("q") type GraphQLResponse struct { Orgs []models.Organization `json:"getOrganizations"` } var result GraphQLResponse op := gqlclient.NewOperation( - `query GetOrganizations() { - getOrganizations { + `query GetOrganizations($search: String) { + getOrganizations(input: {search: $search}) { id name slug @@ -656,6 +657,9 @@ func (s *Service) OrgList(c echo.Context) error { isActive } }`) + if query != "" { + op.Var("search", query) + } err := links.Execute(c.Request().Context(), op, &result) if err != nil { return err @@ -717,6 +721,9 @@ func (s *Service) OrgList(c echo.Context) error { "dOrgSlug": dOrgSlug, "orgs": result.Orgs, } + if query != "" { + gmap["search"] = query + } return links.Render(c, http.StatusOK, "org_list.html", gmap) } diff --git a/templates/email_add_member_invitation_html.html b/templates/email_add_member_invitation_html.html index 2101deb..c394ae0 100644 --- a/templates/email_add_member_invitation_html.html +++ b/templates/email_add_member_invitation_html.html @@ -1,3 +1,5 @@ You have been invited by {{.currentUser}} to join {{.org}} in links. -Please click the link below: {{buildURL .confURL}} +Please click the link below: + +{{.confURL}} diff --git a/templates/email_add_member_invitation_text.txt b/templates/email_add_member_invitation_text.txt index 2101deb..c394ae0 100644 --- a/templates/email_add_member_invitation_text.txt +++ b/templates/email_add_member_invitation_text.txt @@ -1,3 +1,5 @@ You have been invited by {{.currentUser}} to join {{.org}} in links. -Please click the link below: {{buildURL .confURL}} +Please click the link below: + +{{.confURL}} diff --git a/templates/org_list.html b/templates/org_list.html index 3043125..df27a2e 100644 --- a/templates/org_list.html +++ b/templates/org_list.html @@ -6,6 +6,15 @@
+ + -- 2.45.2