~netlandish/links

4f5277a1e8c8405dfa3bca67e7fbb051b9afad87 — Peter Sanchez 4 months ago 6bc6d00
Add org with admin write permissions to user org listing
M api/graph/generated.go => api/graph/generated.go +96 -4
@@ 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

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +7 -0
@@ 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"`

M api/graph/schema.graphqls => api/graph/schema.graphqls +8 -1
@@ 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)

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +28 -7
@@ 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

M core/routes.go => core/routes.go +9 -2
@@ 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)
}


M templates/email_add_member_invitation_html.html => templates/email_add_member_invitation_html.html +3 -1
@@ 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}}

M templates/email_add_member_invitation_text.txt => templates/email_add_member_invitation_text.txt +3 -1
@@ 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}}

M templates/org_list.html => templates/org_list.html +9 -0
@@ 6,6 6,15 @@
</section>

<section class="card shadow-card">
    <form class="app-header__search app-header__search--inline" method="GET" action="{{reverse "core:org_list"}}">
      <input type="search" name="q" value="{{.search}}"/>
      <button type="submit" class="button dark icon-only">
          <svg class="search-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:20px">
              <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
          </svg>
      </button>
    </form>

    <table class="striped">
        <thead>
            <tr>