~netlandish/links

83119f6480721e14652718ef9cb30b757a125c3d — Peter Sanchez 7 days ago 9af944c
Moving getOrganizations call to use org slug versus db ID
M admin/routes.go => admin/routes.go +18 -16
@@ 8,6 8,7 @@ import (
	"links/models"
	"net/http"
	"strconv"
	"strings"
	"time"

	"git.sr.ht/~emersion/gqlclient"


@@ 40,9 41,9 @@ func (s *Service) RegisterRoutes() {
	s.Group.GET("/user/:id/lock", s.UserToggleLock).Name = s.RouteName("user_toggle_lock")
	s.Group.POST("/user/:id/lock", s.UserToggleLock).Name = s.RouteName("user_toggle_lock")
	s.Group.GET("/organization", s.OrgList).Name = s.RouteName("org_list")
	s.Group.GET("/organization/:id", s.OrgDetail).Name = s.RouteName("org_detail")
	s.Group.GET("/organization/:id/type", s.UpdateOrgType).Name = s.RouteName("update_org_type")
	s.Group.POST("/organization/:id/type", s.UpdateOrgType).Name = s.RouteName("update_org_type")
	s.Group.GET("/organization/:slug", s.OrgDetail).Name = s.RouteName("org_detail")
	s.Group.GET("/organization/:slug/type", s.UpdateOrgType).Name = s.RouteName("update_org_type")
	s.Group.POST("/organization/:slug/type", s.UpdateOrgType).Name = s.RouteName("update_org_type")
	s.Group.GET("/domain", s.DomainList).Name = s.RouteName("domain_list")
	s.Group.GET("/domain/:id", s.DomainDetail).Name = s.RouteName("domain_detail")
	s.Group.GET("/domain/add", s.DomainCreate).Name = s.RouteName("domain_create")


@@ 237,8 238,8 @@ func (s *Service) Dashboard(c echo.Context) error {
}

func (s *Service) UpdateOrgType(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
	slug := strings.ToLower(c.Param("slug"))
	if slug == "" {
		return echo.NotFoundHandler(c)
	}
	lt := localizer.GetSessionLocalizer(c)


@@ 262,8 263,8 @@ func (s *Service) UpdateOrgType(c echo.Context) error {
	}
	var result GraphQLResponse
	op := gqlclient.NewOperation(
		`query GetOrganization($id: Int!) {
			getOrganization(id: $id) {
		`query GetOrganization($slug: String!) {
			getOrganization(slug: $slug) {
				id
				name
				slug


@@ 274,8 275,8 @@ func (s *Service) UpdateOrgType(c echo.Context) error {
				}
		}
	}`)
	op.Var("id", id)
	err = links.Execute(c.Request().Context(), op, &result)
	op.Var("slug", slug)
	err := links.Execute(c.Request().Context(), op, &result)
	if err != nil {
		return err
	}


@@ 308,6 309,7 @@ func (s *Service) UpdateOrgType(c echo.Context) error {
			mutation UpdateAdminOrgType($orgSlug: String!, $orgType: Int!) {
				updateAdminOrgType(orgSlug: $orgSlug, orgType: $orgType) {
					id
					slug
				}
			}
		`)


@@ 318,7 320,7 @@ func (s *Service) UpdateOrgType(c echo.Context) error {
			return err
		}
		return c.Redirect(http.StatusMovedPermanently,
			c.Echo().Reverse(s.RouteName("org_detail"), resultUpdated.Org.ID))
			c.Echo().Reverse(s.RouteName("org_detail"), resultUpdated.Org.Slug))
	}

	form.OrgType = result.Org.Settings.Billing.Status


@@ 327,8 329,8 @@ func (s *Service) UpdateOrgType(c echo.Context) error {
}

func (s *Service) OrgDetail(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
	slug := strings.ToLower(c.Param("slug"))
	if slug == "" {
		return echo.NotFoundHandler(c)
	}
	lt := localizer.GetSessionLocalizer(c)


@@ 377,8 379,8 @@ func (s *Service) OrgDetail(c echo.Context) error {
	}
	var result GraphQLResponse
	op := gqlclient.NewOperation(
		`query GetOrganization($id: Int!) {
			getOrganization(id: $id) {
		`query GetOrganization($slug: String!) {
			getOrganization(slug: $slug) {
				id
				name
				slug


@@ 391,8 393,8 @@ func (s *Service) OrgDetail(c echo.Context) error {
				}
		}
	}`)
	op.Var("id", id)
	err = links.Execute(c.Request().Context(), op, &result)
	op.Var("slug", slug)
	err := links.Execute(c.Request().Context(), op, &result)
	if err != nil {
		if graphError, ok := err.(*gqlclient.Error); ok {
			return links.ParseInputErrors(c, graphError, gobwebs.Map{})

M api/graph/generated.go => api/graph/generated.go +227 -243
@@ 376,7 376,7 @@ type ComplexityRoot struct {
		GetOrgLink            func(childComplexity int, hash string) int
		GetOrgLinks           func(childComplexity int, input *model.GetLinkInput) int
		GetOrgMembers         func(childComplexity int, orgSlug string) int
		GetOrganization       func(childComplexity int, id int) int
		GetOrganization       func(childComplexity int, slug string) 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


@@ 477,10 477,8 @@ type OrganizationResolver interface {
type QueryResolver interface {
	Version(ctx context.Context) (*model.Version, error)
	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, input *model.GetOrganizationsInput) ([]*models.Organization, error)
	GetOrganization(ctx context.Context, id int) (*models.Organization, error)
	GetOrganization(ctx context.Context, slug string) (*models.Organization, error)
	GetPaymentHistory(ctx context.Context, input *model.GetPaymentInput) (*model.PaymentCursor, error)
	GetPopularLinks(ctx context.Context, input *model.PopularLinksInput) ([]*models.BaseURL, error)
	GetOrgLink(ctx context.Context, hash string) (*models.OrgLink, error)


@@ 498,6 496,8 @@ type QueryResolver interface {
	Analytics(ctx context.Context, input model.AnalyticsInput) (*model.Analytics, error)
	GetFeed(ctx context.Context, input *model.GetFeedInput) (*model.OrgLinkCursor, error)
	GetFeedFollowing(ctx context.Context) ([]*models.Organization, error)
	GetUsers(ctx context.Context, input *model.GetUserInput) (*model.UserCursor, error)
	GetUser(ctx context.Context, id int) (*models.User, error)
	GetAdminOrganizations(ctx context.Context, input *model.GetAdminOrganizationsInput) (*model.OrganizationCursor, error)
	GetAdminOrgStats(ctx context.Context, id int) (*model.OrganizationStats, error)
	GetAdminBillingStats(ctx context.Context, input *model.AdminBillingInput) (*model.AdminBillingStats, error)


@@ 2302,7 2302,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
			return 0, false
		}

		return e.complexity.Query.GetOrganization(childComplexity, args["id"].(int)), true
		return e.complexity.Query.GetOrganization(childComplexity, args["slug"].(string)), true

	case "Query.getOrganizations":
		if e.complexity.Query.GetOrganizations == nil {


@@ 3551,15 3551,15 @@ func (ec *executionContext) field_Query_getOrgMembers_args(ctx context.Context, 
func (ec *executionContext) field_Query_getOrganization_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	var arg0 int
	if tmp, ok := rawArgs["id"]; ok {
		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
		arg0, err = ec.unmarshalNInt2int(ctx, tmp)
	var arg0 string
	if tmp, ok := rawArgs["slug"]; ok {
		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("slug"))
		arg0, err = ec.unmarshalNString2string(ctx, tmp)
		if err != nil {
			return nil, err
		}
	}
	args["id"] = arg0
	args["slug"] = arg0
	return args, nil
}



@@ 16024,194 16024,6 @@ func (ec *executionContext) fieldContext_Query_me(_ context.Context, field graph
	return fc, nil
}

func (ec *executionContext) _Query_getUsers(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_Query_getUsers(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.Query().GetUsers(rctx, fc.Args["input"].(*model.GetUserInput))
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "PROFILE")
			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.UserCursor); ok {
			return data, nil
		}
		return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/api/graph/model.UserCursor`, 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.UserCursor)
	fc.Result = res
	return ec.marshalNUserCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐUserCursor(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_Query_getUsers(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "Query",
		Field:      field,
		IsMethod:   true,
		IsResolver: true,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			switch field.Name {
			case "result":
				return ec.fieldContext_UserCursor_result(ctx, field)
			case "pageInfo":
				return ec.fieldContext_UserCursor_pageInfo(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type UserCursor", 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_getUsers_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
		ec.Error(ctx, err)
		return fc, err
	}
	return fc, nil
}

func (ec *executionContext) _Query_getUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_Query_getUser(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.Query().GetUser(rctx, fc.Args["id"].(int))
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "PROFILE")
			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.(*models.User); ok {
			return data, nil
		}
		return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/models.User`, 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.(*models.User)
	fc.Result = res
	return ec.marshalNUser2ᚖlinksᚋmodelsᚐUser(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_Query_getUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "Query",
		Field:      field,
		IsMethod:   true,
		IsResolver: true,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			switch field.Name {
			case "id":
				return ec.fieldContext_User_id(ctx, field)
			case "name":
				return ec.fieldContext_User_name(ctx, field)
			case "email":
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReason":
				return ec.fieldContext_User_lockReason(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type User", 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_getUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
		ec.Error(ctx, err)
		return fc, err
	}
	return fc, nil
}

func (ec *executionContext) _Query_getOrganizations(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_Query_getOrganizations(ctx, field)
	if err != nil {


@@ 16336,7 16148,7 @@ func (ec *executionContext) _Query_getOrganization(ctx context.Context, field gr
	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().GetOrganization(rctx, fc.Args["id"].(int))
			return ec.resolvers.Query().GetOrganization(rctx, fc.Args["slug"].(string))
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "ORGS")


@@ 18110,6 17922,178 @@ func (ec *executionContext) fieldContext_Query_getFeedFollowing(_ context.Contex
	return fc, nil
}

func (ec *executionContext) _Query_getUsers(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_Query_getUsers(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.Query().GetUsers(rctx, fc.Args["input"].(*model.GetUserInput))
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			if ec.directives.Admin == nil {
				return nil, errors.New("directive admin is not implemented")
			}
			return ec.directives.Admin(ctx, nil, directive0)
		}

		tmp, err := directive1(rctx)
		if err != nil {
			return nil, graphql.ErrorOnPath(ctx, err)
		}
		if tmp == nil {
			return nil, nil
		}
		if data, ok := tmp.(*model.UserCursor); ok {
			return data, nil
		}
		return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/api/graph/model.UserCursor`, 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.UserCursor)
	fc.Result = res
	return ec.marshalNUserCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐUserCursor(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_Query_getUsers(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "Query",
		Field:      field,
		IsMethod:   true,
		IsResolver: true,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			switch field.Name {
			case "result":
				return ec.fieldContext_UserCursor_result(ctx, field)
			case "pageInfo":
				return ec.fieldContext_UserCursor_pageInfo(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type UserCursor", 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_getUsers_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
		ec.Error(ctx, err)
		return fc, err
	}
	return fc, nil
}

func (ec *executionContext) _Query_getUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_Query_getUser(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.Query().GetUser(rctx, fc.Args["id"].(int))
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			if ec.directives.Admin == nil {
				return nil, errors.New("directive admin is not implemented")
			}
			return ec.directives.Admin(ctx, nil, directive0)
		}

		tmp, err := directive1(rctx)
		if err != nil {
			return nil, graphql.ErrorOnPath(ctx, err)
		}
		if tmp == nil {
			return nil, nil
		}
		if data, ok := tmp.(*models.User); ok {
			return data, nil
		}
		return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/models.User`, 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.(*models.User)
	fc.Result = res
	return ec.marshalNUser2ᚖlinksᚋmodelsᚐUser(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_Query_getUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "Query",
		Field:      field,
		IsMethod:   true,
		IsResolver: true,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			switch field.Name {
			case "id":
				return ec.fieldContext_User_id(ctx, field)
			case "name":
				return ec.fieldContext_User_name(ctx, field)
			case "email":
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReason":
				return ec.fieldContext_User_lockReason(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type User", 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_getUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
		ec.Error(ctx, err)
		return fc, err
	}
	return fc, nil
}

func (ec *executionContext) _Query_getAdminOrganizations(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_Query_getAdminOrganizations(ctx, field)
	if err != nil {


@@ 25894,50 25878,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
		case "getUsers":
			field := field

			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Query_getUsers(ctx, field)
				if res == graphql.Null {
					atomic.AddUint32(&fs.Invalids, 1)
				}
				return res
			}

			rrm := func(ctx context.Context) graphql.Marshaler {
				return ec.OperationContext.RootResolverMiddleware(ctx,
					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
		case "getUser":
			field := field

			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Query_getUser(ctx, field)
				if res == graphql.Null {
					atomic.AddUint32(&fs.Invalids, 1)
				}
				return res
			}

			rrm := func(ctx context.Context) graphql.Marshaler {
				return ec.OperationContext.RootResolverMiddleware(ctx,
					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
		case "getOrganizations":
			field := field



@@ 26350,6 26290,50 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
		case "getUsers":
			field := field

			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Query_getUsers(ctx, field)
				if res == graphql.Null {
					atomic.AddUint32(&fs.Invalids, 1)
				}
				return res
			}

			rrm := func(ctx context.Context) graphql.Marshaler {
				return ec.OperationContext.RootResolverMiddleware(ctx,
					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
		case "getUser":
			field := field

			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Query_getUser(ctx, field)
				if res == graphql.Null {
					atomic.AddUint32(&fs.Invalids, 1)
				}
				return res
			}

			rrm := func(ctx context.Context) graphql.Marshaler {
				return ec.OperationContext.RootResolverMiddleware(ctx,
					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
		case "getAdminOrganizations":
			field := field


M api/graph/schema.graphqls => api/graph/schema.graphqls +4 -8
@@ 678,17 678,11 @@ type Query {
    "Returns the authenticated user."
    me: User! @access(scope: PROFILE, kind: RO)

    "Returns an array of users"
    getUsers(input: GetUserInput): UserCursor! @access(scope: PROFILE, kind: RW)

    "Returns a specific user"
    getUser(id: Int!): User! @access(scope: PROFILE, kind: RW)

    "Returns an array of organizations"
    getOrganizations(input: GetOrganizationsInput): [Organization!]! @access(scope: ORGS, kind: RO)

    "Returns a specific organization"
    getOrganization(id: Int!): Organization @access(scope: ORGS, kind: RO)
    getOrganization(slug: String!): Organization @access(scope: ORGS, kind: RO)

    "Returns payment history based on given input"
    getPaymentHistory(input: GetPaymentInput): PaymentCursor! @access(scope: BILLING, kind: RO)


@@ 738,10 732,12 @@ type Query {
    "Returns an array of organization links from the calling users feed (orgs they follow)"
    getFeed(input: GetFeedInput): OrgLinkCursor! @access(scope: PROFILE, kind: RO)

    "REturns an array of organizations that the calling user follows"
    "Returns an array of organizations that the calling user follows"
    getFeedFollowing: [Organization!]! @access(scope: PROFILE, kind: RO)

    "Admin only. Not open to public calls"
    getUsers(input: GetUserInput): UserCursor! @admin
    getUser(id: Int!): User! @admin
    getAdminOrganizations(input: GetAdminOrganizationsInput): OrganizationCursor! @admin
    getAdminOrgStats(id: Int!): OrganizationStats! @admin
    getAdminBillingStats(input: AdminBillingInput): AdminBillingStats! @admin

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +153 -159
@@ 39,7 39,7 @@ import (
	"golang.org/x/image/draw"
	"golang.org/x/net/idna"
	"netlandish.com/x/gobwebs"
	oauth2 "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"


@@ 4225,155 4225,6 @@ func (r *queryResolver) Me(ctx context.Context) (*models.User, error) {
	return user, nil
}

// GetUsers is the resolver for the ADMIN AREA getUsers field.
func (r *queryResolver) GetUsers(ctx context.Context, input *model.GetUserInput) (*model.UserCursor, error) {
	if input == nil {
		input = &model.GetUserInput{}
	}
	tokenUser := oauth2.ForContext(ctx)
	if tokenUser == nil {
		return nil, valid.ErrAuthorization
	}
	user := tokenUser.User.(*models.User)
	lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
	lt := localizer.GetLocalizer(lang)
	validator := valid.New(ctx)
	if !user.IsSuperUser() {
		validator.Error(lt.Translate("This user is not allowed to perform this action")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil

	}
	opts := &database.FilterOptions{
		Filter: sq.And{
			sq.Eq{"o.org_type": models.OrgTypeUser},
		},
		OrderBy: "u.id DESC",
	}

	if input.Search != nil && *input.Search != "" {
		// We want to search for partial match
		s := links.ParseSearch(*input.Search, true)
		opts.Filter = sq.And{
			opts.Filter,
			sq.Expr(`to_tsvector('simple', u.full_name || ' ' || u.email || ' ' || o.slug)
				@@ websearch_to_tsquery('simple', ?)`, s),
		}

	}

	if input.After != nil && input.Before != nil {
		validator.Error(lt.Translate("You can not send both after and before cursors")).
			WithCode(valid.ErrValidationGlobalCode)
		return nil, nil
	}

	numElements := model.PaginationDefault
	var hasPrevPage bool
	var hasNextPage bool
	if input.After != nil {
		opts.Filter = sq.And{
			opts.Filter,
			sq.LtOrEq{"u.id": input.After.After},
		}
		numElements = input.After.Limit
	} else if input.Before != nil {
		opts.Filter = sq.And{
			opts.Filter,
			sq.GtOrEq{"u.id": input.Before.Before},
		}
		opts.OrderBy = "u.id ASC"
		numElements = input.Before.Limit
	}
	// If limit specifically set, it overrides any cursor limit
	if input.Limit != nil && *input.Limit > 0 {
		numElements = *input.Limit
	}
	if numElements > model.PaginationMax {
		numElements = model.PaginationMax
	}

	opts.Limit = numElements + 2
	users, err := models.GetUsers(ctx, opts)
	if err != nil {
		return nil, err
	}
	c := model.Cursor{Limit: numElements}
	count := len(users)
	if count > 0 {
		// Checking for previous page
		if input.Before != nil {
			if int(users[0].ID) == input.Before.Before {
				hasPrevPage = true
				users = users[1:]
				count--
			}
		} else if input.After != nil {
			if int(users[0].ID) == input.After.After {
				hasPrevPage = true
				users = users[1:]
				count--
			}
		}
		if count == opts.Limit {
			// No previous page
			users = users[:count-1]
			count--
		}

		if count > numElements {
			hasNextPage = true
			users = users[:count-1]
			count--
		}
		if count > 0 {
			if input.Before != nil {
				tmp := hasPrevPage
				hasPrevPage = hasNextPage
				hasNextPage = tmp
				c.After = int(users[0].ID)
				c.Before = int(users[count-1].ID)
			} else {
				c.After = int(users[count-1].ID)
				c.Before = int(users[0].ID)
			}
		} else {
			hasPrevPage = false
		}
	}
	pageInfo := &model.PageInfo{
		Cursor:      c,
		HasNextPage: hasNextPage,
		HasPrevPage: hasPrevPage,
	}
	return &model.UserCursor{Result: users, PageInfo: pageInfo}, nil
}

// GetUser is the resolver for the ADMIN AREA getUser field.
func (r *queryResolver) GetUser(ctx context.Context, id int) (*models.User, error) {
	tokenUser := oauth2.ForContext(ctx)
	if tokenUser == nil {
		return nil, valid.ErrAuthorization
	}
	user := tokenUser.User.(*models.User)
	lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
	lt := localizer.GetLocalizer(lang)
	validator := valid.New(ctx)
	if !user.IsSuperUser() {
		validator.Error(lt.Translate("This user is not allowed to perform this action")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil

	}

	user, err := models.GetUser(ctx, id, false)
	if err != nil {
		return nil, err
	}

	return user, nil
}

// GetOrganizations is the resolver for the getOrganizations field.
func (r *queryResolver) GetOrganizations(ctx context.Context, input *model.GetOrganizationsInput) ([]*models.Organization, error) {
	tokenUser := oauth2.ForContext(ctx)


@@ 4414,7 4265,7 @@ func (r *queryResolver) GetOrganizations(ctx context.Context, input *model.GetOr
}

// GetOrganization is the resolver for the getOrganization field.
func (r *queryResolver) GetOrganization(ctx context.Context, id int) (*models.Organization, error) {
func (r *queryResolver) GetOrganization(ctx context.Context, slug string) (*models.Organization, error) {
	tokenUser := oauth2.ForContext(ctx)
	if tokenUser == nil {
		return nil, valid.ErrAuthorization


@@ 4423,16 4274,10 @@ func (r *queryResolver) GetOrganization(ctx context.Context, id int) (*models.Or
	lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
	lt := localizer.GetLocalizer(lang)
	opts := &database.FilterOptions{
		Filter: sq.Eq{"o.id": id},
		Filter: sq.Eq{"o.slug": strings.ToLower(slug)},
		Limit:  1,
	}
	if !user.IsSuperUser() {
		// XXX Remove this? Not sure why we have this filter in place.
		opts.Filter = sq.And{
			opts.Filter,
			sq.Eq{"o.is_active": true},
		}
	}

	orgs, err := models.GetOrganizations(ctx, opts)
	if err != nil {
		return nil, err


@@ 6243,6 6088,155 @@ func (r *queryResolver) GetFeedFollowing(ctx context.Context) ([]*models.Organiz
	return orgs, nil
}

// GetUsers is the resolver for the ADMIN AREA getUsers field.
func (r *queryResolver) GetUsers(ctx context.Context, input *model.GetUserInput) (*model.UserCursor, error) {
	if input == nil {
		input = &model.GetUserInput{}
	}
	tokenUser := oauth2.ForContext(ctx)
	if tokenUser == nil {
		return nil, valid.ErrAuthorization
	}
	user := tokenUser.User.(*models.User)
	lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
	lt := localizer.GetLocalizer(lang)
	validator := valid.New(ctx)
	if !user.IsSuperUser() {
		validator.Error(lt.Translate("This user is not allowed to perform this action")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil

	}
	opts := &database.FilterOptions{
		Filter: sq.And{
			sq.Eq{"o.org_type": models.OrgTypeUser},
		},
		OrderBy: "u.id DESC",
	}

	if input.Search != nil && *input.Search != "" {
		// We want to search for partial match
		s := links.ParseSearch(*input.Search, true)
		opts.Filter = sq.And{
			opts.Filter,
			sq.Expr(`to_tsvector('simple', u.full_name || ' ' || u.email || ' ' || o.slug)
				@@ websearch_to_tsquery('simple', ?)`, s),
		}

	}

	if input.After != nil && input.Before != nil {
		validator.Error(lt.Translate("You can not send both after and before cursors")).
			WithCode(valid.ErrValidationGlobalCode)
		return nil, nil
	}

	numElements := model.PaginationDefault
	var hasPrevPage bool
	var hasNextPage bool
	if input.After != nil {
		opts.Filter = sq.And{
			opts.Filter,
			sq.LtOrEq{"u.id": input.After.After},
		}
		numElements = input.After.Limit
	} else if input.Before != nil {
		opts.Filter = sq.And{
			opts.Filter,
			sq.GtOrEq{"u.id": input.Before.Before},
		}
		opts.OrderBy = "u.id ASC"
		numElements = input.Before.Limit
	}
	// If limit specifically set, it overrides any cursor limit
	if input.Limit != nil && *input.Limit > 0 {
		numElements = *input.Limit
	}
	if numElements > model.PaginationMax {
		numElements = model.PaginationMax
	}

	opts.Limit = numElements + 2
	users, err := models.GetUsers(ctx, opts)
	if err != nil {
		return nil, err
	}
	c := model.Cursor{Limit: numElements}
	count := len(users)
	if count > 0 {
		// Checking for previous page
		if input.Before != nil {
			if int(users[0].ID) == input.Before.Before {
				hasPrevPage = true
				users = users[1:]
				count--
			}
		} else if input.After != nil {
			if int(users[0].ID) == input.After.After {
				hasPrevPage = true
				users = users[1:]
				count--
			}
		}
		if count == opts.Limit {
			// No previous page
			users = users[:count-1]
			count--
		}

		if count > numElements {
			hasNextPage = true
			users = users[:count-1]
			count--
		}
		if count > 0 {
			if input.Before != nil {
				tmp := hasPrevPage
				hasPrevPage = hasNextPage
				hasNextPage = tmp
				c.After = int(users[0].ID)
				c.Before = int(users[count-1].ID)
			} else {
				c.After = int(users[count-1].ID)
				c.Before = int(users[0].ID)
			}
		} else {
			hasPrevPage = false
		}
	}
	pageInfo := &model.PageInfo{
		Cursor:      c,
		HasNextPage: hasNextPage,
		HasPrevPage: hasPrevPage,
	}
	return &model.UserCursor{Result: users, PageInfo: pageInfo}, nil
}

// GetUser is the resolver for the ADMIN AREA getUser field.
func (r *queryResolver) GetUser(ctx context.Context, id int) (*models.User, error) {
	tokenUser := oauth2.ForContext(ctx)
	if tokenUser == nil {
		return nil, valid.ErrAuthorization
	}
	user := tokenUser.User.(*models.User)
	lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
	lt := localizer.GetLocalizer(lang)
	validator := valid.New(ctx)
	if !user.IsSuperUser() {
		validator.Error(lt.Translate("This user is not allowed to perform this action")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil

	}

	user, err := models.GetUser(ctx, id, false)
	if err != nil {
		return nil, err
	}

	return user, nil
}

// GetAdminOrganizations is the resolver for the ADMIN AREA getAdminOrganizations field.
func (r *queryResolver) GetAdminOrganizations(ctx context.Context, input *model.GetAdminOrganizationsInput) (*model.OrganizationCursor, error) {
	if input == nil {

M templates/admin_billing_list.html => templates/admin_billing_list.html +1 -1
@@ 109,7 109,7 @@
                  <td>${{formatAmt .AmountPaid}}</td>
                  <td>${{formatAmt .PaymentFee}}</td>
                  <td>${{formatAmt .AmountNet}}</td>
                  <td><a href="{{reverse "admin:org_detail" .OrgID}}">{{.OrgSlug}}</a></td>
                  <td><a href="{{reverse "admin:org_detail" .OrgSlug}}">{{.OrgSlug}}</a></td>
                  <td>{{formatDate .CreatedOn}}</td>
              </tr>
          {{end}}

M templates/admin_dashboard.html => templates/admin_dashboard.html +3 -3
@@ 82,8 82,8 @@
      {{ if .orgs }}
          {{range .orgs}}
          <tr>
              <td class="text-center"><a href="{{reverse "admin:org_detail" .ID}}">{{.ID}}</a></td>
              <td class="text-center"><a href="{{reverse "admin:org_detail" .ID}}">{{.Name}}</a></td>
              <td class="text-center"><a href="{{reverse "admin:org_detail" .Slug}}">{{.ID}}</a></td>
              <td class="text-center"><a href="{{reverse "admin:org_detail" .Slug}}">{{.Name}}</a></td>
              <td class="text-center">{{.Slug}}</td>
              <td class="text-center"><a class="text-primary" href="{{reverse "admin:user_detail" .OwnerID}}">{{.OwnerName}}</a></td>
              <td class="text-center">{{formatDate .CreatedOn}}</td>


@@ 137,7 137,7 @@
                    {{$.pd.Data.list}}
                  {{end}}
              </td>
              <td class="text-center">{{if .OrgID.Valid}}<a class="text-primary" href="{{reverse "admin:org_detail" .OrgID.Int64}}">{{.OrgSlug.String}}</a>{{else}}-{{end}}</td>
              <td class="text-center">{{if .OrgSlug.Valid}}<a class="text-primary" href="{{reverse "admin:org_detail" .OrgSlug.String}}">{{.OrgSlug.String}}</a>{{else}}-{{end}}</td>
              <td class="text-center">
                    {{if .IsActive}}
                        <svg style="width:20px" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-primary">

M templates/admin_domains_list.html => templates/admin_domains_list.html +1 -1
@@ 77,7 77,7 @@
                    {{$.pd.Data.list}}
                  {{end}}
              </td>
              <td class="text-center">{{if .OrgID.Valid}}<a class="text-primary" href="{{reverse "admin:org_detail" .OrgID.Int64}}">{{.OrgSlug.String}}</a>{{else}}-{{end}}</td>
              <td class="text-center">{{if .OrgSlug.Valid}}<a class="text-primary" href="{{reverse "admin:org_detail" .OrgSlug.String}}">{{.OrgSlug.String}}</a>{{else}}-{{end}}</td>
              <td class="text-center">
                    {{if .IsActive}}
                        <svg style="width:20px" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-primary">

M templates/admin_org_detail.html => templates/admin_org_detail.html +1 -1
@@ 3,7 3,7 @@
<section class="app-header">
  <h1 class="app-header__title">{{.org.Name}}</h1>
  <div>
      <a href="{{reverse "admin:update_org_type" .org.ID}}" class="button is-small primary">{{.pd.Data.update_type}}</a>
      <a href="{{reverse "admin:update_org_type" .org.Slug}}" class="button is-small primary">{{.pd.Data.update_type}}</a>
      <a class="button primary is-small" href="{{reverse "admin:org_list"}}">{{.pd.Data.back}}</a>
  </div>
</section>

M templates/admin_organization_list.html => templates/admin_organization_list.html +2 -2
@@ 29,8 29,8 @@
      {{ if .orgs }}
          {{range .orgs}}
          <tr>
              <td class="text-center"><a href="{{reverse "admin:org_detail" .ID}}">{{.ID}}</a></td>
              <td class="text-center"><a href="{{reverse "admin:org_detail" .ID}}">{{.Name}}</a></td>
              <td class="text-center"><a href="{{reverse "admin:org_detail" .Slug}}">{{.ID}}</a></td>
              <td class="text-center"><a href="{{reverse "admin:org_detail" .Slug}}">{{.Name}}</a></td>
              <td class="text-center">{{.Slug}}</td>
              <td class="text-center"><a class="text-primary" href="{{reverse "admin:user_detail" .OwnerID}}">{{.OwnerName}}</a></td>
              <td class="text-center">{{formatDate .CreatedOn}}</td>

M templates/admin_update_org_type.html => templates/admin_update_org_type.html +3 -3
@@ 3,11 3,11 @@
<section class="app-header">
  <h1 class="app-header__title">{{.pd.Title}} {{.pd.Data.for}} {{.org.Name}}</h1>
  <div>
      <a href="{{reverse "admin:org_detail" .org.ID}}" class="button primary is-small">{{.pd.Data.back}}</a>
      <a href="{{reverse "admin:org_detail" .org.Slug}}" class="button primary is-small">{{.pd.Data.back}}</a>
  </div>
</section>
<section class="card shadow-card">
  <form id="org-edit-form" action="{{reverse "admin:update_org_type" .org.ID}}" method="POST">
  <form id="org-edit-form" action="{{reverse "admin:update_org_type" .org.Slug}}" method="POST">
    <input type="hidden" name="csrf" value="{{ .CSRF }}">
    {{if .errors._global_ }}
        {{range .errors._global_}}


@@ 29,7 29,7 @@
  </form>
  <footer class="is-right">
      <button form="org-edit-form" type="submit" class="button dark">{{.pd.Data.save}}</button>
      <a class="button error" href="{{reverse "admin:org_detail" .org.ID}}">{{.pd.Data.cancel}}</a>
      <a class="button error" href="{{reverse "admin:org_detail" .org.Slug}}">{{.pd.Data.cancel}}</a>
  </footer>
</section>


M templates/admin_user_detail.html => templates/admin_user_detail.html +2 -2
@@ 71,8 71,8 @@
        <tbody>
          {{range .orgs}}
              <tr>
                <td><a href="{{reverse "admin:org_detail" .ID}}">{{.Name}}</a></td>
                <td><a href="{{reverse "admin:org_detail" .ID}}">{{.Slug}}</a></td>
                <td><a href="{{reverse "admin:org_detail" .Slug}}">{{.Name}}</a></td>
                <td><a href="{{reverse "admin:org_detail" .Slug}}">{{.Slug}}</a></td>
                <td>{{ .DisplayBillingStatus $.context }}</td>
                <td>
                    {{if .IsActive}}