~netlandish/links

65b536bc1d89972cb901b632baccad6a89e42403 — Peter Sanchez 4 months ago 99219ee
Removing `is_active` from User model as it's not used anywhere.
Instead we rely on `is_verified` and `is_locked` for access control.
M admin/input.go => admin/input.go +4 -4
@@ 21,9 21,9 @@ func (l *UserLockForm) Validate(c echo.Context) error {
}

type UserForm struct {
	Name     string `form:"name" validate:"required"`
	Email    string `form:"email" validate:"required,email"`
	IsActive bool   `form:"is_active"`
	Name       string `form:"name" validate:"required"`
	Email      string `form:"email" validate:"required,email"`
	IsVerified bool   `form:"is_verified"`
}

func (u *UserForm) Validate(c echo.Context) error {


@@ 31,7 31,7 @@ func (u *UserForm) Validate(c echo.Context) error {
		FailFast(false).
		String("name", &u.Name).
		String("email", &u.Email).
		Bool("is_active", &u.IsActive).
		Bool("is_verified", &u.IsVerified).
		BindErrors()
	if errs != nil {
		return validate.GetInputErrors(errs)

M admin/routes.go => admin/routes.go +19 -8
@@ 89,6 89,8 @@ func (s *Service) Dashboard(c echo.Context) error {
	pd.Data["users"] = lt.Translate("Users")
	pd.Data["no_users"] = lt.Translate("No Users")
	pd.Data["is_active"] = lt.Translate("Is Active")
	pd.Data["is_verified"] = lt.Translate("Is Verified")
	pd.Data["is_locked"] = lt.Translate("Is Locked")
	pd.Data["name"] = lt.Translate("Name")
	pd.Data["created_on"] = lt.Translate("Created On")
	pd.Data["see_more"] = lt.Translate("See more")


@@ 130,7 132,8 @@ func (s *Service) Dashboard(c echo.Context) error {
					id
					name
					email
					isActive
					isEmailVerified
					isLocked
					createdOn
				}
			}


@@ 392,6 395,9 @@ func (s *Service) OrgDetail(c echo.Context) error {
	op.Var("id", id)
	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{})
		}
		return err
	}



@@ 1352,6 1358,9 @@ func (s *Service) UserDetail(c echo.Context) error {
	pd.Data["email"] = lt.Translate("Email")
	pd.Data["created_on"] = lt.Translate("Created On")
	pd.Data["is_active"] = lt.Translate("Is Active")
	pd.Data["is_verified"] = lt.Translate("Is Verified")
	pd.Data["is_locked"] = lt.Translate("Is Locked")
	pd.Data["lock_reason"] = lt.Translate("Lock Reason")
	pd.Data["name"] = lt.Translate("Name")
	pd.Data["yes"] = lt.Translate("Yes")
	pd.Data["edit"] = lt.Translate("Edit")


@@ 1382,8 1391,9 @@ func (s *Service) UserDetail(c echo.Context) error {
				name
				email
				createdOn
				isActive
				isEmailVerified
				isLocked
				lockReason
		}
	}`)
	op.Var("id", id)


@@ 1456,11 1466,11 @@ func (s *Service) UserUpdate(c echo.Context) error {
	lt := localizer.GetSessionLocalizer(c)
	pd := localizer.NewPageData(lt.Translate("Edit User"))
	pd.Data["save"] = lt.Translate("Save")
	pd.Data["is_active"] = lt.Translate("Is Active")
	pd.Data["name"] = lt.Translate("Name")
	pd.Data["email"] = lt.Translate("Email")
	pd.Data["back"] = lt.Translate("Back")
	pd.Data["cancel"] = lt.Translate("Cancel")
	pd.Data["is_verified"] = lt.Translate("Is Verified")
	gmap := gobwebs.Map{
		"user":    user,
		"pd":      pd,


@@ 1485,8 1495,8 @@ func (s *Service) UserUpdate(c echo.Context) error {
		}
		var result GraphQLResponse
		op := gqlclient.NewOperation(`
			mutation UpdateUser($id: Int!, $isActive: Boolean!, $name: String!, $email: String!) {
				updateAdminUser(input: {id: $id, isActive: $isActive, email: $email, name: $name}) {
			mutation UpdateUser($id: Int!, $isVerified: Boolean!, $name: String!, $email: String!) {
				updateAdminUser(input: {id: $id, isVerified: $isVerified, email: $email, name: $name}) {
					id
				}
			}


@@ 1494,7 1504,7 @@ func (s *Service) UserUpdate(c echo.Context) error {
		op.Var("id", id)
		op.Var("name", form.Name)
		op.Var("email", form.Email)
		op.Var("isActive", form.IsActive)
		op.Var("isVerified", form.IsVerified)
		err = links.Execute(c.Request().Context(), op, &result)
		if err != nil {
			if graphError, ok := err.(*gqlclient.Error); ok {


@@ 1511,9 1521,9 @@ func (s *Service) UserUpdate(c echo.Context) error {
		messages.Success(c, lt.Translate("User updated successfully"))
		return c.Redirect(http.StatusMovedPermanently, c.Echo().Reverse(s.RouteName("user_detail"), id))
	}
	form.IsActive = user.IsActive
	form.Name = user.Name
	form.Email = user.Email
	form.IsVerified = user.IsVerified()
	return links.Render(c, http.StatusOK, "admin_user_edit.html", gmap)
}



@@ 1571,6 1581,7 @@ func (s *Service) UserList(c echo.Context) error {
	pd := localizer.NewPageData(lt.Translate("User List"))
	pd.Data["user_list"] = lt.Translate("User List")
	pd.Data["is_active"] = lt.Translate("Is Active")
	pd.Data["is_verified"] = lt.Translate("Is Verified")
	pd.Data["name"] = lt.Translate("Name")
	pd.Data["created_on"] = lt.Translate("Created On")
	pd.Data["next"] = lt.Translate("Next")


@@ 1600,7 1611,7 @@ func (s *Service) UserList(c echo.Context) error {
					id
					name
					email
					isActive
					isEmailVerified
					isLocked
					createdOn
				}

M api/gqlgen.yml => api/gqlgen.yml +0 -1
@@ 53,7 53,6 @@ models:
  Cursor:
    model:
      - links/api/graph/model.Cursor

  ID:
    model:
      - github.com/99designs/gqlgen/graphql.ID

M api/graph/generated.go => api/graph/generated.go +66 -81
@@ 405,13 405,13 @@ type ComplexityRoot struct {
	}

	User struct {
		CreatedOn  func(childComplexity int) int
		Email      func(childComplexity int) int
		ID         func(childComplexity int) int
		IsActive   func(childComplexity int) int
		IsLocked   func(childComplexity int) int
		LockReadon func(childComplexity int) int
		Name       func(childComplexity int) int
		CreatedOn       func(childComplexity int) int
		Email           func(childComplexity int) int
		ID              func(childComplexity int) int
		IsEmailVerified func(childComplexity int) int
		IsLocked        func(childComplexity int) int
		LockReason      func(childComplexity int) int
		Name            func(childComplexity int) int
	}

	UserCursor struct {


@@ 501,8 501,6 @@ type QueryResolver interface {
}
type UserResolver interface {
	ID(ctx context.Context, obj *models.User) (int, error)

	LockReadon(ctx context.Context, obj *models.User) (string, error)
}

type executableSchema struct {


@@ 2470,12 2468,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.User.ID(childComplexity), true

	case "User.isActive":
		if e.complexity.User.IsActive == nil {
	case "User.isEmailVerified":
		if e.complexity.User.IsEmailVerified == nil {
			break
		}

		return e.complexity.User.IsActive(childComplexity), true
		return e.complexity.User.IsEmailVerified(childComplexity), true

	case "User.isLocked":
		if e.complexity.User.IsLocked == nil {


@@ 2484,12 2482,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.User.IsLocked(childComplexity), true

	case "User.lockReadon":
		if e.complexity.User.LockReadon == nil {
	case "User.lockReason":
		if e.complexity.User.LockReason == nil {
			break
		}

		return e.complexity.User.LockReadon(childComplexity), true
		return e.complexity.User.LockReason(childComplexity), true

	case "User.name":
		if e.complexity.User.Name == nil {


@@ 9558,12 9556,12 @@ func (ec *executionContext) fieldContext_Mutation_register(ctx context.Context, 
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isActive":
				return ec.fieldContext_User_isActive(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReadon":
				return ec.fieldContext_User_lockReadon(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)
		},


@@ 9657,12 9655,12 @@ func (ec *executionContext) fieldContext_Mutation_completeRegister(ctx context.C
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isActive":
				return ec.fieldContext_User_isActive(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReadon":
				return ec.fieldContext_User_lockReadon(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)
		},


@@ 9756,12 9754,12 @@ func (ec *executionContext) fieldContext_Mutation_updateProfile(ctx context.Cont
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isActive":
				return ec.fieldContext_User_isActive(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReadon":
				return ec.fieldContext_User_lockReadon(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)
		},


@@ 11710,12 11708,12 @@ func (ec *executionContext) fieldContext_Mutation_updateAdminUser(ctx context.Co
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isActive":
				return ec.fieldContext_User_isActive(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReadon":
				return ec.fieldContext_User_lockReadon(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)
		},


@@ 15774,12 15772,12 @@ func (ec *executionContext) fieldContext_Query_me(ctx context.Context, field gra
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isActive":
				return ec.fieldContext_User_isActive(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReadon":
				return ec.fieldContext_User_lockReadon(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)
		},


@@ 15951,12 15949,12 @@ func (ec *executionContext) fieldContext_Query_getUser(ctx context.Context, fiel
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isActive":
				return ec.fieldContext_User_isActive(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReadon":
				return ec.fieldContext_User_lockReadon(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)
		},


@@ 16657,12 16655,12 @@ func (ec *executionContext) fieldContext_Query_getOrgMembers(ctx context.Context
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isActive":
				return ec.fieldContext_User_isActive(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReadon":
				return ec.fieldContext_User_lockReadon(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)
		},


@@ 18930,8 18928,8 @@ func (ec *executionContext) fieldContext_User_createdOn(ctx context.Context, fie
	return fc, nil
}

func (ec *executionContext) _User_isActive(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_User_isActive(ctx, field)
func (ec *executionContext) _User_isEmailVerified(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_User_isEmailVerified(ctx, field)
	if err != nil {
		return graphql.Null
	}


@@ 18945,7 18943,7 @@ func (ec *executionContext) _User_isActive(ctx context.Context, field graphql.Co
	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 obj.IsActive, nil
			return obj.IsEmailVerified, nil
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "PROFILE")


@@ 18989,7 18987,7 @@ func (ec *executionContext) _User_isActive(ctx context.Context, field graphql.Co
	return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_User_isActive(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
func (ec *executionContext) fieldContext_User_isEmailVerified(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "User",
		Field:      field,


@@ 19074,8 19072,8 @@ func (ec *executionContext) fieldContext_User_isLocked(ctx context.Context, fiel
	return fc, nil
}

func (ec *executionContext) _User_lockReadon(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_User_lockReadon(ctx, field)
func (ec *executionContext) _User_lockReason(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_User_lockReason(ctx, field)
	if err != nil {
		return graphql.Null
	}


@@ 19089,7 19087,7 @@ func (ec *executionContext) _User_lockReadon(ctx context.Context, field graphql.
	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.User().LockReadon(rctx, obj)
			return obj.LockReason, nil
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "PROFILE")


@@ 19133,12 19131,12 @@ func (ec *executionContext) _User_lockReadon(ctx context.Context, field graphql.
	return ec.marshalNString2string(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_User_lockReadon(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
func (ec *executionContext) fieldContext_User_lockReason(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "User",
		Field:      field,
		IsMethod:   true,
		IsResolver: true,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type String does not have child fields")
		},


@@ 19193,12 19191,12 @@ func (ec *executionContext) fieldContext_UserCursor_result(ctx context.Context, 
				return ec.fieldContext_User_email(ctx, field)
			case "createdOn":
				return ec.fieldContext_User_createdOn(ctx, field)
			case "isActive":
				return ec.fieldContext_User_isActive(ctx, field)
			case "isEmailVerified":
				return ec.fieldContext_User_isEmailVerified(ctx, field)
			case "isLocked":
				return ec.fieldContext_User_isLocked(ctx, field)
			case "lockReadon":
				return ec.fieldContext_User_lockReadon(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)
		},


@@ 23388,7 23386,7 @@ func (ec *executionContext) unmarshalInputUpdateUserInput(ctx context.Context, o
		asMap[k] = v
	}

	fieldsInOrder := [...]string{"id", "email", "name", "isActive"}
	fieldsInOrder := [...]string{"id", "email", "name", "isVerified"}
	for _, k := range fieldsInOrder {
		v, ok := asMap[k]
		if !ok {


@@ 23419,11 23417,11 @@ func (ec *executionContext) unmarshalInputUpdateUserInput(ctx context.Context, o
			if err != nil {
				return it, err
			}
		case "isActive":
		case "isVerified":
			var err error

			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("isActive"))
			it.IsActive, err = ec.unmarshalNBoolean2bool(ctx, v)
			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("isVerified"))
			it.IsVerified, err = ec.unmarshalNBoolean2bool(ctx, v)
			if err != nil {
				return it, err
			}


@@ 26432,9 26430,9 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&invalids, 1)
			}
		case "isActive":
		case "isEmailVerified":

			out.Values[i] = ec._User_isActive(ctx, field, obj)
			out.Values[i] = ec._User_isEmailVerified(ctx, field, obj)

			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&invalids, 1)


@@ 26446,26 26444,13 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&invalids, 1)
			}
		case "lockReadon":
			field := field

			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._User_lockReadon(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			}
		case "lockReason":

			out.Concurrently(i, func() graphql.Marshaler {
				return innerFunc(ctx)
			out.Values[i] = ec._User_lockReason(ctx, field, obj)

			})
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&invalids, 1)
			}
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +4 -4
@@ 430,10 430,10 @@ type UpdateOrganizationInput struct {
}

type UpdateUserInput struct {
	ID       int    `json:"id"`
	Email    string `json:"email"`
	Name     string `json:"name"`
	IsActive bool   `json:"isActive"`
	ID         int    `json:"id"`
	Email      string `json:"email"`
	Name       string `json:"name"`
	IsVerified bool   `json:"isVerified"`
}

type UserCursor struct {

M api/graph/schema.graphqls => api/graph/schema.graphqls +3 -3
@@ 87,9 87,9 @@ type User {
    name: String!
    email: String!
    createdOn: Time! @access(scope: PROFILE, kind: RO)
    isActive: Boolean! @access(scope: PROFILE, kind: RO)
    isEmailVerified: Boolean! @access(scope: PROFILE, kind: RO)
    isLocked: Boolean! @access(scope: PROFILE, kind: RO)
    lockReadon: String! @access(scope: PROFILE, kind: RO)
    lockReason: String! @access(scope: PROFILE, kind: RO)
}

type BillingSettings {


@@ 576,7 576,7 @@ input UpdateUserInput {
    id: Int!
    email: String!
    name: String!
    isActive: Boolean!
    isVerified: Boolean!
}

input CompleteRegisterInput {

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +19 -10
@@ 3994,9 3994,9 @@ func (r *mutationResolver) UpdateAdminUser(ctx context.Context, input *model.Upd
		return nil, nil
	}
	currUser := users[0]
	currUser.IsActive = input.IsActive
	currUser.Email = input.Email
	currUser.Name = input.Name
	currUser.SetVerified(input.IsVerified)
	err = currUser.Store(ctx)
	if err != nil {
		return nil, err


@@ 4278,11 4278,15 @@ 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.And{
			sq.Eq{"o.id": id},
		Filter: sq.Eq{"o.id": id},
		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},
		},
		Limit: 1,
		}
	}
	orgs, err := models.GetOrganizations(ctx, opts)
	if err != nil {


@@ 6540,11 6544,6 @@ func (r *userResolver) ID(ctx context.Context, obj *models.User) (int, error) {
	return int(obj.ID), nil
}

// LockReadon is the resolver for the lockReadon field.
func (r *userResolver) LockReadon(ctx context.Context, obj *models.User) (string, error) {
	panic(fmt.Errorf("not implemented: LockReadon - lockReadon"))
}

// Domain returns DomainResolver implementation.
func (r *Resolver) Domain() DomainResolver { return &domainResolver{r} }



@@ 6569,3 6568,13 @@ type orgLinkResolver struct{ *Resolver }
type organizationResolver 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 migrations/0001_initial.up.sql => migrations/0001_initial.up.sql +0 -1
@@ 17,7 17,6 @@ CREATE TABLE users (
  email VARCHAR ( 255 ) UNIQUE NOT NULL,
  picture VARCHAR(1024) DEFAULT '',
  settings JSONB DEFAULT '{}',
  is_active BOOLEAN DEFAULT TRUE,
  is_verified BOOLEAN DEFAULT FALSE,
  is_superuser BOOLEAN DEFAULT FALSE,
  is_staff BOOLEAN DEFAULT FALSE,

M models/models.go => models/models.go +2 -2
@@ 26,11 26,11 @@ type User struct {
	accounts.BaseUser
	Name       string       `db:"full_name" json:"name"`
	Settings   UserSettings `db:"settings"`
	IsActive   bool         `db:"is_active"`
	IsLocked   bool         `db:"is_locked" json:"isLocked"`
	LockReason string       `db:"lock_reason" json:"lockReason"`

	OrgSlug sql.NullString `db:"-" json:"-"` // This field counts like the username for personal organization context
	OrgSlug         sql.NullString `db:"-" json:"-"` // This field counts like the username for personal organization context
	IsEmailVerified bool           `db:"-" json:"isEmailVerified"`
}

// Organization is...

M models/organization.go => models/organization.go +66 -66
@@ 55,15 55,65 @@ func (os *OrganizationSettings) Scan(value interface{}) error {
	return json.Unmarshal(b, &os)
}

// ToLocalTZ convert UTC date to local tz
func (o *Organization) ToLocalTZ(tz string) error {
	loc, err := time.LoadLocation(tz)
	if err != nil {
		return err
// GetOrganizations ...
func GetOrganizations(ctx context.Context, opts *database.FilterOptions) ([]*Organization, error) {
	if opts == nil {
		opts = &database.FilterOptions{}
	}
	o.CreatedOn = o.CreatedOn.In(loc)
	o.UpdatedOn = o.UpdatedOn.In(loc)
	return nil
	tz := timezone.ForContext(ctx)
	orgs := make([]*Organization, 0)
	if err := database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error {
		q := opts.GetBuilder(nil)
		rows, err := q.
			Columns("o.id", "o.owner_id", "o.org_type", "o.name", "o.slug", "o.image", "o.timezone",
				"o.settings", "o.is_active", "o.created_on", "o.updated_on", "u.full_name", "sc.id",
				"mc.id").
			From("organizations o").
			Join("users u ON o.owner_id = u.id").
			LeftJoin("organization_users ou ON o.id = ou.org_id").
			LeftJoin("slack_connections sc ON o.id = sc.org_id").
			LeftJoin("mattermost_connections mc ON o.id = mc.org_id").
			LeftJoin("domains d ON o.id = d.org_id").
			LeftJoin("followers f ON f.org_id = o.id").
			Distinct().
			PlaceholderFormat(sq.Dollar).
			RunWith(tx).
			QueryContext(ctx)
		if err != nil {
			if err == sql.ErrNoRows {
				return nil
			}
			return err
		}
		defer rows.Close()

		for rows.Next() {
			var org Organization
			if err = rows.Scan(&org.ID, &org.OwnerID, &org.OrgType, &org.Name, &org.Slug,
				&org.Image, &org.Timezone, &org.Settings, &org.IsActive, &org.CreatedOn, &org.UpdatedOn,
				&org.OwnerName, &org.SlackConnID, &org.MattermostConnID,
			); err != nil {
				return err
			}

			err = org.ToLocalTZ(tz)
			if err != nil {
				return err
			}
			orgs = append(orgs, &org)
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return orgs, nil
}

// GetOrganization ...
func GetOrganization(ctx context.Context, id int) (*Organization, error) {
	o := &Organization{ID: id}
	err := o.Load(ctx)
	return o, err
}

// Load  organization


@@ 153,65 203,15 @@ func (o *Organization) Delete(ctx context.Context) error {
	return err
}

// GetOrganization ...
func GetOrganization(ctx context.Context, id int) (*Organization, error) {
	o := &Organization{ID: id}
	err := o.Load(ctx)
	return o, err
}

// GetOrganizations ...
func GetOrganizations(ctx context.Context, opts *database.FilterOptions) ([]*Organization, error) {
	if opts == nil {
		opts = &database.FilterOptions{}
	}
	tz := timezone.ForContext(ctx)
	orgs := make([]*Organization, 0)
	if err := database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error {
		q := opts.GetBuilder(nil)
		rows, err := q.
			Columns("o.id", "o.owner_id", "o.org_type", "o.name", "o.slug", "o.image", "o.timezone",
				"o.settings", "o.is_active", "o.created_on", "o.updated_on", "u.full_name", "sc.id",
				"mc.id").
			From("organizations o").
			Join("users u ON o.owner_id = u.id").
			LeftJoin("organization_users ou ON o.id = ou.org_id").
			LeftJoin("slack_connections sc ON o.id = sc.org_id").
			LeftJoin("mattermost_connections mc ON o.id = mc.org_id").
			LeftJoin("domains d ON o.id = d.org_id").
			LeftJoin("followers f ON f.org_id = o.id").
			Distinct().
			PlaceholderFormat(sq.Dollar).
			RunWith(tx).
			QueryContext(ctx)
		if err != nil {
			if err == sql.ErrNoRows {
				return nil
			}
			return err
		}
		defer rows.Close()

		for rows.Next() {
			var org Organization
			if err = rows.Scan(&org.ID, &org.OwnerID, &org.OrgType, &org.Name, &org.Slug,
				&org.Image, &org.Timezone, &org.Settings, &org.IsActive, &org.CreatedOn, &org.UpdatedOn,
				&org.OwnerName, &org.SlackConnID, &org.MattermostConnID,
			); err != nil {
				return err
			}

			err = org.ToLocalTZ(tz)
			if err != nil {
				return err
			}
			orgs = append(orgs, &org)
		}
		return nil
	}); err != nil {
		return nil, err
// ToLocalTZ convert UTC date to local tz
func (o *Organization) ToLocalTZ(tz string) error {
	loc, err := time.LoadLocation(tz)
	if err != nil {
		return err
	}
	return orgs, nil
	o.CreatedOn = o.CreatedOn.In(loc)
	o.UpdatedOn = o.UpdatedOn.In(loc)
	return nil
}

func (o *Organization) permCheck(ctx context.Context, user *User, perm int) bool {

M models/schema.sql => models/schema.sql +0 -1
@@ 17,7 17,6 @@ CREATE TABLE users (
  email VARCHAR ( 255 ) UNIQUE NOT NULL,
  picture VARCHAR(1024) DEFAULT '',
  settings JSONB DEFAULT '{}',
  is_active BOOLEAN DEFAULT TRUE,
  is_verified BOOLEAN DEFAULT FALSE,
  is_superuser BOOLEAN DEFAULT FALSE,
  is_staff BOOLEAN DEFAULT FALSE,

M models/user.go => models/user.go +5 -5
@@ 72,7 72,7 @@ func GetUser(ctx context.Context, uid any, markAuth bool) (*User, error) {
		err := sq.
			Select().
			Columns("u.id", "u.full_name", "u.password", "u.email", "u.settings", "u.is_verified",
				"u.is_superuser", "u.is_staff", "u.created_on", "u.last_login", "u.is_active",
				"u.is_superuser", "u.is_staff", "u.created_on", "u.last_login",
				"u.is_locked", "u.lock_reason", "o.slug").
			From("users u").
			LeftJoin("organizations o ON o.owner_id = u.id").


@@ 90,7 90,6 @@ func GetUser(ctx context.Context, uid any, markAuth bool) (*User, error) {
				&staff,
				&user.CreatedOn,
				&user.LastLogin,
				&user.IsActive,
				&user.IsLocked,
				&user.LockReason,
				&user.OrgSlug,


@@ 112,6 111,7 @@ func GetUser(ctx context.Context, uid any, markAuth bool) (*User, error) {
	user.SetSuperUser(superuser)
	user.SetStaff(staff)
	user.SetAuthenticated(markAuth)
	user.IsEmailVerified = user.IsVerified() // hack
	return user, nil
}



@@ 127,7 127,7 @@ func GetUsers(ctx context.Context, opts *database.FilterOptions) ([]*User, error
		rows, err := q.
			Columns("u.id", "u.full_name", "u.password", "u.email", "u.settings",
				"u.is_verified", "u.is_superuser", "u.is_staff", "u.created_on",
				"u.last_login", "u.is_active", "u.is_locked", "u.lock_reason", "o.slug").
				"u.last_login", "u.is_locked", "u.lock_reason", "o.slug").
			From("users u").
			LeftJoin("organizations o ON o.owner_id = u.id").
			LeftJoin("organization_users ou ON u.id = ou.user_id").


@@ 149,7 149,7 @@ func GetUsers(ctx context.Context, opts *database.FilterOptions) ([]*User, error
				verified, superuser, staff bool
			)
			if err = rows.Scan(&u.ID, &u.Name, &u.Password, &u.Email, &u.Settings,
				&verified, &superuser, &staff, &u.CreatedOn, &u.LastLogin, &u.IsActive,
				&verified, &superuser, &staff, &u.CreatedOn, &u.LastLogin,
				&u.IsLocked, &u.LockReason, &u.OrgSlug,
			); err != nil {
				return err


@@ 157,6 157,7 @@ func GetUsers(ctx context.Context, opts *database.FilterOptions) ([]*User, error
			u.SetVerified(verified)
			u.SetSuperUser(superuser)
			u.SetStaff(staff)
			u.IsEmailVerified = u.IsVerified() // hack
			err = u.ToLocalTZ(tz)
			if err != nil {
				return err


@@ 198,7 199,6 @@ func (u *User) Store(ctx context.Context) error {
				Set("is_verified", u.IsVerified()).
				Set("is_superuser", u.IsSuperUser()).
				Set("is_staff", u.IsStaff()).
				Set("is_active", u.IsActive).
				Set("is_locked", u.IsLocked).
				Set("lock_reason", u.LockReason).
				Where("id = ?", u.GetID()).

M templates/admin_dashboard.html => templates/admin_dashboard.html +14 -2
@@ 22,7 22,8 @@
        <th>{{.pd.Data.name}}</th>
        <th>Email</th>
        <th>{{.pd.Data.created_on}}</th>
        <th class="text-center">{{.pd.Data.is_active}}</th>
        <th class="text-center">{{.pd.Data.is_verified}}</th>
        <th class="text-center">{{.pd.Data.is_locked}}</th>
      </tr>
    </thead>
    <tbody>


@@ 34,7 35,18 @@
              <td>{{.Email}}</td>
              <td>{{formatDate .CreatedOn}}</td>
              <td class="text-center">
                    {{if .IsActive}}
                    {{if .IsEmailVerified}}
                        <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">
                          <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
                        </svg>
                    {{else}}
                        <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-error">
                          <path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
                        </svg>
                    {{end}}
              </td>
              <td class="text-center">
                    {{if .IsLocked}}
                        <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">
                          <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
                        </svg>

M templates/admin_user_detail.html => templates/admin_user_detail.html +18 -2
@@ 29,10 29,10 @@
        <td>{{formatDate .user.CreatedOn}}</td>
      </tr>
      <tr>
        <th>{{.pd.Data.is_active}}</th>
        <th>{{.pd.Data.is_verified}}</th>
        <td>
          <p class="is-vertical-align">
            {{if .user.IsActive}}
            {{if .user.IsEmailVerified}}
            <svg version="1.1" class="has-solid " viewBox="0 0 36 36" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" focusable="false" role="img" width="22" height="22" fill="#008e00"><path class="clr-i-outline clr-i-outline-path-1" d="M18,6A12,12,0,1,0,30,18,12,12,0,0,0,18,6Zm0,22A10,10,0,1,1,28,18,10,10,0,0,1,18,28Z"/><path class="clr-i-outline clr-i-outline-path-2" d="M16.34,23.74l-5-5a1,1,0,0,1,1.41-1.41l3.59,3.59,6.78-6.78a1,1,0,0,1,1.41,1.41Z"/><path class="clr-i-solid clr-i-solid-path-1" d="M30,18A12,12,0,1,1,18,6,12,12,0,0,1,30,18Zm-4.77-2.16a1.4,1.4,0,0,0-2-2l-6.77,6.77L13,17.16a1.4,1.4,0,0,0-2,2l5.45,5.45Z" style="display:none"/></svg>
            {{else}}
            <svg version="1.1" class="has-solid " viewBox="0 0 36 36" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" focusable="false" role="img" width="16" height="16" fill="#ff2600"><path class="clr-i-outline clr-i-outline-path-1" d="M19.61,18l4.86-4.86a1,1,0,0,0-1.41-1.41L18.2,16.54l-4.89-4.89a1,1,0,0,0-1.41,1.41L16.78,18,12,22.72a1,1,0,1,0,1.41,1.41l4.77-4.77,4.74,4.74a1,1,0,0,0,1.41-1.41Z"/><path class="clr-i-outline clr-i-outline-path-2" d="M18,34A16,16,0,1,1,34,18,16,16,0,0,1,18,34ZM18,4A14,14,0,1,0,32,18,14,14,0,0,0,18,4Z"/><path class="clr-i-solid clr-i-solid-path-1" d="M18,2A16,16,0,1,0,34,18,16,16,0,0,0,18,2Zm8,22.1a1.4,1.4,0,0,1-2,2l-6-6L12,26.12a1.4,1.4,0,1,1-2-2L16,18.08,9.83,11.86a1.4,1.4,0,1,1,2-2L18,16.1l6.17-6.17a1.4,1.4,0,1,1,2,2L20,18.08Z" style="display:none"/></svg>


@@ 40,6 40,22 @@
          </p>
        </td>
      </tr>
      <tr>
        <th>{{.pd.Data.is_locked}}</th>
        <td>
            {{if .user.IsLocked}}
            <svg version="1.1" class="has-solid " viewBox="0 0 36 36" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" focusable="false" role="img" width="22" height="22" fill="#008e00"><path class="clr-i-outline clr-i-outline-path-1" d="M18,6A12,12,0,1,0,30,18,12,12,0,0,0,18,6Zm0,22A10,10,0,1,1,28,18,10,10,0,0,1,18,28Z"/><path class="clr-i-outline clr-i-outline-path-2" d="M16.34,23.74l-5-5a1,1,0,0,1,1.41-1.41l3.59,3.59,6.78-6.78a1,1,0,0,1,1.41,1.41Z"/><path class="clr-i-solid clr-i-solid-path-1" d="M30,18A12,12,0,1,1,18,6,12,12,0,0,1,30,18Zm-4.77-2.16a1.4,1.4,0,0,0-2-2l-6.77,6.77L13,17.16a1.4,1.4,0,0,0-2,2l5.45,5.45Z" style="display:none"/></svg>
            {{else}}
            <svg version="1.1" class="has-solid " viewBox="0 0 36 36" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" focusable="false" role="img" width="16" height="16" fill="#ff2600"><path class="clr-i-outline clr-i-outline-path-1" d="M19.61,18l4.86-4.86a1,1,0,0,0-1.41-1.41L18.2,16.54l-4.89-4.89a1,1,0,0,0-1.41,1.41L16.78,18,12,22.72a1,1,0,1,0,1.41,1.41l4.77-4.77,4.74,4.74a1,1,0,0,0,1.41-1.41Z"/><path class="clr-i-outline clr-i-outline-path-2" d="M18,34A16,16,0,1,1,34,18,16,16,0,0,1,18,34ZM18,4A14,14,0,1,0,32,18,14,14,0,0,0,18,4Z"/><path class="clr-i-solid clr-i-solid-path-1" d="M18,2A16,16,0,1,0,34,18,16,16,0,0,0,18,2Zm8,22.1a1.4,1.4,0,0,1-2,2l-6-6L12,26.12a1.4,1.4,0,1,1-2-2L16,18.08,9.83,11.86a1.4,1.4,0,1,1,2-2L18,16.1l6.17-6.17a1.4,1.4,0,1,1,2,2L20,18.08Z" style="display:none"/></svg>
            {{end}}
	</td>
      </tr>
      {{if .user.IsLocked}}
      <tr>
        <th>{{.pd.Data.lock_reason}}</th>
        <td>{{.user.LockReason}}</td>
      </tr>
      {{ end }}
    </tbody>
  </table>
  <h4>{{.pd.Data.organizations}}</h4>

M templates/admin_user_edit.html => templates/admin_user_edit.html +10 -10
@@ 28,16 28,16 @@
      {{ end }}
    </div>
    <div>
      <label for="is_active">{{.pd.Data.is_active}}</label>
      <input type="checkbox" value="true" name="is_active" id="is_active"{{if .form.IsActive}} checked{{end}}>
      {{ with .errors.IsActive }}
      <p class="error">{{ . }}</p>
      {{ end }}
    </div>
     <label for="is_verified">{{.pd.Data.is_verified}}</label>
     <input type="checkbox" value="true" name="is_verified" id="is_verified"{{if .form.IsVerified}} checked{{end}}>
     {{ with .errors.IsVerified }}
     <p class="error">{{ . }}</p>
     {{ end }}
   </div>
   <footer class="is-right">
     <button form="user-edit-form" type="submit" class="button dark">{{.pd.Data.save}}</button>
     <a class="button error" href="{{reverse "admin:user_detail" .user.ID}}">{{.pd.Data.cancel}}</a>
   </footer>
  </form>
  <footer class="is-right">
    <button form="user-edit-form" type="submit" class="button dark">{{.pd.Data.save}}</button>
    <a class="button error" href="{{reverse "admin:user_detail" .user.ID}}">{{.pd.Data.cancel}}</a>
  </footer>
</section>
{{template "base_footer" .}}

M templates/admin_user_list.html => templates/admin_user_list.html +2 -2
@@ 24,7 24,7 @@
        <th class="text-center">{{.pd.Data.name}}</th>
        <th class="text-center">Email</th>
        <th class="text-center">{{.pd.Data.created_on}}</th>
        <th class="text-center">{{.pd.Data.is_active}}</th>
        <th class="text-center">{{.pd.Data.is_verified}}</th>
      </tr>
    </thead>
    <tbody>


@@ 35,7 35,7 @@
        <td class="text-center">{{.Email}}</td>
        <td class="text-center">{{formatDate .CreatedOn}}</td>
        <td class="text-center">
            {{if .IsActive}}
            {{if .IsEmailVerified}}
                <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">
                  <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
                </svg>