M api/graph/generated.go => api/graph/generated.go +232 -4
@@ 227,6 227,7 @@ type ComplexityRoot struct {
DeleteLinkShort func(childComplexity int, id int) int
DeleteListing func(childComplexity int, id int) int
DeleteListingLink func(childComplexity int, id int) int
+ DeleteMember func(childComplexity int, orgSlug string, email string) int
DeleteQRCode func(childComplexity int, id int) int
Follow func(childComplexity int, orgSlug string) int
Register func(childComplexity int, input *model.RegisterInput) int
@@ 374,7 375,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
@@ 438,6 439,7 @@ type MutationResolver interface {
DeleteLink(ctx context.Context, id int) (*model.DeletePayload, error)
AddNote(ctx context.Context, input *model.NoteInput) (*models.OrgLink, error)
AddMember(ctx context.Context, input *model.MemberInput) (*model.AddMemberPayload, error)
+ DeleteMember(ctx context.Context, orgSlug string, email string) (*model.AddMemberPayload, error)
ConfirmMember(ctx context.Context, key string) (*model.AddMemberPayload, error)
Register(ctx context.Context, input *model.RegisterInput) (*models.User, error)
CompleteRegister(ctx context.Context, input *model.CompleteRegisterInput) (*models.User, error)
@@ 475,7 477,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)
@@ 1394,6 1396,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.DeleteListingLink(childComplexity, args["id"].(int)), true
+ case "Mutation.deleteMember":
+ if e.complexity.Mutation.DeleteMember == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_deleteMember_args(context.TODO(), rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.DeleteMember(childComplexity, args["orgSlug"].(string), args["email"].(string)), true
+
case "Mutation.deleteQRCode":
if e.complexity.Mutation.DeleteQRCode == nil {
break
@@ 2289,7 2303,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 2580,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputGetLinkShortInput,
ec.unmarshalInputGetListingDetailInput,
ec.unmarshalInputGetListingInput,
+ ec.unmarshalInputGetOrganizationsInput,
ec.unmarshalInputGetPaymentInput,
ec.unmarshalInputGetUserInput,
ec.unmarshalInputLinkInput,
@@ 2951,6 2971,30 @@ func (ec *executionContext) field_Mutation_deleteListing_args(ctx context.Contex
return args, nil
}
+func (ec *executionContext) field_Mutation_deleteMember_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+ var err error
+ args := map[string]interface{}{}
+ var arg0 string
+ if tmp, ok := rawArgs["orgSlug"]; ok {
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("orgSlug"))
+ arg0, err = ec.unmarshalNString2string(ctx, tmp)
+ if err != nil {
+ return nil, err
+ }
+ }
+ args["orgSlug"] = arg0
+ var arg1 string
+ if tmp, ok := rawArgs["email"]; ok {
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
+ arg1, err = ec.unmarshalNString2string(ctx, tmp)
+ if err != nil {
+ return nil, err
+ }
+ }
+ args["email"] = arg1
+ return args, nil
+}
+
func (ec *executionContext) field_Mutation_deleteQRCode_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ 3476,6 3520,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{}{}
@@ 9420,6 9479,95 @@ func (ec *executionContext) fieldContext_Mutation_addMember(ctx context.Context,
return fc, nil
}
+func (ec *executionContext) _Mutation_deleteMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Mutation_deleteMember(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ 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().DeleteMember(rctx, fc.Args["orgSlug"].(string), fc.Args["email"].(string))
+ }
+ directive1 := func(ctx context.Context) (interface{}, error) {
+ scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "ORGS")
+ if err != nil {
+ return nil, err
+ }
+ kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RW")
+ if err != nil {
+ return nil, err
+ }
+ if ec.directives.Access == nil {
+ return nil, errors.New("directive access is not implemented")
+ }
+ return ec.directives.Access(ctx, nil, directive0, scope, kind)
+ }
+
+ tmp, err := directive1(rctx)
+ if err != nil {
+ return nil, graphql.ErrorOnPath(ctx, err)
+ }
+ if tmp == nil {
+ return nil, nil
+ }
+ if data, ok := tmp.(*model.AddMemberPayload); ok {
+ return data, nil
+ }
+ return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/api/graph/model.AddMemberPayload`, tmp)
+ })
+ 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.(*model.AddMemberPayload)
+ fc.Result = res
+ return ec.marshalNAddMemberPayload2ᚖlinksᚋapiᚋgraphᚋmodelᚐAddMemberPayload(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_deleteMember(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "success":
+ return ec.fieldContext_AddMemberPayload_success(ctx, field)
+ case "message":
+ return ec.fieldContext_AddMemberPayload_message(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type AddMemberPayload", 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_Mutation_deleteMember_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Mutation_confirmMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_confirmMember(ctx, field)
if err != nil {
@@ 15988,7 16136,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 16216,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 22402,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{}{}
@@ 24576,6 24787,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
+ case "deleteMember":
+
+ out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+ return ec._Mutation_deleteMember(ctx, field)
+ })
+
+ if out.Values[i] == graphql.Null {
+ invalids++
+ }
case "confirmMember":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
@@ 28281,6 28501,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 +9 -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)
@@ 756,6 763,7 @@ type Mutation {
"Add organization members"
addMember(input: MemberInput): AddMemberPayload! @access(scope: ORGS, kind: RW)
+ deleteMember(orgSlug: String!, email: String!): AddMemberPayload! @access(scope: ORGS, kind: RW)
confirmMember(key: String!): AddMemberPayload! @access(scope: PROFILE, kind: RW)
"Register an account"
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +122 -6
@@ 11,6 11,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ "html/template"
"image"
"image/jpeg"
"image/png"
@@ 41,6 42,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 741,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 816,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 832,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 867,22 @@ 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.Path = "/member/confirm"
+ 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": template.URL(confURL.String()),
}
helper := email.NewHelper(srv.Email, tmap, tmpl)
err = helper.Send(
@@ 891,6 898,94 @@ func (r *mutationResolver) AddMember(ctx context.Context, input *model.MemberInp
return addMemberPayload, nil
}
+// DeleteMember is the resolver for the deleteMember field.
+func (r *mutationResolver) DeleteMember(ctx context.Context, orgSlug string, email string) (*model.AddMemberPayload, error) {
+ tokenUser := oauth2.ForContext(ctx)
+ if tokenUser == nil {
+ return nil, valid.ErrAuthorization
+ }
+ user := tokenUser.User.(*models.User)
+ c := server.EchoForContext(ctx)
+ lang := links.GetLangFromRequest(c.Request(), user)
+ lt := localizer.GetLocalizer(lang)
+
+ emailRegex, err := regexp.Compile(links.EmailPattern)
+ if err != nil {
+ return nil, fmt.Errorf(lt.Translate("Error compiling url regex: %s", err))
+ }
+
+ validator := valid.New(ctx)
+ validator.Expect(orgSlug != "", lt.Translate("Org slug is required")).
+ WithField("org").
+ WithCode(valid.ErrValidationCode)
+ validator.Expect(email != "", lt.Translate("User email is required")).
+ WithField("email").
+ WithCode(valid.ErrValidationCode)
+ validator.Expect(len(email) < 255, lt.Translate("Email may not exceed 255 characters")).
+ WithField("email").
+ WithCode(valid.ErrValidationCode)
+ validator.Expect(emailRegex.MatchString(email), lt.Translate("Invalid email format")).
+ WithField("email").
+ WithCode(valid.ErrValidationCode)
+
+ if !validator.Ok() {
+ return nil, nil
+ }
+
+ org, err := user.GetOrgsSlug(ctx, models.OrgUserPermissionAdminWrite, orgSlug)
+ if err != nil {
+ return nil, err
+ }
+ if org == nil {
+ validator.Error(lt.Translate("Organization Not Found")).
+ WithCode(valid.ErrNotFoundCode)
+ return nil, nil
+ }
+
+ opts := &database.FilterOptions{
+ Filter: sq.Eq{"u.email": strings.ToLower(email)},
+ Limit: 1,
+ }
+
+ users, err := models.GetUsers(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ if len(users) == 0 {
+ validator.Error(lt.Translate("User not found for given email")).
+ WithCode(valid.ErrNotFoundCode)
+ return nil, nil
+ }
+ duser := users[0]
+
+ opts = &database.FilterOptions{
+ Filter: sq.And{
+ sq.Eq{"org_id": org.ID},
+ sq.Eq{"user_id": duser.ID},
+ },
+ }
+ ousers, err := models.GetOrgUsers(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ addMemberPayload := &model.AddMemberPayload{}
+ if len(ousers) == 0 {
+ addMemberPayload.Success = false
+ addMemberPayload.Message = lt.Translate("The user for given email is not a member of given organization")
+ } else {
+ for _, ou := range ousers {
+ err = ou.Delete(ctx)
+ if err != nil {
+ return nil, err
+ }
+ }
+ addMemberPayload.Success = true
+ addMemberPayload.Message = lt.Translate("The member was removed successfully")
+ }
+ return addMemberPayload, nil
+}
+
// ConfirmMember is the resolver for the confirmMember field.
func (r *mutationResolver) ConfirmMember(ctx context.Context, key string) (*model.AddMemberPayload, error) {
lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), nil)
@@ 4251,7 4346,7 @@ 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
@@ 4261,6 4356,27 @@ func (r *queryResolver) GetOrganizations(ctx context.Context) ([]*models.Organiz
Filter: sq.Eq{"o.owner_id": user.ID},
OrderBy: "o.created_on ASC",
}
+ // XXX Uncomment when we decide on a path for org members with admin write permissions
+ //opts := &database.FilterOptions{
+ // 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 +123 -20
@@ 88,7 88,9 @@ func (s *Service) RegisterRoutes() {
s.eg.POST("/:slug/edit", s.OrgUpdate).Name = s.RouteName("org_edit_post")
s.eg.GET("/:slug/members", s.OrgMembersList).Name = s.RouteName("org_member_list")
s.eg.GET("/:slug/members/add", s.OrgMembersAdd).Name = s.RouteName("org_member_add")
- s.eg.POST("/:slug/members/add", s.OrgMembersAdd).Name = s.RouteName("org_member_add")
+ s.eg.POST("/:slug/members/add", s.OrgMembersAdd).Name = s.RouteName("org_member_add_post")
+ s.eg.GET("/:slug/members/delete/:id", s.OrgMembersDelete).Name = s.RouteName("org_member_delete")
+ s.eg.POST("/:slug/members/delete/:id", s.OrgMembersDelete).Name = s.RouteName("org_member_delete_post")
s.eg.GET("/:slug/export", s.ExportData).Name = s.RouteName("export_data")
s.eg.POST("/:slug/export", s.ExportData).Name = s.RouteName("export_data_post")
s.eg.GET("/:slug/import", s.ImportData).Name = s.RouteName("import_data")
@@ 638,13 640,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 659,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 723,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)
}
@@ 1098,6 1107,107 @@ func (s *Service) OrgMembersAdd(c echo.Context) error {
return links.Render(c, http.StatusOK, "member_add.html", gmap)
}
+// OrgMembersDelete ...
+func (s *Service) OrgMembersDelete(c echo.Context) error {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ return echo.NotFoundHandler(c)
+ }
+ slug := c.Param("slug")
+ if slug == "" {
+ return echo.NotFoundHandler(c)
+ }
+
+ gctx := c.(*server.Context)
+ user := gctx.User.(*models.User)
+
+ req := c.Request()
+ org, err := user.GetOrgsSlug(req.Context(), models.OrgUserPermissionAdminWrite, slug)
+ if err != nil {
+ return err
+ }
+ if org == nil {
+ return echo.NotFoundHandler(c)
+ }
+
+ opts := &database.FilterOptions{
+ Filter: sq.And{
+ sq.Eq{"org_id": org.ID},
+ sq.Eq{"user_id": id},
+ sq.Eq{"is_active": true},
+ },
+ Limit: 1,
+ }
+ ousers, err := models.GetOrgUsers(req.Context(), opts)
+ if err != nil {
+ return err
+ }
+ if len(ousers) == 0 {
+ return echo.NotFoundHandler(c)
+ }
+ orgUser := ousers[0]
+
+ ouser, err := models.GetUser(req.Context(), orgUser.UserID, false)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return echo.NotFoundHandler(c)
+ }
+ return err
+ }
+
+ lt := localizer.GetSessionLocalizer(c)
+ pd := localizer.NewPageData(lt.Translate("Delete Org Member"))
+
+ if req.Method == http.MethodPost {
+ type GraphQLResponse struct {
+ DeleteMember struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ } `json:"deleteMember"`
+ }
+
+ var result GraphQLResponse
+ op := gqlclient.NewOperation(
+ `mutation DeleteMember($orgSlug: String!, $email: String!) {
+ deleteMember(orgSlug: $orgSlug, email: $email) {
+ success
+ message
+ }
+ }`)
+ op.Var("orgSlug", slug)
+ op.Var("email", strings.ToLower(ouser.Email))
+ err = links.Execute(c.Request().Context(), op, &result)
+ if err != nil {
+ return err
+ }
+ if !result.DeleteMember.Success {
+ messages.Error(
+ c,
+ lt.Translate(
+ "Something went wrong. This member could not be deleted: %s",
+ result.DeleteMember.Message,
+ ),
+ )
+ } else {
+ messages.Success(c, lt.Translate("Member successfully deleted"))
+ }
+ return c.Redirect(http.StatusMovedPermanently,
+ c.Echo().Reverse(s.RouteName("org_member_list"), slug))
+ }
+
+ pd.Data["message"] = lt.Translate(
+ "Delete member %s (%s) from Organization %s?", ouser.Name, ouser.Email, slug)
+ pd.Data["yes"] = lt.Translate("Yes")
+ pd.Data["cancel"] = lt.Translate("Cancel")
+
+ gmap := gobwebs.Map{
+ "pd": pd,
+ "url": c.Echo().Reverse(s.RouteName("org_member_delete"), slug, id),
+ "back": c.Echo().Reverse(s.RouteName("org_member_list"), slug),
+ }
+ return links.Render(c, http.StatusOK, "element_delete.html", gmap)
+}
+
// OrgMemberConfirmation ...
func (s *Service) OrgMemberConfirmation(c echo.Context) error {
key := c.QueryParam("key")
@@ 1197,6 1307,7 @@ func (s *Service) OrgMembersList(c echo.Context) error {
pd.Data["no_member"] = lt.Translate("No members")
pd.Data["add"] = lt.Translate("Add")
pd.Data["back"] = lt.Translate("Back")
+ pd.Data["delete"] = lt.Translate("Delete")
pd.Data["restricted"] = lt.Translate("Please upgrade to a Business organization to add members")
pd.Data["continue_to_upgrade"] = lt.Translate("Continue to Upgrade")
gmap := gobwebs.Map{
@@ 2211,7 2322,7 @@ func (s *Service) OrgLinkDelete(c echo.Context) error {
return err
}
if !result.DeleteLink.Success {
- messages.Error(c, lt.Translate("Something went wrong. This element could not be deleted."))
+ messages.Error(c, lt.Translate("Something went wrong. This bookmark could not be deleted."))
redirect := c.Request().Header.Get("Referer")
if redirect == "" {
redirect = c.Echo().Reverse(s.RouteName("home_link_list"))
@@ 2219,7 2330,7 @@ func (s *Service) OrgLinkDelete(c echo.Context) error {
return c.Redirect(http.StatusMovedPermanently, redirect)
}
- messages.Success(c, lt.Translate("Element successfully deleted"))
+ messages.Success(c, lt.Translate("Bookmark successfully deleted"))
return c.Redirect(http.StatusMovedPermanently,
c.Echo().Reverse(s.RouteName("home_link_list")))
}
@@ 2241,31 2352,23 @@ func (s *Service) OrgLinkDelete(c echo.Context) error {
}
link := orgLinks[0]
- // If the user is not the link creator, check
- // if it's the org owner
+
+ // If the user is not the link creator, check that the user
+ //has admin write permissions.
user := gctx.User.(*models.User)
if link.UserID != int(user.ID) {
- opts = &database.FilterOptions{
- Filter: sq.And{
- sq.Expr("o.id = ?", link.OrgID),
- sq.Expr("o.owner_id = ?", user.ID),
- sq.Expr("o.is_active = true"),
- },
- Limit: 1,
- }
-
- orgs, err := models.GetOrganizations(c.Request().Context(), opts)
+ org, err := user.GetOrgsID(
+ c.Request().Context(), models.OrgUserPermissionWrite, link.OrgID)
if err != nil {
return err
}
-
- if len(orgs) == 0 {
+ if org == nil {
return echo.NotFoundHandler(c)
}
}
pd.Data["yes"] = lt.Translate("Yes")
pd.Data["cancel"] = lt.Translate("Cancel")
- pd.Data["message"] = lt.Translate("Do you really whant to delete this element")
+ pd.Data["message"] = lt.Translate("Do you really whant to delete this bookmark?")
gmap := gobwebs.Map{
"pd": pd,
"url": c.Echo().Reverse(s.RouteName("link_delete"), link.ID),
M templates/email_add_member_invitation_html.html => templates/email_add_member_invitation_html.html +1 -3
@@ 1,11 1,9 @@
{{template "email_base" .}}
-
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
You have been invited by {{.currentUser}} to join {{.org}} in links.
</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
Please click the link below:
- <a href="{{buildURL .confURL}}" class="btn btn-primary">{{buildURL .confURL}}</a>
+ <a href="{{.confURL}}" class="btn btn-primary">{{.confURL}}</a>
</p>
-
{{template "email_base_footer" .}}
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/member_list.html => templates/member_list.html +1 -1
@@ 29,7 29,7 @@
<tr>
<td>{{.Name}}</td>
<td>{{.Email}}</td>
- <td></td>
+ <td><a class="button primary is-small" href="{{ reverse "core:org_member_delete" $.slug .ID }}">{{ $.pd.Data.delete }}</a></td>
</tr>
{{end}}
{{else}}
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>