~netlandish/links

b7f6dd6f9cfddd7f15ae57ec1dd8be1d192965e0 — Peter Sanchez a month ago da031b3
Moving organization_users to enum
M accounts/routes.go => accounts/routes.go +3 -1
@@ 278,7 278,9 @@ func (s *Service) Settings(c echo.Context) error {
	return s.Render(c, http.StatusOK, "settings.html", gmap)
}

// CompleteRegister ...
// CompleteRegister is used when a user has been invited to an organization but does not
// yet have an account. For normal user registration "completion", see
// gobwebs/accounts/routes.go
func (s *Service) CompleteRegister(c echo.Context) error {
	if !links.IsRegistrationEnabled(c) {
		return echo.NotFoundHandler(c)

M api/api_test.go => api/api_test.go +4 -4
@@ 182,7 182,7 @@ func TestDirective(t *testing.T) {
		}
		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation AddMember($slug: String!, $email: String!, $perm: Int!) {
			`mutation AddMember($slug: String!, $email: String!, $perm: MemberPermission!) {
					addMember(input: {orgSlug: $slug, email: $email, permission: $perm}) {
						success
						message


@@ 752,7 752,7 @@ func TestAPI(t *testing.T) {
		orgUser := &models.OrgUser{
			OrgID:      2,
			UserID:     2,
			Permission: 0,
			Permission: "READ",
		}
		err = orgUser.Store(dbCtx)
		c.NoError(err)


@@ 775,7 775,7 @@ func TestAPI(t *testing.T) {

		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation AddMember($slug: String!, $email: String!, $perm: Int!) {
			`mutation AddMember($slug: String!, $email: String!, $perm: MemberPermission!) {
					addMember(input: {orgSlug: $slug, email: $email, permission: $perm}) {
						success
						message


@@ 825,7 825,7 @@ func TestAPI(t *testing.T) {

		var result GraphQLResponse
		op = gqlclient.NewOperation(
			`mutation AddMember($slug: String!, $email: String!, $perm: Int!) {
			`mutation AddMember($slug: String!, $email: String!, $perm: MemberPermission!) {
					addMember(input: {orgSlug: $slug, email: $email, permission: $perm}) {
						success
						message

M api/graph/generated.go => api/graph/generated.go +11 -1
@@ 22798,7 22798,7 @@ func (ec *executionContext) unmarshalInputMemberInput(ctx context.Context, obj i
			it.Email = data
		case "permission":
			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("permission"))
			data, err := ec.unmarshalNInt2int(ctx, v)
			data, err := ec.unmarshalNMemberPermission2linksᚋapiᚋgraphᚋmodelᚐMemberPermission(ctx, v)
			if err != nil {
				return it, err
			}


@@ 27892,6 27892,16 @@ func (ec *executionContext) marshalNListingLinkCursor2ᚖlinksᚋapiᚋgraphᚋm
	return ec._ListingLinkCursor(ctx, sel, v)
}

func (ec *executionContext) unmarshalNMemberPermission2linksᚋapiᚋgraphᚋmodelᚐMemberPermission(ctx context.Context, v interface{}) (model.MemberPermission, error) {
	var res model.MemberPermission
	err := res.UnmarshalGQL(v)
	return res, graphql.ErrorOnPath(ctx, err)
}

func (ec *executionContext) marshalNMemberPermission2linksᚋapiᚋgraphᚋmodelᚐMemberPermission(ctx context.Context, sel ast.SelectionSet, v model.MemberPermission) graphql.Marshaler {
	return v
}

func (ec *executionContext) marshalNMetadata2linksᚋmodelsᚐMetadata(ctx context.Context, sel ast.SelectionSet, v models.Metadata) graphql.Marshaler {
	return ec._Metadata(ctx, sel, &v)
}

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +46 -3
@@ 260,9 260,9 @@ type ListingLinkCursor struct {
}

type MemberInput struct {
	OrgSlug    string `json:"orgSlug"`
	Email      string `json:"email"`
	Permission int    `json:"permission"`
	OrgSlug    string           `json:"orgSlug"`
	Email      string           `json:"email"`
	Permission MemberPermission `json:"permission"`
}

type Mutation struct {


@@ 766,3 766,46 @@ func (e *LinkVisibility) UnmarshalGQL(v interface{}) error {
func (e LinkVisibility) MarshalGQL(w io.Writer) {
	fmt.Fprint(w, strconv.Quote(e.String()))
}

type MemberPermission string

const (
	MemberPermissionRead       MemberPermission = "READ"
	MemberPermissionWrite      MemberPermission = "WRITE"
	MemberPermissionAdminWrite MemberPermission = "ADMIN_WRITE"
)

var AllMemberPermission = []MemberPermission{
	MemberPermissionRead,
	MemberPermissionWrite,
	MemberPermissionAdminWrite,
}

func (e MemberPermission) IsValid() bool {
	switch e {
	case MemberPermissionRead, MemberPermissionWrite, MemberPermissionAdminWrite:
		return true
	}
	return false
}

func (e MemberPermission) String() string {
	return string(e)
}

func (e *MemberPermission) UnmarshalGQL(v interface{}) error {
	str, ok := v.(string)
	if !ok {
		return fmt.Errorf("enums must be strings")
	}

	*e = MemberPermission(str)
	if !e.IsValid() {
		return fmt.Errorf("%s is not a valid MemberPermission", str)
	}
	return nil
}

func (e MemberPermission) MarshalGQL(w io.Writer) {
	fmt.Fprint(w, strconv.Quote(e.String()))
}

M api/graph/schema.graphqls => api/graph/schema.graphqls +6 -1
@@ 91,6 91,11 @@ enum LinkType {
  NOTE
}

enum MemberPermission {
  READ
  WRITE
  ADMIN_WRITE
}

# Considering removing these Null* fields:
# https://todo.code.netlandish.com/~netlandish/links/75


@@ 594,7 599,7 @@ input UpdateListingLinkInput {
input MemberInput {
    orgSlug: String!
    email: String!
    permission: Int!
    permission: MemberPermission!
}

input ProfileInput {

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +18 -25
@@ 776,8 776,7 @@ func (r *mutationResolver) AddMember(ctx context.Context, input *model.MemberInp
	validator.Expect(emailRegex.MatchString(input.Email), lt.Translate("Invalid email format")).
		WithField("userEmail").
		WithCode(valid.ErrValidationCode)
	validator.Expect(input.Permission >= models.OrgUserPermissionRead ||
		input.Permission <= models.OrgUserPermissionAdminWrite,
	validator.Expect(models.ValidatePermission(string(input.Permission)),
		lt.Translate("Invalid Permission")).
		WithField("permission").
		WithCode(valid.ErrValidationCode)


@@ 874,7 873,7 @@ func (r *mutationResolver) AddMember(ctx context.Context, input *model.MemberInp
		models.MEMBERINVITATIONCONF, user, time.Now().In(time.UTC).Add(time.Hour*72))

	conf.ConfirmationTarget = sql.NullString{
		String: fmt.Sprintf(`{"org": %d, "perm": %d}`, org.ID, input.Permission),
		String: fmt.Sprintf(`{"org": %d, "perm": "%s"}`, org.ID, input.Permission),
		Valid:  true,
	}
	if err := conf.Store(ctx); err != nil {


@@ 1028,25 1027,27 @@ func (r *mutationResolver) ConfirmMember(ctx context.Context, key string) (*mode
	}

	var org *models.Organization
	var perm int
	if !conf.ConfirmationTarget.Valid {
		validator.Error(lt.Translate("Invalid Confirmation Target")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil
	}
	var data map[string]int
	var data = struct {
		Org  int    `json:"org"`
		Perm string `json:"perm"`
	}{}
	err = json.Unmarshal([]byte(conf.ConfirmationTarget.String), &data)
	if err != nil {
		return nil, err
	}
	orgID, ok := data["org"]
	if !ok {
	orgID := data.Org
	if orgID <= 0 {
		validator.Error(lt.Translate("Organization Not Found")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil
	}
	perm, ok = data["perm"]
	if !ok {
	perm := data.Perm
	if !models.ValidatePermission(perm) {
		validator.Error(lt.Translate("Permission Not Found")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil


@@ 1321,25 1322,27 @@ func (r *mutationResolver) CompleteRegister(ctx context.Context, input *model.Co
		return nil, nil
	}

	var perm int
	if !conf.ConfirmationTarget.Valid {
		validator.Error(lt.Translate("Invalid Confirmation Target")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil
	}
	var data map[string]int
	var data = struct {
		Org  int    `json:"org"`
		Perm string `json:"perm"`
	}{}
	err = json.Unmarshal([]byte(conf.ConfirmationTarget.String), &data)
	if err != nil {
		return nil, err
	}
	targetOrgID, ok := data["org"]
	if !ok {
	targetOrgID := data.Org
	if targetOrgID <= 0 {
		validator.Error(lt.Translate("Organization Not Found")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil
	}
	perm, ok = data["perm"]
	if !ok {
	perm := data.Perm
	if !models.ValidatePermission(perm) {
		validator.Error(lt.Translate("Permission Not Found")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil


@@ 6716,13 6719,3 @@ type organizationResolver struct{ *Resolver }
type organizationSettingsResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type userResolver struct{ *Resolver }

// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
//   - When renaming or deleting a resolver the old code will be put in here. You can safely delete
//     it when you're done.
//   - You have helper methods in this file. Move them out to keep these resolver files clean.
func (r *userResolver) LockReadon(ctx context.Context, obj *models.User) (string, error) {
	panic(fmt.Errorf("not implemented: LockReadon - lockReadon"))
}

M core/inputs.go => core/inputs.go +2 -2
@@ 76,7 76,7 @@ func (uo *UpdateOrganizationForm) Validate(c echo.Context) error {
// AddMemberForm ...
type AddMemberForm struct {
	Email      string `form:"email" validate:"required,email"`
	Permission int    `form:"permission" validate:"number,gte=0,lte=2"`
	Permission string `form:"permission" validate:"oneof=READ WRITE ADMIN_WRITE"`
}

// Validate ...


@@ 85,7 85,7 @@ func (a *AddMemberForm) Validate(c echo.Context) error {
	errs := validate.FormFieldBinder(c, a).
		FailFast(false).
		String("email", &a.Email).
		Int("permission", &a.Permission).
		String("permission", &a.Permission).
		BindErrors()
	if errs != nil {
		return validate.GetInputErrors(errs)

M core/routes.go => core/routes.go +9 -3
@@ 918,7 918,7 @@ func (s *Service) OrgUpdate(c echo.Context) error {
		}
		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation UpdateOrganization($currentSlug: String!, $name: String!, $perm: Int,
			`mutation UpdateOrganization($currentSlug: String!, $name: String!, $perm: LinkVisibility,
										 $slug: String!, $image: Upload, $deleteImg: Boolean,
										 $isActive: Boolean) {
				updateOrganization(input: {


@@ 1017,17 1017,23 @@ func (s *Service) OrgMembersAdd(c echo.Context) error {
	pd.Data["back"] = lt.Translate("Back")
	pd.Data["cancel"] = lt.Translate("Cancel")
	pd.Data["continue_to_upgrade"] = lt.Translate("Continue to Upgrade")
	permissions := map[int]string{
	permissions := map[string]string{
		models.OrgUserPermissionRead:       lt.Translate("Read"),
		models.OrgUserPermissionWrite:      lt.Translate("Write"),
		models.OrgUserPermissionAdminWrite: lt.Translate("Admin Write"),
	}
	permOrder := []string{
		models.OrgUserPermissionRead,
		models.OrgUserPermissionWrite,
		models.OrgUserPermissionAdminWrite,
	}
	gmap := gobwebs.Map{
		"pd":             pd,
		"settingSection": true,
		"navFlag":        "settings",
		"org":            org,
		"permissions":    permissions,
		"permOrder":      permOrder,
	}

	form := &AddMemberForm{}


@@ 1053,7 1059,7 @@ func (s *Service) OrgMembersAdd(c echo.Context) error {
		slug := c.Param("slug")
		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation AddMember($slug: String!, $email: String!, $perm: Int!) {
			`mutation AddMember($slug: String!, $email: String!, $perm: MemberPermission!) {
					addMember(input: {orgSlug: $slug, email: $email, permission: $perm}) {
						success
						message

M migrations/0001_initial.down.sql => migrations/0001_initial.down.sql +1 -0
@@ 38,3 38,4 @@ DROP TYPE IF EXISTS domain_status;
DROP TYPE IF EXISTS invoice_status;
DROP TYPE IF EXISTS org_link_visibility;
DROP TYPE IF EXISTS org_link_type;
DROP TYPE IF EXISTS org_user_perm;

M migrations/0001_initial.up.sql => migrations/0001_initial.up.sql +10 -1
@@ 70,6 70,15 @@ EXCEPTION
    WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
  CREATE TYPE org_link_perm AS ENUM (
    'READ',
    'WRITE',
    'ADMIN_WRITE'
  );
EXCEPTION
    WHEN duplicate_object THEN null;
END $$;


CREATE TABLE users (


@@ 185,7 194,7 @@ CREATE TABLE organization_users (
  id SERIAL PRIMARY KEY,
  org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL,
  user_id INT REFERENCES users (id) ON DELETE CASCADE NOT NULL,
  permission INT DEFAULT 0,
  permission org_link_perm default 'READ',
  is_active BOOLEAN DEFAULT TRUE,
  created_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,

M models/models.go => models/models.go +1 -1
@@ 97,7 97,7 @@ type OrgUser struct {
	ID         int       `db:"id"`
	OrgID      int       `db:"org_id"`
	UserID     int       `db:"user_id"`
	Permission int       `db:"permission"`
	Permission string    `db:"permission"`
	IsActive   bool      `db:"is_active"`
	CreatedOn  time.Time `db:"created_on"`
	UpdatedOn  time.Time `db:"updated_on"`

M models/org_link.go => models/org_link.go +0 -1
@@ 414,5 414,4 @@ func ValidateLinkVisibility(vis string) bool {
	}
	_, ok := visMap[vis]
	return ok

}

M models/org_user.go => models/org_user.go +14 -3
@@ 13,11 13,11 @@ import (

const (
	// OrgUserPermissionRead can read org data
	OrgUserPermissionRead int = iota
	OrgUserPermissionRead string = "READ"
	// OrgUserPermissionWrite can write org links and notes
	OrgUserPermissionWrite
	OrgUserPermissionWrite string = "WRITE"
	// OrgUserPermissionAdminWrite can write org management items (members, domains, etc.)
	OrgUserPermissionAdminWrite
	OrgUserPermissionAdminWrite string = "ADMIN_WRITE"
)

// GetOrgUsers ...


@@ 169,3 169,14 @@ func ToggleOrgUserBatch(ctx context.Context, opts *database.FilterOptions, flag 
	})
	return err
}

// ValidatePermission ensures proper value for OrgLink.Visibility
func ValidatePermission(perm string) bool {
	perMap := map[string]bool{
		OrgUserPermissionRead:       true,
		OrgUserPermissionWrite:      true,
		OrgUserPermissionAdminWrite: true,
	}
	_, ok := perMap[perm]
	return ok
}

M models/organization.go => models/organization.go +1 -1
@@ 214,7 214,7 @@ func (o *Organization) ToLocalTZ(tz string) error {
	return nil
}

func (o *Organization) permCheck(ctx context.Context, user *User, perm int) bool {
func (o *Organization) permCheck(ctx context.Context, user *User, perm string) bool {
	if o.OwnerID == int(user.ID) {
		return true
	}

M models/schema.sql => models/schema.sql +6 -1
@@ 45,6 45,11 @@ CREATE TYPE org_link_type AS ENUM (
  'NOTE'
);

CREATE TYPE org_link_perm AS ENUM (
  'READ',
  'WRITE',
  'ADMIN_WRITE'
);

CREATE TABLE users (
  id SERIAL PRIMARY KEY,


@@ 160,7 165,7 @@ CREATE TABLE organization_users (
  id SERIAL PRIMARY KEY,
  org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL,
  user_id INT REFERENCES users (id) ON DELETE CASCADE NOT NULL,
  permission INT DEFAULT 0,
  permission org_link_perm default 'READ',
  is_active BOOLEAN DEFAULT TRUE,
  created_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,

M models/user.go => models/user.go +3 -3
@@ 227,7 227,7 @@ func (u *User) WritePassword(ctx context.Context) error {
}

// GetOrgs ...
func (u *User) GetOrgs(ctx context.Context, perm int) ([]*Organization, error) {
func (u *User) GetOrgs(ctx context.Context, perm string) ([]*Organization, error) {
	if u.ID == 0 {
		return nil, fmt.Errorf("User object not populated")
	}


@@ 254,7 254,7 @@ func (u *User) GetOrgs(ctx context.Context, perm int) ([]*Organization, error) {
}

// GetOrgsSlug ...
func (u *User) GetOrgsSlug(ctx context.Context, perm int, slug string) (*Organization, error) {
func (u *User) GetOrgsSlug(ctx context.Context, perm string, slug string) (*Organization, error) {
	orgs, err := u.GetOrgs(ctx, perm)
	if err != nil {
		return nil, err


@@ 268,7 268,7 @@ func (u *User) GetOrgsSlug(ctx context.Context, perm int, slug string) (*Organiz
}

// GetOrgsID ...
func (u *User) GetOrgsID(ctx context.Context, perm int, id int) (*Organization, error) {
func (u *User) GetOrgsID(ctx context.Context, perm string, id int) (*Organization, error) {
	orgs, err := u.GetOrgs(ctx, perm)
	if err != nil {
		return nil, err

M templates/member_add.html => templates/member_add.html +2 -2
@@ 27,8 27,8 @@
        <div>
          <label for="permission">{{.pd.Data.permission}}</label>
          <select name="permission" required>
            {{ range $key, $val := .permissions  }}
            <option value="{{ $key }}"{{if eq $key $.form.Permission}} selected{{end}}>{{$val}}</option>
            {{ range $key := .permOrder  }}
            <option value="{{ $key }}"{{if eq $key $.form.Permission}} selected{{end}}>{{ index $.permissions $key}}</option>
            {{end}}
          </select>
          {{ with .errors.Permission }}