forked from mirrors/homebox
feat: user profiles (#32)
* add user profiles and theme selectors * lowercase buttons by default * basic layout * (wip) init token APIs * refactor server to support variable options * fix types * api refactor / registration tests * implement UI for url and join * remove console.logs * rename repository factory * fix upload size
This commit is contained in:
parent
1ca430af21
commit
79f7ad40cb
76 changed files with 5154 additions and 388 deletions
|
@ -0,0 +1,4 @@
|
|||
-- create "group_invitation_tokens" table
|
||||
CREATE TABLE `group_invitation_tokens` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `token` blob NOT NULL, `expires_at` datetime NOT NULL, `uses` integer NOT NULL DEFAULT 0, `group_invitation_tokens` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `group_invitation_tokens_groups_invitation_tokens` FOREIGN KEY (`group_invitation_tokens`) REFERENCES `groups` (`id`) ON DELETE CASCADE);
|
||||
-- create index "group_invitation_tokens_token_key" to table: "group_invitation_tokens"
|
||||
CREATE UNIQUE INDEX `group_invitation_tokens_token_key` ON `group_invitation_tokens` (`token`);
|
|
@ -1,2 +1,3 @@
|
|||
h1:ihsTwGsfNb8b/1qt+jw0OPKM8I/Bcw1J3Ise0ZFu5co=
|
||||
h1:JE1IHs4N6SqydqXavjqcO40KuPMZ4uA9q9eQmqeYI/o=
|
||||
20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q=
|
||||
20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw=
|
||||
|
|
|
@ -19,7 +19,7 @@ var (
|
|||
tClient *ent.Client
|
||||
tRepos *AllRepos
|
||||
tUser UserOut
|
||||
tGroup *ent.Group
|
||||
tGroup Group
|
||||
)
|
||||
|
||||
func bootstrap() {
|
||||
|
@ -28,7 +28,7 @@ func bootstrap() {
|
|||
ctx = context.Background()
|
||||
)
|
||||
|
||||
tGroup, err = tRepos.Groups.Create(ctx, "test-group")
|
||||
tGroup, err = tRepos.Groups.GroupCreate(ctx, "test-group")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ func TestMain(m *testing.M) {
|
|||
}
|
||||
|
||||
tClient = client
|
||||
tRepos = EntAllRepos(tClient, os.TempDir())
|
||||
tRepos = New(tClient, os.TempDir())
|
||||
defer client.Close()
|
||||
|
||||
bootstrap()
|
||||
|
|
|
@ -2,21 +2,112 @@ package repo
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/ent"
|
||||
"github.com/hay-kot/homebox/backend/ent/groupinvitationtoken"
|
||||
)
|
||||
|
||||
type GroupRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
func (r *GroupRepository) Create(ctx context.Context, name string) (*ent.Group, error) {
|
||||
return r.db.Group.Create().
|
||||
SetName(name).
|
||||
Save(ctx)
|
||||
type (
|
||||
Group struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Currency string
|
||||
}
|
||||
|
||||
GroupInvitationCreate struct {
|
||||
Token []byte `json:"-"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
Uses int `json:"uses"`
|
||||
}
|
||||
|
||||
GroupInvitation struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
Uses int `json:"uses"`
|
||||
Group Group `json:"group"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
mapToGroupErr = mapTErrFunc(mapToGroup)
|
||||
)
|
||||
|
||||
func mapToGroup(g *ent.Group) Group {
|
||||
return Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
Currency: g.Currency.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GetOneId(ctx context.Context, id uuid.UUID) (*ent.Group, error) {
|
||||
return r.db.Group.Get(ctx, id)
|
||||
var (
|
||||
mapToGroupInvitationErr = mapTErrFunc(mapToGroupInvitation)
|
||||
)
|
||||
|
||||
func mapToGroupInvitation(g *ent.GroupInvitationToken) GroupInvitation {
|
||||
return GroupInvitation{
|
||||
ID: g.ID,
|
||||
ExpiresAt: g.ExpiresAt,
|
||||
Uses: g.Uses,
|
||||
Group: mapToGroup(g.Edges.Group),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group, error) {
|
||||
return mapToGroupErr(r.db.Group.Create().
|
||||
SetName(name).
|
||||
Save(ctx))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GroupByID(ctx context.Context, id uuid.UUID) (Group, error) {
|
||||
return mapToGroupErr(r.db.Group.Get(ctx, id))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) InvitationGet(ctx context.Context, token []byte) (GroupInvitation, error) {
|
||||
return mapToGroupInvitationErr(r.db.GroupInvitationToken.Query().
|
||||
Where(groupinvitationtoken.Token(token)).
|
||||
WithGroup().
|
||||
Only(ctx))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) InvitationCreate(ctx context.Context, groupID uuid.UUID, invite GroupInvitationCreate) (GroupInvitation, error) {
|
||||
entity, err := r.db.GroupInvitationToken.Create().
|
||||
SetGroupID(groupID).
|
||||
SetToken(invite.Token).
|
||||
SetExpiresAt(invite.ExpiresAt).
|
||||
SetUses(invite.Uses).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return GroupInvitation{}, err
|
||||
}
|
||||
|
||||
return r.InvitationGet(ctx, entity.Token)
|
||||
}
|
||||
|
||||
func (r *GroupRepository) InvitationUpdate(ctx context.Context, id uuid.UUID, uses int) error {
|
||||
_, err := r.db.GroupInvitationToken.UpdateOneID(id).SetUses(uses).Save(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// InvitationPurge removes all expired invitations or those that have been used up.
|
||||
// It returns the number of deleted invitations.
|
||||
func (r *GroupRepository) InvitationPurge(ctx context.Context) (amount int, err error) {
|
||||
q := r.db.GroupInvitationToken.Delete()
|
||||
q.Where(groupinvitationtoken.Or(
|
||||
groupinvitationtoken.ExpiresAtLT(time.Now()),
|
||||
groupinvitationtoken.UsesLTE(0),
|
||||
))
|
||||
|
||||
return q.Exec(ctx)
|
||||
}
|
||||
|
|
|
@ -8,13 +8,13 @@ import (
|
|||
)
|
||||
|
||||
func Test_Group_Create(t *testing.T) {
|
||||
g, err := tRepos.Groups.Create(context.Background(), "test")
|
||||
g, err := tRepos.Groups.GroupCreate(context.Background(), "test")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", g.Name)
|
||||
|
||||
// Get by ID
|
||||
foundGroup, err := tRepos.Groups.GetOneId(context.Background(), g.ID)
|
||||
foundGroup, err := tRepos.Groups.GroupByID(context.Background(), g.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, g.ID, foundGroup.ID)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ type AllRepos struct {
|
|||
Attachments *AttachmentRepo
|
||||
}
|
||||
|
||||
func EntAllRepos(db *ent.Client, root string) *AllRepos {
|
||||
func New(db *ent.Client, root string) *AllRepos {
|
||||
return &AllRepos{
|
||||
Users: &UserRepository{db},
|
||||
AuthTokens: &TokenRepository{db},
|
||||
|
|
|
@ -21,7 +21,7 @@ var (
|
|||
tClient *ent.Client
|
||||
tRepos *repo.AllRepos
|
||||
tUser repo.UserOut
|
||||
tGroup *ent.Group
|
||||
tGroup repo.Group
|
||||
tSvc *AllServices
|
||||
)
|
||||
|
||||
|
@ -31,7 +31,7 @@ func bootstrap() {
|
|||
ctx = context.Background()
|
||||
)
|
||||
|
||||
tGroup, err = tRepos.Groups.Create(ctx, "test-group")
|
||||
tGroup, err = tRepos.Groups.GroupCreate(ctx, "test-group")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ func TestMain(m *testing.M) {
|
|||
}
|
||||
|
||||
tClient = client
|
||||
tRepos = repo.EntAllRepos(tClient, os.TempDir()+"/homebox")
|
||||
tRepos = repo.New(tClient, os.TempDir()+"/homebox")
|
||||
tSvc = NewServices(tRepos)
|
||||
defer client.Close()
|
||||
|
||||
|
|
|
@ -24,20 +24,16 @@ type UserService struct {
|
|||
|
||||
type (
|
||||
UserRegistration struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
GroupName string `json:"groupName"`
|
||||
GroupToken string `json:"token"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
GroupName string `json:"groupName"`
|
||||
}
|
||||
UserAuthTokenDetail struct {
|
||||
Raw string `json:"raw"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
||||
UserAuthTokenCreate struct {
|
||||
TokenHash []byte `json:"token"`
|
||||
UserID uuid.UUID `json:"userId"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
||||
LoginForm struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
|
@ -51,11 +47,28 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
|
|||
Str("name", data.Name).
|
||||
Str("email", data.Email).
|
||||
Str("groupName", data.GroupName).
|
||||
Str("groupToken", data.GroupToken).
|
||||
Msg("Registering new user")
|
||||
|
||||
group, err := svc.repos.Groups.Create(ctx, data.GroupName)
|
||||
if err != nil {
|
||||
return repo.UserOut{}, err
|
||||
var (
|
||||
err error
|
||||
group repo.Group
|
||||
token repo.GroupInvitation
|
||||
)
|
||||
|
||||
if data.GroupToken == "" {
|
||||
group, err = svc.repos.Groups.GroupCreate(ctx, data.GroupName)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to create group")
|
||||
return repo.UserOut{}, err
|
||||
}
|
||||
} else {
|
||||
token, err = svc.repos.Groups.InvitationGet(ctx, hasher.HashToken(data.GroupToken))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get invitation token")
|
||||
return repo.UserOut{}, err
|
||||
}
|
||||
group = token.Group
|
||||
}
|
||||
|
||||
hashed, _ := hasher.HashPassword(data.Password)
|
||||
|
@ -86,6 +99,15 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
|
|||
}
|
||||
}
|
||||
|
||||
// Decrement the invitation token if it was used
|
||||
if token.ID != uuid.Nil {
|
||||
err = svc.repos.Groups.InvitationUpdate(ctx, token.ID, token.Uses-1)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to update invitation token")
|
||||
return repo.UserOut{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return usr, nil
|
||||
}
|
||||
|
||||
|
@ -155,3 +177,18 @@ func (svc *UserService) RenewToken(ctx context.Context, token string) (UserAuthT
|
|||
func (svc *UserService) DeleteSelf(ctx context.Context, ID uuid.UUID) error {
|
||||
return svc.repos.Users.Delete(ctx, ID)
|
||||
}
|
||||
|
||||
func (svc *UserService) NewInvitation(ctx Context, uses int, expiresAt time.Time) (string, error) {
|
||||
token := hasher.GenerateToken()
|
||||
|
||||
_, err := svc.repos.Groups.InvitationCreate(ctx, ctx.GID, repo.GroupInvitationCreate{
|
||||
Token: token.Hash,
|
||||
Uses: uses,
|
||||
ExpiresAt: expiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.Raw, nil
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue