From 711661d661677f2ad905f5a54fe085eaa69251c8 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 9 Oct 2022 12:37:58 -0500 Subject: [PATCH] add roles, activation, superuser fields on user --- backend/app/api/docs/docs.go | 3 + backend/app/api/docs/swagger.json | 3 + backend/app/api/docs/swagger.yaml | 2 + backend/ent/migrate/schema.go | 5 +- backend/ent/mutation.go | 188 +++++++++++++++++- backend/ent/runtime.go | 4 + backend/ent/schema/user.go | 7 + backend/ent/user.go | 39 +++- backend/ent/user/user.go | 38 ++++ backend/ent/user/where.go | 142 +++++++++++++ backend/ent/user_create.go | 85 ++++++++ backend/ent/user_update.go | 160 +++++++++++++++ .../20221009173029_add_user_roles.sql | 14 ++ .../internal/migrations/migrations/atlas.sum | 3 +- backend/internal/repo/repo_users.go | 9 + backend/internal/services/service_user.go | 14 +- frontend/lib/api/types/data-contracts.ts | 1 + 17 files changed, 705 insertions(+), 12 deletions(-) create mode 100644 backend/internal/migrations/migrations/20221009173029_add_user_roles.sql diff --git a/backend/app/api/docs/docs.go b/backend/app/api/docs/docs.go index 32af2e3..1eecf5e 100644 --- a/backend/app/api/docs/docs.go +++ b/backend/app/api/docs/docs.go @@ -1503,6 +1503,9 @@ const docTemplate = `{ "id": { "type": "string" }, + "isOwner": { + "type": "boolean" + }, "isSuperuser": { "type": "boolean" }, diff --git a/backend/app/api/docs/swagger.json b/backend/app/api/docs/swagger.json index 831066f..81fb334 100644 --- a/backend/app/api/docs/swagger.json +++ b/backend/app/api/docs/swagger.json @@ -1495,6 +1495,9 @@ "id": { "type": "string" }, + "isOwner": { + "type": "boolean" + }, "isSuperuser": { "type": "boolean" }, diff --git a/backend/app/api/docs/swagger.yaml b/backend/app/api/docs/swagger.yaml index e52d258..7172c12 100644 --- a/backend/app/api/docs/swagger.yaml +++ b/backend/app/api/docs/swagger.yaml @@ -285,6 +285,8 @@ definitions: type: string id: type: string + isOwner: + type: boolean isSuperuser: type: boolean name: diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index f71e4e0..d849a68 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -312,6 +312,9 @@ var ( {Name: "email", Type: field.TypeString, Unique: true, Size: 255}, {Name: "password", Type: field.TypeString, Size: 255}, {Name: "is_superuser", Type: field.TypeBool, Default: false}, + {Name: "role", Type: field.TypeEnum, Enums: []string{"user", "owner"}, Default: "user"}, + {Name: "superuser", Type: field.TypeBool, Default: false}, + {Name: "activated_on", Type: field.TypeTime, Nullable: true}, {Name: "group_users", Type: field.TypeUUID}, } // UsersTable holds the schema information for the "users" table. @@ -322,7 +325,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "users_groups_users", - Columns: []*schema.Column{UsersColumns[7]}, + Columns: []*schema.Column{UsersColumns[10]}, RefColumns: []*schema.Column{GroupsColumns[0]}, OnDelete: schema.Cascade, }, diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index fff3519..9155ee6 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -8548,6 +8548,9 @@ type UserMutation struct { email *string password *string is_superuser *bool + role *user.Role + superuser *bool + activated_on *time.Time clearedFields map[string]struct{} group *uuid.UUID clearedgroup bool @@ -8879,6 +8882,127 @@ func (m *UserMutation) ResetIsSuperuser() { m.is_superuser = nil } +// SetRole sets the "role" field. +func (m *UserMutation) SetRole(u user.Role) { + m.role = &u +} + +// Role returns the value of the "role" field in the mutation. +func (m *UserMutation) Role() (r user.Role, exists bool) { + v := m.role + if v == nil { + return + } + return *v, true +} + +// OldRole returns the old "role" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldRole(ctx context.Context) (v user.Role, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRole is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRole requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRole: %w", err) + } + return oldValue.Role, nil +} + +// ResetRole resets all changes to the "role" field. +func (m *UserMutation) ResetRole() { + m.role = nil +} + +// SetSuperuser sets the "superuser" field. +func (m *UserMutation) SetSuperuser(b bool) { + m.superuser = &b +} + +// Superuser returns the value of the "superuser" field in the mutation. +func (m *UserMutation) Superuser() (r bool, exists bool) { + v := m.superuser + if v == nil { + return + } + return *v, true +} + +// OldSuperuser returns the old "superuser" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldSuperuser(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSuperuser is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSuperuser requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSuperuser: %w", err) + } + return oldValue.Superuser, nil +} + +// ResetSuperuser resets all changes to the "superuser" field. +func (m *UserMutation) ResetSuperuser() { + m.superuser = nil +} + +// SetActivatedOn sets the "activated_on" field. +func (m *UserMutation) SetActivatedOn(t time.Time) { + m.activated_on = &t +} + +// ActivatedOn returns the value of the "activated_on" field in the mutation. +func (m *UserMutation) ActivatedOn() (r time.Time, exists bool) { + v := m.activated_on + if v == nil { + return + } + return *v, true +} + +// OldActivatedOn returns the old "activated_on" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldActivatedOn(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldActivatedOn is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldActivatedOn requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldActivatedOn: %w", err) + } + return oldValue.ActivatedOn, nil +} + +// ClearActivatedOn clears the value of the "activated_on" field. +func (m *UserMutation) ClearActivatedOn() { + m.activated_on = nil + m.clearedFields[user.FieldActivatedOn] = struct{}{} +} + +// ActivatedOnCleared returns if the "activated_on" field was cleared in this mutation. +func (m *UserMutation) ActivatedOnCleared() bool { + _, ok := m.clearedFields[user.FieldActivatedOn] + return ok +} + +// ResetActivatedOn resets all changes to the "activated_on" field. +func (m *UserMutation) ResetActivatedOn() { + m.activated_on = nil + delete(m.clearedFields, user.FieldActivatedOn) +} + // SetGroupID sets the "group" edge to the Group entity by id. func (m *UserMutation) SetGroupID(id uuid.UUID) { m.group = &id @@ -8991,7 +9115,7 @@ func (m *UserMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *UserMutation) Fields() []string { - fields := make([]string, 0, 6) + fields := make([]string, 0, 9) if m.created_at != nil { fields = append(fields, user.FieldCreatedAt) } @@ -9010,6 +9134,15 @@ func (m *UserMutation) Fields() []string { if m.is_superuser != nil { fields = append(fields, user.FieldIsSuperuser) } + if m.role != nil { + fields = append(fields, user.FieldRole) + } + if m.superuser != nil { + fields = append(fields, user.FieldSuperuser) + } + if m.activated_on != nil { + fields = append(fields, user.FieldActivatedOn) + } return fields } @@ -9030,6 +9163,12 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) { return m.Password() case user.FieldIsSuperuser: return m.IsSuperuser() + case user.FieldRole: + return m.Role() + case user.FieldSuperuser: + return m.Superuser() + case user.FieldActivatedOn: + return m.ActivatedOn() } return nil, false } @@ -9051,6 +9190,12 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er return m.OldPassword(ctx) case user.FieldIsSuperuser: return m.OldIsSuperuser(ctx) + case user.FieldRole: + return m.OldRole(ctx) + case user.FieldSuperuser: + return m.OldSuperuser(ctx) + case user.FieldActivatedOn: + return m.OldActivatedOn(ctx) } return nil, fmt.Errorf("unknown User field %s", name) } @@ -9102,6 +9247,27 @@ func (m *UserMutation) SetField(name string, value ent.Value) error { } m.SetIsSuperuser(v) return nil + case user.FieldRole: + v, ok := value.(user.Role) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRole(v) + return nil + case user.FieldSuperuser: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSuperuser(v) + return nil + case user.FieldActivatedOn: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetActivatedOn(v) + return nil } return fmt.Errorf("unknown User field %s", name) } @@ -9131,7 +9297,11 @@ func (m *UserMutation) AddField(name string, value ent.Value) error { // ClearedFields returns all nullable fields that were cleared during this // mutation. func (m *UserMutation) ClearedFields() []string { - return nil + var fields []string + if m.FieldCleared(user.FieldActivatedOn) { + fields = append(fields, user.FieldActivatedOn) + } + return fields } // FieldCleared returns a boolean indicating if a field with the given name was @@ -9144,6 +9314,11 @@ func (m *UserMutation) FieldCleared(name string) bool { // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. func (m *UserMutation) ClearField(name string) error { + switch name { + case user.FieldActivatedOn: + m.ClearActivatedOn() + return nil + } return fmt.Errorf("unknown User nullable field %s", name) } @@ -9169,6 +9344,15 @@ func (m *UserMutation) ResetField(name string) error { case user.FieldIsSuperuser: m.ResetIsSuperuser() return nil + case user.FieldRole: + m.ResetRole() + return nil + case user.FieldSuperuser: + m.ResetSuperuser() + return nil + case user.FieldActivatedOn: + m.ResetActivatedOn() + return nil } return fmt.Errorf("unknown User field %s", name) } diff --git a/backend/ent/runtime.go b/backend/ent/runtime.go index 043b1fc..bfa73d1 100644 --- a/backend/ent/runtime.go +++ b/backend/ent/runtime.go @@ -525,6 +525,10 @@ func init() { userDescIsSuperuser := userFields[3].Descriptor() // user.DefaultIsSuperuser holds the default value on creation for the is_superuser field. user.DefaultIsSuperuser = userDescIsSuperuser.Default.(bool) + // userDescSuperuser is the schema descriptor for superuser field. + userDescSuperuser := userFields[5].Descriptor() + // user.DefaultSuperuser holds the default value on creation for the superuser field. + user.DefaultSuperuser = userDescSuperuser.Default.(bool) // userDescID is the schema descriptor for id field. userDescID := userMixinFields0[0].Descriptor() // user.DefaultID holds the default value on creation for the id field. diff --git a/backend/ent/schema/user.go b/backend/ent/schema/user.go index cc9188e..d522ddb 100644 --- a/backend/ent/schema/user.go +++ b/backend/ent/schema/user.go @@ -35,6 +35,13 @@ func (User) Fields() []ent.Field { Sensitive(), field.Bool("is_superuser"). Default(false), + field.Enum("role"). + Default("user"). + Values("user", "owner"), + field.Bool("superuser"). + Default(false), + field.Time("activated_on"). + Optional(), } } diff --git a/backend/ent/user.go b/backend/ent/user.go index ffe33a4..547e21f 100644 --- a/backend/ent/user.go +++ b/backend/ent/user.go @@ -30,6 +30,12 @@ type User struct { Password string `json:"-"` // IsSuperuser holds the value of the "is_superuser" field. IsSuperuser bool `json:"is_superuser,omitempty"` + // Role holds the value of the "role" field. + Role user.Role `json:"role,omitempty"` + // Superuser holds the value of the "superuser" field. + Superuser bool `json:"superuser,omitempty"` + // ActivatedOn holds the value of the "activated_on" field. + ActivatedOn time.Time `json:"activated_on,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the UserQuery when eager-loading is set. Edges UserEdges `json:"edges"` @@ -74,11 +80,11 @@ func (*User) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case user.FieldIsSuperuser: + case user.FieldIsSuperuser, user.FieldSuperuser: values[i] = new(sql.NullBool) - case user.FieldName, user.FieldEmail, user.FieldPassword: + case user.FieldName, user.FieldEmail, user.FieldPassword, user.FieldRole: values[i] = new(sql.NullString) - case user.FieldCreatedAt, user.FieldUpdatedAt: + case user.FieldCreatedAt, user.FieldUpdatedAt, user.FieldActivatedOn: values[i] = new(sql.NullTime) case user.FieldID: values[i] = new(uuid.UUID) @@ -141,6 +147,24 @@ func (u *User) assignValues(columns []string, values []any) error { } else if value.Valid { u.IsSuperuser = value.Bool } + case user.FieldRole: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field role", values[i]) + } else if value.Valid { + u.Role = user.Role(value.String) + } + case user.FieldSuperuser: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field superuser", values[i]) + } else if value.Valid { + u.Superuser = value.Bool + } + case user.FieldActivatedOn: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field activated_on", values[i]) + } else if value.Valid { + u.ActivatedOn = value.Time + } case user.ForeignKeys[0]: if value, ok := values[i].(*sql.NullScanner); !ok { return fmt.Errorf("unexpected type %T for field group_users", values[i]) @@ -202,6 +226,15 @@ func (u *User) String() string { builder.WriteString(", ") builder.WriteString("is_superuser=") builder.WriteString(fmt.Sprintf("%v", u.IsSuperuser)) + builder.WriteString(", ") + builder.WriteString("role=") + builder.WriteString(fmt.Sprintf("%v", u.Role)) + builder.WriteString(", ") + builder.WriteString("superuser=") + builder.WriteString(fmt.Sprintf("%v", u.Superuser)) + builder.WriteString(", ") + builder.WriteString("activated_on=") + builder.WriteString(u.ActivatedOn.Format(time.ANSIC)) builder.WriteByte(')') return builder.String() } diff --git a/backend/ent/user/user.go b/backend/ent/user/user.go index fccdcba..c8b61c2 100644 --- a/backend/ent/user/user.go +++ b/backend/ent/user/user.go @@ -3,6 +3,7 @@ package user import ( + "fmt" "time" "github.com/google/uuid" @@ -25,6 +26,12 @@ const ( FieldPassword = "password" // FieldIsSuperuser holds the string denoting the is_superuser field in the database. FieldIsSuperuser = "is_superuser" + // FieldRole holds the string denoting the role field in the database. + FieldRole = "role" + // FieldSuperuser holds the string denoting the superuser field in the database. + FieldSuperuser = "superuser" + // FieldActivatedOn holds the string denoting the activated_on field in the database. + FieldActivatedOn = "activated_on" // EdgeGroup holds the string denoting the group edge name in mutations. EdgeGroup = "group" // EdgeAuthTokens holds the string denoting the auth_tokens edge name in mutations. @@ -56,6 +63,9 @@ var Columns = []string{ FieldEmail, FieldPassword, FieldIsSuperuser, + FieldRole, + FieldSuperuser, + FieldActivatedOn, } // ForeignKeys holds the SQL foreign-keys that are owned by the "users" @@ -94,6 +104,34 @@ var ( PasswordValidator func(string) error // DefaultIsSuperuser holds the default value on creation for the "is_superuser" field. DefaultIsSuperuser bool + // DefaultSuperuser holds the default value on creation for the "superuser" field. + DefaultSuperuser bool // DefaultID holds the default value on creation for the "id" field. DefaultID func() uuid.UUID ) + +// Role defines the type for the "role" enum field. +type Role string + +// RoleUser is the default value of the Role enum. +const DefaultRole = RoleUser + +// Role values. +const ( + RoleUser Role = "user" + RoleOwner Role = "owner" +) + +func (r Role) String() string { + return string(r) +} + +// RoleValidator is a validator for the "role" field enum values. It is called by the builders before save. +func RoleValidator(r Role) error { + switch r { + case RoleUser, RoleOwner: + return nil + default: + return fmt.Errorf("user: invalid enum value for role field: %q", r) + } +} diff --git a/backend/ent/user/where.go b/backend/ent/user/where.go index 7288a41..df4adbb 100644 --- a/backend/ent/user/where.go +++ b/backend/ent/user/where.go @@ -124,6 +124,20 @@ func IsSuperuser(v bool) predicate.User { }) } +// Superuser applies equality check predicate on the "superuser" field. It's identical to SuperuserEQ. +func Superuser(v bool) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldSuperuser), v)) + }) +} + +// ActivatedOn applies equality check predicate on the "activated_on" field. It's identical to ActivatedOnEQ. +func ActivatedOn(v time.Time) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldActivatedOn), v)) + }) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.User { return predicate.User(func(s *sql.Selector) { @@ -563,6 +577,134 @@ func IsSuperuserNEQ(v bool) predicate.User { }) } +// RoleEQ applies the EQ predicate on the "role" field. +func RoleEQ(v Role) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldRole), v)) + }) +} + +// RoleNEQ applies the NEQ predicate on the "role" field. +func RoleNEQ(v Role) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldRole), v)) + }) +} + +// RoleIn applies the In predicate on the "role" field. +func RoleIn(vs ...Role) predicate.User { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.User(func(s *sql.Selector) { + s.Where(sql.In(s.C(FieldRole), v...)) + }) +} + +// RoleNotIn applies the NotIn predicate on the "role" field. +func RoleNotIn(vs ...Role) predicate.User { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.User(func(s *sql.Selector) { + s.Where(sql.NotIn(s.C(FieldRole), v...)) + }) +} + +// SuperuserEQ applies the EQ predicate on the "superuser" field. +func SuperuserEQ(v bool) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldSuperuser), v)) + }) +} + +// SuperuserNEQ applies the NEQ predicate on the "superuser" field. +func SuperuserNEQ(v bool) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldSuperuser), v)) + }) +} + +// ActivatedOnEQ applies the EQ predicate on the "activated_on" field. +func ActivatedOnEQ(v time.Time) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldActivatedOn), v)) + }) +} + +// ActivatedOnNEQ applies the NEQ predicate on the "activated_on" field. +func ActivatedOnNEQ(v time.Time) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldActivatedOn), v)) + }) +} + +// ActivatedOnIn applies the In predicate on the "activated_on" field. +func ActivatedOnIn(vs ...time.Time) predicate.User { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.User(func(s *sql.Selector) { + s.Where(sql.In(s.C(FieldActivatedOn), v...)) + }) +} + +// ActivatedOnNotIn applies the NotIn predicate on the "activated_on" field. +func ActivatedOnNotIn(vs ...time.Time) predicate.User { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.User(func(s *sql.Selector) { + s.Where(sql.NotIn(s.C(FieldActivatedOn), v...)) + }) +} + +// ActivatedOnGT applies the GT predicate on the "activated_on" field. +func ActivatedOnGT(v time.Time) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.GT(s.C(FieldActivatedOn), v)) + }) +} + +// ActivatedOnGTE applies the GTE predicate on the "activated_on" field. +func ActivatedOnGTE(v time.Time) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.GTE(s.C(FieldActivatedOn), v)) + }) +} + +// ActivatedOnLT applies the LT predicate on the "activated_on" field. +func ActivatedOnLT(v time.Time) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.LT(s.C(FieldActivatedOn), v)) + }) +} + +// ActivatedOnLTE applies the LTE predicate on the "activated_on" field. +func ActivatedOnLTE(v time.Time) predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.LTE(s.C(FieldActivatedOn), v)) + }) +} + +// ActivatedOnIsNil applies the IsNil predicate on the "activated_on" field. +func ActivatedOnIsNil() predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.IsNull(s.C(FieldActivatedOn))) + }) +} + +// ActivatedOnNotNil applies the NotNil predicate on the "activated_on" field. +func ActivatedOnNotNil() predicate.User { + return predicate.User(func(s *sql.Selector) { + s.Where(sql.NotNull(s.C(FieldActivatedOn))) + }) +} + // HasGroup applies the HasEdge predicate on the "group" edge. func HasGroup() predicate.User { return predicate.User(func(s *sql.Selector) { diff --git a/backend/ent/user_create.go b/backend/ent/user_create.go index 51c8151..5835244 100644 --- a/backend/ent/user_create.go +++ b/backend/ent/user_create.go @@ -83,6 +83,48 @@ func (uc *UserCreate) SetNillableIsSuperuser(b *bool) *UserCreate { return uc } +// SetRole sets the "role" field. +func (uc *UserCreate) SetRole(u user.Role) *UserCreate { + uc.mutation.SetRole(u) + return uc +} + +// SetNillableRole sets the "role" field if the given value is not nil. +func (uc *UserCreate) SetNillableRole(u *user.Role) *UserCreate { + if u != nil { + uc.SetRole(*u) + } + return uc +} + +// SetSuperuser sets the "superuser" field. +func (uc *UserCreate) SetSuperuser(b bool) *UserCreate { + uc.mutation.SetSuperuser(b) + return uc +} + +// SetNillableSuperuser sets the "superuser" field if the given value is not nil. +func (uc *UserCreate) SetNillableSuperuser(b *bool) *UserCreate { + if b != nil { + uc.SetSuperuser(*b) + } + return uc +} + +// SetActivatedOn sets the "activated_on" field. +func (uc *UserCreate) SetActivatedOn(t time.Time) *UserCreate { + uc.mutation.SetActivatedOn(t) + return uc +} + +// SetNillableActivatedOn sets the "activated_on" field if the given value is not nil. +func (uc *UserCreate) SetNillableActivatedOn(t *time.Time) *UserCreate { + if t != nil { + uc.SetActivatedOn(*t) + } + return uc +} + // SetID sets the "id" field. func (uc *UserCreate) SetID(u uuid.UUID) *UserCreate { uc.mutation.SetID(u) @@ -212,6 +254,14 @@ func (uc *UserCreate) defaults() { v := user.DefaultIsSuperuser uc.mutation.SetIsSuperuser(v) } + if _, ok := uc.mutation.Role(); !ok { + v := user.DefaultRole + uc.mutation.SetRole(v) + } + if _, ok := uc.mutation.Superuser(); !ok { + v := user.DefaultSuperuser + uc.mutation.SetSuperuser(v) + } if _, ok := uc.mutation.ID(); !ok { v := user.DefaultID() uc.mutation.SetID(v) @@ -253,6 +303,17 @@ func (uc *UserCreate) check() error { if _, ok := uc.mutation.IsSuperuser(); !ok { return &ValidationError{Name: "is_superuser", err: errors.New(`ent: missing required field "User.is_superuser"`)} } + if _, ok := uc.mutation.Role(); !ok { + return &ValidationError{Name: "role", err: errors.New(`ent: missing required field "User.role"`)} + } + if v, ok := uc.mutation.Role(); ok { + if err := user.RoleValidator(v); err != nil { + return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)} + } + } + if _, ok := uc.mutation.Superuser(); !ok { + return &ValidationError{Name: "superuser", err: errors.New(`ent: missing required field "User.superuser"`)} + } if _, ok := uc.mutation.GroupID(); !ok { return &ValidationError{Name: "group", err: errors.New(`ent: missing required edge "User.group"`)} } @@ -340,6 +401,30 @@ func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { }) _node.IsSuperuser = value } + if value, ok := uc.mutation.Role(); ok { + _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ + Type: field.TypeEnum, + Value: value, + Column: user.FieldRole, + }) + _node.Role = value + } + if value, ok := uc.mutation.Superuser(); ok { + _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ + Type: field.TypeBool, + Value: value, + Column: user.FieldSuperuser, + }) + _node.Superuser = value + } + if value, ok := uc.mutation.ActivatedOn(); ok { + _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: user.FieldActivatedOn, + }) + _node.ActivatedOn = value + } if nodes := uc.mutation.GroupIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/backend/ent/user_update.go b/backend/ent/user_update.go index 7d9bbad..9316360 100644 --- a/backend/ent/user_update.go +++ b/backend/ent/user_update.go @@ -69,6 +69,54 @@ func (uu *UserUpdate) SetNillableIsSuperuser(b *bool) *UserUpdate { return uu } +// SetRole sets the "role" field. +func (uu *UserUpdate) SetRole(u user.Role) *UserUpdate { + uu.mutation.SetRole(u) + return uu +} + +// SetNillableRole sets the "role" field if the given value is not nil. +func (uu *UserUpdate) SetNillableRole(u *user.Role) *UserUpdate { + if u != nil { + uu.SetRole(*u) + } + return uu +} + +// SetSuperuser sets the "superuser" field. +func (uu *UserUpdate) SetSuperuser(b bool) *UserUpdate { + uu.mutation.SetSuperuser(b) + return uu +} + +// SetNillableSuperuser sets the "superuser" field if the given value is not nil. +func (uu *UserUpdate) SetNillableSuperuser(b *bool) *UserUpdate { + if b != nil { + uu.SetSuperuser(*b) + } + return uu +} + +// SetActivatedOn sets the "activated_on" field. +func (uu *UserUpdate) SetActivatedOn(t time.Time) *UserUpdate { + uu.mutation.SetActivatedOn(t) + return uu +} + +// SetNillableActivatedOn sets the "activated_on" field if the given value is not nil. +func (uu *UserUpdate) SetNillableActivatedOn(t *time.Time) *UserUpdate { + if t != nil { + uu.SetActivatedOn(*t) + } + return uu +} + +// ClearActivatedOn clears the value of the "activated_on" field. +func (uu *UserUpdate) ClearActivatedOn() *UserUpdate { + uu.mutation.ClearActivatedOn() + return uu +} + // SetGroupID sets the "group" edge to the Group entity by ID. func (uu *UserUpdate) SetGroupID(id uuid.UUID) *UserUpdate { uu.mutation.SetGroupID(id) @@ -213,6 +261,11 @@ func (uu *UserUpdate) check() error { return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "User.password": %w`, err)} } } + if v, ok := uu.mutation.Role(); ok { + if err := user.RoleValidator(v); err != nil { + return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)} + } + } if _, ok := uu.mutation.GroupID(); uu.mutation.GroupCleared() && !ok { return errors.New(`ent: clearing a required unique edge "User.group"`) } @@ -272,6 +325,33 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) { Column: user.FieldIsSuperuser, }) } + if value, ok := uu.mutation.Role(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeEnum, + Value: value, + Column: user.FieldRole, + }) + } + if value, ok := uu.mutation.Superuser(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeBool, + Value: value, + Column: user.FieldSuperuser, + }) + } + if value, ok := uu.mutation.ActivatedOn(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: user.FieldActivatedOn, + }) + } + if uu.mutation.ActivatedOnCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Column: user.FieldActivatedOn, + }) + } if uu.mutation.GroupCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -418,6 +498,54 @@ func (uuo *UserUpdateOne) SetNillableIsSuperuser(b *bool) *UserUpdateOne { return uuo } +// SetRole sets the "role" field. +func (uuo *UserUpdateOne) SetRole(u user.Role) *UserUpdateOne { + uuo.mutation.SetRole(u) + return uuo +} + +// SetNillableRole sets the "role" field if the given value is not nil. +func (uuo *UserUpdateOne) SetNillableRole(u *user.Role) *UserUpdateOne { + if u != nil { + uuo.SetRole(*u) + } + return uuo +} + +// SetSuperuser sets the "superuser" field. +func (uuo *UserUpdateOne) SetSuperuser(b bool) *UserUpdateOne { + uuo.mutation.SetSuperuser(b) + return uuo +} + +// SetNillableSuperuser sets the "superuser" field if the given value is not nil. +func (uuo *UserUpdateOne) SetNillableSuperuser(b *bool) *UserUpdateOne { + if b != nil { + uuo.SetSuperuser(*b) + } + return uuo +} + +// SetActivatedOn sets the "activated_on" field. +func (uuo *UserUpdateOne) SetActivatedOn(t time.Time) *UserUpdateOne { + uuo.mutation.SetActivatedOn(t) + return uuo +} + +// SetNillableActivatedOn sets the "activated_on" field if the given value is not nil. +func (uuo *UserUpdateOne) SetNillableActivatedOn(t *time.Time) *UserUpdateOne { + if t != nil { + uuo.SetActivatedOn(*t) + } + return uuo +} + +// ClearActivatedOn clears the value of the "activated_on" field. +func (uuo *UserUpdateOne) ClearActivatedOn() *UserUpdateOne { + uuo.mutation.ClearActivatedOn() + return uuo +} + // SetGroupID sets the "group" edge to the Group entity by ID. func (uuo *UserUpdateOne) SetGroupID(id uuid.UUID) *UserUpdateOne { uuo.mutation.SetGroupID(id) @@ -575,6 +703,11 @@ func (uuo *UserUpdateOne) check() error { return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "User.password": %w`, err)} } } + if v, ok := uuo.mutation.Role(); ok { + if err := user.RoleValidator(v); err != nil { + return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)} + } + } if _, ok := uuo.mutation.GroupID(); uuo.mutation.GroupCleared() && !ok { return errors.New(`ent: clearing a required unique edge "User.group"`) } @@ -651,6 +784,33 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) Column: user.FieldIsSuperuser, }) } + if value, ok := uuo.mutation.Role(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeEnum, + Value: value, + Column: user.FieldRole, + }) + } + if value, ok := uuo.mutation.Superuser(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeBool, + Value: value, + Column: user.FieldSuperuser, + }) + } + if value, ok := uuo.mutation.ActivatedOn(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: user.FieldActivatedOn, + }) + } + if uuo.mutation.ActivatedOnCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Column: user.FieldActivatedOn, + }) + } if uuo.mutation.GroupCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/backend/internal/migrations/migrations/20221009173029_add_user_roles.sql b/backend/internal/migrations/migrations/20221009173029_add_user_roles.sql new file mode 100644 index 0000000..95c6ab0 --- /dev/null +++ b/backend/internal/migrations/migrations/20221009173029_add_user_roles.sql @@ -0,0 +1,14 @@ +-- disable the enforcement of foreign-keys constraints +PRAGMA foreign_keys = off; +-- create "new_users" table +CREATE TABLE `new_users` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `email` text NOT NULL, `password` text NOT NULL, `is_superuser` bool NOT NULL DEFAULT false, `role` text NOT NULL DEFAULT 'user', `superuser` bool NOT NULL DEFAULT false, `activated_on` datetime NULL, `group_users` uuid NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `users_groups_users` FOREIGN KEY (`group_users`) REFERENCES `groups` (`id`) ON DELETE CASCADE); +-- copy rows from old table "users" to new temporary table "new_users" +INSERT INTO `new_users` (`id`, `created_at`, `updated_at`, `name`, `email`, `password`, `is_superuser`, `group_users`) SELECT `id`, `created_at`, `updated_at`, `name`, `email`, `password`, `is_superuser`, `group_users` FROM `users`; +-- drop "users" table after copying rows +DROP TABLE `users`; +-- rename temporary table "new_users" to "users" +ALTER TABLE `new_users` RENAME TO `users`; +-- create index "users_email_key" to table: "users" +CREATE UNIQUE INDEX `users_email_key` ON `users` (`email`); +-- enable back the enforcement of foreign-keys constraints +PRAGMA foreign_keys = on; diff --git a/backend/internal/migrations/migrations/atlas.sum b/backend/internal/migrations/migrations/atlas.sum index a5c2573..0bc29a5 100644 --- a/backend/internal/migrations/migrations/atlas.sum +++ b/backend/internal/migrations/migrations/atlas.sum @@ -1,3 +1,4 @@ -h1:JE1IHs4N6SqydqXavjqcO40KuPMZ4uA9q9eQmqeYI/o= +h1:nN8ZUHScToap+2yJKsb+AnKu8o2DubUoiKc9neQJ93M= 20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q= 20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw= +20221009173029_add_user_roles.sql h1:vWmzAfgEWQeGk0Vn70zfVPCcfEZth3E0JcvyKTjpYyU= diff --git a/backend/internal/repo/repo_users.go b/backend/internal/repo/repo_users.go index 9763e38..828c4c2 100644 --- a/backend/internal/repo/repo_users.go +++ b/backend/internal/repo/repo_users.go @@ -22,6 +22,7 @@ type ( Password string `json:"password"` IsSuperuser bool `json:"isSuperuser"` GroupID uuid.UUID `json:"groupID"` + IsOwner bool `json:"isOwner"` } UserUpdate struct { @@ -37,6 +38,7 @@ type ( GroupID uuid.UUID `json:"groupId"` GroupName string `json:"groupName"` PasswordHash string `json:"-"` + IsOwner bool `json:"isOwner"` } ) @@ -54,6 +56,7 @@ func mapUserOut(user *ent.User) UserOut { GroupID: user.Edges.Group.ID, GroupName: user.Edges.Group.Name, PasswordHash: user.Password, + IsOwner: user.Role == "owner", } } @@ -77,6 +80,11 @@ func (e *UserRepository) GetAll(ctx context.Context) ([]UserOut, error) { } func (e *UserRepository) Create(ctx context.Context, usr UserCreate) (UserOut, error) { + role := user.RoleUser + if usr.IsOwner { + role = user.RoleOwner + } + entUser, err := e.db.User. Create(). SetName(usr.Name). @@ -84,6 +92,7 @@ func (e *UserRepository) Create(ctx context.Context, usr UserCreate) (UserOut, e SetPassword(usr.Password). SetIsSuperuser(usr.IsSuperuser). SetGroupID(usr.GroupID). + SetRole(role). Save(ctx) if err != nil { return UserOut{}, err diff --git a/backend/internal/services/service_user.go b/backend/internal/services/service_user.go index e8bc00c..b678ab9 100644 --- a/backend/internal/services/service_user.go +++ b/backend/internal/services/service_user.go @@ -49,18 +49,21 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration) Msg("Registering new user") var ( - err error - group repo.Group - token repo.GroupInvitation + err error + group repo.Group + token repo.GroupInvitation + isOwner = false ) - if data.GroupToken == "" { + switch data.GroupToken { + case "": + isOwner = true group, err = svc.repos.Groups.GroupCreate(ctx, "Home") if err != nil { log.Err(err).Msg("Failed to create group") return repo.UserOut{}, err } - } else { + default: token, err = svc.repos.Groups.InvitationGet(ctx, hasher.HashToken(data.GroupToken)) if err != nil { log.Err(err).Msg("Failed to get invitation token") @@ -76,6 +79,7 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration) Password: hashed, IsSuperuser: false, GroupID: group.ID, + IsOwner: isOwner, } usr, err := svc.repos.Users.Create(ctx, usrCreate) diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 3111a85..ae726c4 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -192,6 +192,7 @@ export interface UserOut { groupId: string; groupName: string; id: string; + isOwner: boolean; isSuperuser: boolean; name: string; }