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>