3
0

SAM types, handlers, services, repositories implemented

This commit is contained in:
Denis Arh
2018-07-17 15:24:35 +02:00
parent 152f089148
commit 70a5042db5
24 changed files with 1058 additions and 373 deletions
+2
View File
@@ -411,6 +411,8 @@
"fields": [
{ "type": "uint64", "name": "ID" },
{ "type": "string", "name": "Username" },
{ "type": "interface{}", "name": "Meta", "tag": "json:\"-\"" },
{ "type": "uint64", "name": "OrganisationID", "dbname": "rel_organisation" },
{ "type": "[]byte", "name": "Password", "tag": "json:\"-\"", "complex": true },
{ "type": "*time.Time", "name": "SuspendedAt", "tag": "json:\",omitempty\"", "complex": true },
{ "type": "*time.Time", "name": "DeletedAt", "tag": "json:\",omitempty\"", "complex": true }
+10
View File
@@ -13,6 +13,16 @@
"name": "Username",
"type": "string"
},
{
"name": "Meta",
"tag": "json:\"-\"",
"type": "interface{}"
},
{
"dbname": "rel_organisation",
"name": "OrganisationID",
"type": "uint64"
},
{
"complex": true,
"name": "Password",
+104
View File
@@ -0,0 +1,104 @@
package repository
import (
"context"
"github.com/crusttech/crust/sam/types"
"github.com/titpetric/factory"
"time"
)
const (
sqlChannelScope = "deleted_at IS NULL AND archived_at IS NULL"
ErrChannelNotFound = repositoryError("ChannelNotFound")
)
type (
channel struct{}
)
func Channel() channel {
return channel{}
}
func (r channel) FindById(ctx context.Context, id uint64) (*types.Channel, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
mod := &types.Channel{}
if err := db.Get(mod, "SELECT * FROM channels WHERE id = ? AND "+sqlChannelScope, id); err != nil {
return nil, ErrDatabaseError
} else if mod.ID == 0 {
return nil, ErrChannelNotFound
} else {
return mod, nil
}
}
func (r channel) Find(ctx context.Context, filter *types.ChannelFilter) ([]*types.Channel, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
var params = make([]interface{}, 0)
sql := "SELECT * FROM channels WHERE " + sqlChannelScope
if filter != nil {
if filter.Query != "" {
sql += "AND name LIKE ?"
params = append(params, filter.Query+"%")
}
}
sql += " ORDER BY name ASC"
rval := make([]*types.Channel, 0)
if err := db.Select(&rval, sql, params...); err != nil {
panic(err)
return nil, ErrDatabaseError
} else {
return rval, nil
}
}
func (r channel) Create(ctx context.Context, mod *types.Channel) (*types.Channel, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
mod.SetID(factory.Sonyflake.NextID())
if err := db.Insert("channels", mod); err != nil {
return nil, ErrDatabaseError
} else {
return mod, nil
}
}
func (r channel) Update(ctx context.Context, mod *types.Channel) (*types.Channel, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
if err := db.Replace("channels", mod); err != nil {
return nil, ErrDatabaseError
} else {
return mod, nil
}
}
func (r channel) Archive(ctx context.Context, id uint64) error {
return simpleUpdate(ctx, "channels", "archived_at", time.Now(), id)
}
func (r channel) Unarchive(ctx context.Context, id uint64) error {
return simpleUpdate(ctx, "channels", "archived_at", nil, id)
}
func (r channel) Delete(ctx context.Context, id uint64) error {
return simpleDelete(ctx, "channels", id)
}
+14
View File
@@ -0,0 +1,14 @@
package repository
type (
repositoryError string
)
const (
ErrDatabaseError = repositoryError("DatabaseError")
ErrNotImplemented = repositoryError("NotImplemented")
)
func (e repositoryError) Error() string {
return "crust.sam.repository." + string(e)
}
+37
View File
@@ -0,0 +1,37 @@
package repository
import (
"context"
"fmt"
"github.com/titpetric/factory"
)
func simpleUpdate(ctx context.Context, tableName, columnName string, value interface{}, id uint64) error {
db, err := factory.Database.Get()
if err != nil {
return ErrDatabaseError
}
sql := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", tableName, columnName)
if _, err := db.Exec(sql, value, id); err != nil {
return ErrDatabaseError
} else {
return nil
}
}
func simpleDelete(ctx context.Context, tableName string, id uint64) error {
db, err := factory.Database.Get()
if err != nil {
return ErrDatabaseError
}
sql := fmt.Sprintf("DELETE %s WHERE id = ?", tableName)
if _, err := db.Exec(sql, id); err != nil {
return ErrDatabaseError
} else {
return nil
}
}
+104
View File
@@ -0,0 +1,104 @@
package repository
import (
"context"
"github.com/crusttech/crust/sam/types"
"github.com/titpetric/factory"
"time"
)
const (
sqlOrganisationScope = "deleted_at IS NULL AND archived_at IS NULL"
ErrOrganisationNotFound = repositoryError("OrganisationNotFound")
)
type (
organisation struct{}
)
func Organisation() organisation {
return organisation{}
}
func (r organisation) FindById(ctx context.Context, id uint64) (*types.Organisation, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
mod := &types.Organisation{}
if err := db.Get(mod, "SELECT * FROM organisations WHERE id = ? AND "+sqlOrganisationScope, id); err != nil {
return nil, ErrDatabaseError
} else if mod.ID == 0 {
return nil, ErrOrganisationNotFound
} else {
return mod, nil
}
}
func (r organisation) Find(ctx context.Context, filter *types.OrganisationFilter) ([]*types.Organisation, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
var params = make([]interface{}, 0)
sql := "SELECT * FROM organisations WHERE " + sqlOrganisationScope
if filter != nil {
if filter.Query != "" {
sql += "AND ame LIKE ?"
params = append(params, filter.Query+"%")
}
}
sql += " ORDER BY name ASC"
rval := make([]*types.Organisation, 0)
if err := db.Select(&rval, sql, params...); err != nil {
panic(err)
return nil, ErrDatabaseError
} else {
return rval, nil
}
}
func (r organisation) Create(ctx context.Context, mod *types.Organisation) (*types.Organisation, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
mod.SetID(factory.Sonyflake.NextID())
if err := db.Insert("organisations", mod); err != nil {
return nil, ErrDatabaseError
} else {
return mod, nil
}
}
func (r organisation) Update(ctx context.Context, mod *types.Organisation) (*types.Organisation, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
if err := db.Replace("organisations", mod); err != nil {
return nil, ErrDatabaseError
} else {
return mod, nil
}
}
func (r organisation) Archive(ctx context.Context, id uint64) error {
return simpleUpdate(ctx, "organisations", "archived_at", time.Now(), id)
}
func (r organisation) Unarchive(ctx context.Context, id uint64) error {
return simpleUpdate(ctx, "organisations", "archived_at", nil, id)
}
func (r organisation) Delete(ctx context.Context, id uint64) error {
return simpleDelete(ctx, "organisations", id)
}
+112
View File
@@ -0,0 +1,112 @@
package repository
import (
"context"
"github.com/crusttech/crust/sam/types"
"github.com/titpetric/factory"
"time"
)
const (
sqlTeamScope = "deleted_at IS NULL AND archived_at IS NULL"
ErrTeamNotFound = repositoryError("TeamNotFound")
)
type (
team struct{}
)
func Team() team {
return team{}
}
func (r team) FindById(ctx context.Context, id uint64) (*types.Team, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
mod := &types.Team{}
if err := db.Get(mod, "SELECT * FROM teams WHERE id = ? AND "+sqlTeamScope, id); err != nil {
return nil, ErrDatabaseError
} else if mod.ID == 0 {
return nil, ErrTeamNotFound
} else {
return mod, nil
}
}
func (r team) Find(ctx context.Context, filter *types.TeamFilter) ([]*types.Team, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
var params = make([]interface{}, 0)
sql := "SELECT * FROM teams WHERE " + sqlTeamScope
if filter != nil {
if filter.Query != "" {
sql += "AND name LIKE ?"
params = append(params, filter.Query+"%")
}
}
sql += " ORDER BY name ASC"
rval := make([]*types.Team, 0)
if err := db.Select(&rval, sql, params...); err != nil {
panic(err)
return nil, ErrDatabaseError
} else {
return rval, nil
}
}
func (r team) Create(ctx context.Context, mod *types.Team) (*types.Team, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
mod.SetID(factory.Sonyflake.NextID())
if err := db.Insert("teams", mod); err != nil {
return nil, ErrDatabaseError
} else {
return mod, nil
}
}
func (r team) Update(ctx context.Context, mod *types.Team) (*types.Team, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
if err := db.Replace("teams", mod); err != nil {
return nil, ErrDatabaseError
} else {
return mod, nil
}
}
func (r team) Archive(ctx context.Context, id uint64) error {
return simpleUpdate(ctx, "teams", "archived_at", time.Now(), id)
}
func (r team) Unarchive(ctx context.Context, id uint64) error {
return simpleUpdate(ctx, "teams", "archived_at", nil, id)
}
func (r team) Delete(ctx context.Context, id uint64) error {
return simpleDelete(ctx, "teams", id)
}
func (r team) Merge(ctx context.Context, id, targetTeamId uint64) error {
return ErrNotImplemented
}
func (r team) Move(ctx context.Context, id, targetOrganisationId uint64) error {
return ErrNotImplemented
}
+121
View File
@@ -0,0 +1,121 @@
package repository
import (
"context"
"github.com/crusttech/crust/sam/types"
"github.com/titpetric/factory"
"time"
)
const (
sqlUserScope = "suspended_at IS NULL AND deleted_at IS NULL"
sqlUserSelect = "SELECT * FROM users WHERE " + sqlUserScope
ErrUserNotFound = repositoryError("UserNotFound")
)
type (
user struct{}
)
func User() user {
return user{}
}
func (r user) FindByUsername(ctx context.Context, username string) (*types.User, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
mod := &types.User{}
if err := db.Get(mod, "SELECT * FROM users WHERE username = ? AND "+sqlUserScope, username); err != nil {
return nil, ErrDatabaseError
} else if mod.ID == 0 {
return nil, ErrUserNotFound
} else {
return mod, nil
}
}
func (r user) FindById(ctx context.Context, id uint64) (*types.User, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
mod := &types.User{}
if err := db.Get(mod, "SELECT * FROM users WHERE id = ? AND "+sqlUserScope, id); err != nil {
return nil, ErrDatabaseError
} else if mod.ID == 0 {
return nil, ErrUserNotFound
} else {
return mod, nil
}
}
func (r user) Find(ctx context.Context, filter *types.UserFilter) ([]*types.User, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
var params = make([]interface{}, 0)
sql := "SELECT * FROM users WHERE " + sqlUserScope
if filter != nil {
if filter.Query != "" {
sql += "AND username LIKE ?"
params = append(params, filter.Query+"%")
}
}
sql += " ORDER BY username ASC"
rval := make([]*types.User, 0)
if err := db.Select(&rval, sql, params...); err != nil {
panic(err)
return nil, ErrDatabaseError
} else {
return rval, nil
}
}
func (r user) Create(ctx context.Context, mod *types.User) (*types.User, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
mod.SetID(factory.Sonyflake.NextID())
if err := db.Insert("users", mod); err != nil {
return nil, ErrDatabaseError
} else {
return mod, nil
}
}
func (r user) Update(ctx context.Context, mod *types.User) (*types.User, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, ErrDatabaseError
}
if err := db.Replace("users", mod); err != nil {
return nil, ErrDatabaseError
} else {
return mod, nil
}
}
func (r user) Suspend(ctx context.Context, id uint64) error {
return simpleUpdate(ctx, "users", "suspend_at", time.Now(), id)
}
func (r user) Unsuspend(ctx context.Context, id uint64) error {
return simpleUpdate(ctx, "users", "suspend_at", nil, id)
}
func (r user) Delete(ctx context.Context, id uint64) error {
return simpleDelete(ctx, "users", id)
}
+31 -98
View File
@@ -3,133 +3,66 @@ package rest
import (
"context"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/sam/rest/server"
"github.com/crusttech/crust/sam/service"
"github.com/crusttech/crust/sam/types"
)
var _ = errors.Wrap
const (
sqlChannelScope = "deleted_at IS NULL AND archived_at IS NULL"
sqlChannelSelect = "SELECT * FROM channels WHERE " + sqlChannelScope
)
type Channel struct{}
func (Channel) New() *Channel {
return &Channel{}
}
func (*Channel) Create(ctx context.Context, r *server.ChannelCreateRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
type (
Channel struct {
service channelService
}
// @todo: topic message/log entry
// @todo: channel name cmessage/log entry
// @todo: permission check if user can add channel
channelService interface {
FindById(context.Context, uint64) (*types.Channel, error)
Find(context.Context, *types.ChannelFilter) ([]*types.Channel, error)
c := types.Channel{}.
Create(context.Context, *types.Channel) (*types.Channel, error)
Update(context.Context, *types.Channel) (*types.Channel, error)
deleter
archiver
}
)
func (Channel) New() *Channel {
return &Channel{service: service.Channel()}
}
func (ctrl *Channel) Create(ctx context.Context, r *server.ChannelCreateRequest) (interface{}, error) {
channel := types.Channel{}.
New().
SetName(r.Name).
SetTopic(r.Topic).
SetMeta([]byte("{}")).
SetID(factory.Sonyflake.NextID())
return c, db.Insert("channels", c)
return ctrl.service.Create(ctx, channel)
}
func (*Channel) Edit(ctx context.Context, r *server.ChannelEditRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
/*var c *types.Channel
if c, err = c.load(r.ID); err != nil {
return nil, err
}*/
// @todo: topic change message/log entry
// @todo: channel name change message/log entry
// @todo: permission check if user can edit channel
// @todo: make sure archived & deleted entries can not be edited
// @todo: handle channel moving
// @todo: handle channel archiving
c := types.Channel{}.
func (ctrl *Channel) Edit(ctx context.Context, r *server.ChannelEditRequest) (interface{}, error) {
channel := types.Channel{}.
New().
SetName(r.Name).
SetTopic(r.Topic)
return c, db.Replace("channels", c)
return ctrl.service.Update(ctx, channel)
}
func (*Channel) Delete(ctx context.Context, r *server.ChannelDeleteRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
/*var c *types.Channel
if c, err = c.load(r.ID); err != nil {
return nil, err
}*/
// @todo: make history unavailable
// @todo: notify users that channel has been removed (remove from web UI)
// @todo: permissions check if user cah remove channel
stmt := "UPDATE channels SET deleted_at = NOW() WHERE id = ? AND deleted_at IS NULL"
spew.Dump(r.ID)
return nil, func() error {
_, err := db.Exec(stmt, r.ID)
return err
}()
func (ctrl *Channel) Delete(ctx context.Context, r *server.ChannelDeleteRequest) (interface{}, error) {
return nil, ctrl.service.Delete(ctx, r.ID)
}
func (s *Channel) Read(ctx context.Context, r *server.ChannelReadRequest) (interface{}, error) {
return s.load(r.ID)
func (ctrl *Channel) Read(ctx context.Context, r *server.ChannelReadRequest) (interface{}, error) {
return ctrl.service.FindById(ctx, r.ID)
}
func (*Channel) List(ctx context.Context, r *server.ChannelListRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
// @todo: permission check to return only channels that user has access to
// @todo: actual searching not just a full select
res := make([]types.Channel, 0)
err = db.Select(&res, sqlChannelSelect+" ORDER BY name ASC")
return res, err
}
func (*Channel) load(id uint64) (*types.Channel, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
c := types.Channel{}.New()
if id == 0 {
return nil, errors.New("Provide channel ID")
} else if err := db.Get(c, sqlChannelSelect+" AND id = ?", id); err != nil {
return nil, err
} else if c.ID != id {
spew.Dump(c)
return nil, errors.New("Unexisting channel")
}
// @todo: permission check if user can read channel
return c, nil
func (ctrl *Channel) List(ctx context.Context, r *server.ChannelListRequest) (interface{}, error) {
return ctrl.service.Find(ctx, &types.ChannelFilter{Query: r.Query})
}
+35 -83
View File
@@ -2,111 +2,63 @@ package rest
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/sam/rest/server"
"github.com/crusttech/crust/sam/types"
"github.com/pkg/errors"
)
var _ = errors.Wrap
const (
sqlOrganisationScope = "deleted_at IS NULL AND archived_at IS NULL"
sqlOrganisationSelect = "SELECT * FROM organisations WHERE " + sqlOrganisationScope
)
type (
Organisation struct {
service organisationService
}
type Organisation struct{}
organisationService interface {
FindById(context.Context, uint64) (*types.Organisation, error)
Find(context.Context, *types.OrganisationFilter) ([]*types.Organisation, error)
Create(context.Context, *types.Organisation) (*types.Organisation, error)
Update(context.Context, *types.Organisation) (*types.Organisation, error)
deleter
archiver
}
)
func (Organisation) New() *Organisation {
return &Organisation{}
}
func (*Organisation) Create(ctx context.Context, r *server.OrganisationCreateRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
// @todo: permission check if user can add/edit organisation
// @todo: make sure archived & deleted entries can not be edited
o := types.Organisation{}.New().SetName(r.Name).SetID(factory.Sonyflake.NextID())
return o, db.Insert("organisation", o)
func (ctrl *Organisation) Read(ctx context.Context, r *server.OrganisationReadRequest) (interface{}, error) {
return ctrl.service.FindById(ctx, r.ID)
}
func (*Organisation) Edit(ctx context.Context, r *server.OrganisationEditRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
// @todo: permission check if user can add/edit organisation
// @todo: make sure archived & deleted entries can not be edited
o := types.Organisation{}.New().SetID(r.ID).SetName(r.Name)
return o, db.Replace("organisation", o)
func (ctrl *Organisation) List(ctx context.Context, r *server.OrganisationListRequest) (interface{}, error) {
return ctrl.service.Find(ctx, &types.OrganisationFilter{Query: r.Query})
}
func (*Organisation) Remove(ctx context.Context, r *server.OrganisationRemoveRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
func (ctrl *Organisation) Create(ctx context.Context, r *server.OrganisationCreateRequest) (interface{}, error) {
org := types.Organisation{}.
New().
SetName(r.Name)
// @todo: permission check
// @todo: don't actually delete organisation?!
stmt := "UPDATE organisationss SET deleted_at = NOW() WHERE deleted_at IS NULL AND id = ?"
return nil, func() error {
_, err := db.Exec(stmt, r.ID)
return err
}()
return ctrl.service.Create(ctx, org)
}
func (*Organisation) Read(ctx context.Context, r *server.OrganisationReadRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
func (ctrl *Organisation) Edit(ctx context.Context, r *server.OrganisationEditRequest) (interface{}, error) {
org := types.Organisation{}.
New().
SetID(r.ID).
SetName(r.Name)
// @todo: permissions check
o := types.Organisation{}.New()
return o, db.Get(o, sqlOrganisationSelect+" AND id = ?", r.ID)
return ctrl.service.Update(ctx, org)
}
func (*Organisation) List(ctx context.Context, r *server.OrganisationListRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
// @todo: permissions check
// @todo: actual search for org
res := make([]Organisation, 0)
err = db.Select(&res, sqlOrganisationSelect+" WHERE label LIKE = ? ORDER BY label ASC", r.Query+"%")
return res, err
func (ctrl *Organisation) Remove(ctx context.Context, r *server.OrganisationRemoveRequest) (interface{}, error) {
return nil, ctrl.service.Delete(ctx, r.ID)
}
func (*Organisation) Archive(ctx context.Context, r *server.OrganisationArchiveRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
// @todo: permission check
stmt := fmt.Sprintf(
"UPDATE organisation SET archived_at = NOW() WHERE %s AND id = ?",
sqlChannelScope)
return nil, func() error {
_, err := db.Exec(stmt, r.ID)
return err
}()
func (ctrl *Organisation) Archive(ctx context.Context, r *server.OrganisationArchiveRequest) (interface{}, error) {
return nil, ctrl.service.Archive(ctx, r.ID)
}
+20
View File
@@ -0,0 +1,20 @@
package rest
import (
"context"
)
type (
suspender interface {
Suspend(context.Context, uint64) error
Unsuspend(context.Context, uint64) error
}
archiver interface {
Archive(context.Context, uint64) error
Unarchive(context.Context, uint64) error
}
deleter interface {
Delete(context.Context, uint64) error
}
)
+41 -79
View File
@@ -2,111 +2,73 @@ package rest
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/sam/rest/server"
"github.com/crusttech/crust/sam/types"
"github.com/pkg/errors"
)
var _ = errors.Wrap
const (
sqlTeamScope = "deleted_at IS NULL AND archived_at IS NULL"
sqlTeamSelect = "SELECT * FROM teams WHERE " + sqlTeamScope
)
type (
Team struct {
service teamService
}
type Team struct{}
teamService interface {
FindById(context.Context, uint64) (*types.Team, error)
Find(context.Context, *types.TeamFilter) ([]*types.Team, error)
Create(context.Context, *types.Team) (*types.Team, error)
Update(context.Context, *types.Team) (*types.Team, error)
Merge(context.Context, *types.Team) error
Move(context.Context, *types.Team) error
deleter
archiver
}
)
func (Team) New() *Team {
return &Team{}
}
func (*Team) Create(ctx context.Context, r *server.TeamCreateRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
// @todo: permission check if user can add/edit the team
// @todo: make sure archived & deleted entries can not be edited
t := types.Team{}.New()
t.SetName(r.Name).SetMemberIDs(r.Members).SetID(factory.Sonyflake.NextID())
return t, db.Insert("team", t)
func (ctrl *Team) Read(ctx context.Context, r *server.TeamReadRequest) (interface{}, error) {
return ctrl.service.FindById(ctx, r.ID)
}
func (*Team) Edit(ctx context.Context, r *server.TeamEditRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
// @todo: permission check if user can add/edit the team
// @todo: make sure archived & deleted entries can not be edited
t := types.Team{}.New()
t.SetID(r.ID).SetName(r.Name).SetMemberIDs(r.Members)
return t, db.Replace("team", t)
func (ctrl *Team) List(ctx context.Context, r *server.TeamListRequest) (interface{}, error) {
return ctrl.service.Find(ctx, &types.TeamFilter{Query: r.Query})
}
func (*Team) Remove(ctx context.Context, r *server.TeamRemoveRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
func (ctrl *Team) Create(ctx context.Context, r *server.TeamCreateRequest) (interface{}, error) {
org := types.Team{}.
New().
SetName(r.Name)
stmt := "UPDATE teams SET deleted_at = NOW() WHERE deleted_at IS NULL AND id = ?"
return nil, func() error {
_, err := db.Exec(stmt, r.ID)
return err
}()
return ctrl.service.Create(ctx, org)
}
func (*Team) Read(ctx context.Context, r *server.TeamReadRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
func (ctrl *Team) Edit(ctx context.Context, r *server.TeamEditRequest) (interface{}, error) {
org := types.Team{}.
New().
SetID(r.ID).
SetName(r.Name)
t := types.Team{}.New()
return t, db.Get(t, sqlTeamSelect+" AND id = ?", r.ID)
return ctrl.service.Update(ctx, org)
}
func (*Team) List(ctx context.Context, r *server.TeamListRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
res := make([]Team, 0)
err = db.Select(&res, sqlTeamSelect+" ORDER BY name ASC")
return res, err
func (ctrl *Team) Remove(ctx context.Context, r *server.TeamRemoveRequest) (interface{}, error) {
return nil, ctrl.service.Delete(ctx, r.ID)
}
func (*Team) Archive(ctx context.Context, r *server.TeamArchiveRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
stmt := fmt.Sprintf(
"UPDATE teams SET archived_at = NOW() WHERE %s AND id = ?",
sqlTeamScope)
return nil, func() error {
_, err := db.Exec(stmt, r.ID)
return err
}()
func (ctrl *Team) Archive(ctx context.Context, r *server.TeamArchiveRequest) (interface{}, error) {
return nil, ctrl.service.Archive(ctx, r.ID)
}
func (*Team) Move(ctx context.Context, r *server.TeamMoveRequest) (interface{}, error) {
return nil, errors.New("Not implemented: Team.move")
func (ctrl *Team) Merge(ctx context.Context, r *server.TeamMergeRequest) (interface{}, error) {
return nil, ctrl.service.Merge(ctx, &types.Team{ID: r.ID})
}
func (*Team) Merge(ctx context.Context, r *server.TeamMergeRequest) (interface{}, error) {
return nil, errors.New("Not implemented: Team.merge")
func (ctrl *Team) Move(ctx context.Context, r *server.TeamMoveRequest) (interface{}, error) {
return nil, ctrl.service.Move(ctx, &types.Team{ID: r.ID})
}
+17 -92
View File
@@ -3,118 +3,43 @@ package rest
import (
"context"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/sam/rest/server"
"github.com/crusttech/crust/sam/service"
"github.com/crusttech/crust/sam/types"
"github.com/pkg/errors"
)
var _ = errors.Wrap
const (
sqlUserScope = "suspended_at IS NULL AND archived_at IS NULL"
sqlUserSelect = "SELECT * FROM users WHERE " + sqlUserScope
)
type (
User struct {
service UserInterface
service userService
}
UserInterface interface {
Set(*types.User)
CanLogin() bool
GeneratePassword(value string) ([]byte, error)
ValidatePassword(value string) bool
ValidateUser() bool
userService interface {
ValidateCredentials(context.Context, string, string) (*types.User, error)
FindById(context.Context, uint64) (*types.User, error)
Find(context.Context, *types.UserFilter) ([]*types.User, error)
Create(context.Context, *types.User) (*types.User, error)
Update(context.Context, *types.User) (*types.User, error)
deleter
suspender
}
)
func (User) New() *User {
return &User{service.User{}.New()}
return &User{service: service.User()}
}
/*
func (self *User) Read(ctx context.Context, r *teamReadRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
t := types.User{}.New()
return t, db.Get(t, sqlUserSelect+" AND id = ?", r.ID)
}
*/
// User lookup & login
func (self *User) Login(ctx context.Context, r *server.UserLoginRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
u := types.User{}.New()
if err = db.Get(u, sqlUserSelect+" AND username = ?", r.Username); err != nil {
return nil, err
}
self.service.Set(u)
if self.service.ValidateUser() || !self.service.ValidatePassword(r.Password) {
return nil, errors.New("Invalid username and password combination")
}
if !self.service.CanLogin() {
return nil, errors.New("User is not allowed to login")
}
return u, nil
return self.service.ValidateCredentials(ctx, r.Username, r.Password)
}
// Searches the users table in the database to find users by matching (by-prefix) their.Username
func (*User) Search(ctx context.Context, r *server.UserSearchRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
uu := []types.User{}
if err != db.Get(uu, sqlUserSelect+" AND username LIKE ?", r.Query+"%") {
return nil, err
}
return uu, nil
func (self *User) Search(ctx context.Context, r *server.UserSearchRequest) (interface{}, error) {
return self.service.Find(ctx, &types.UserFilter{Query: r.Query})
}
/*
func (self *User) Remove(ctx context.Context, r *teamRemoveRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
stmt := "UPDATE users SET deleted_at = NOW() WHERE deleted_at IS NULL AND id = ?"
return nil, func() error {
_, err := db.Exec(stmt, r.ID)
return err
}()
}
func (self *User) Archive(ctx context.Context, r *teamArchiveRequest) (interface{}, error) {
db, err := factory.Database.Get()
if err != nil {
return nil, err
}
stmt := fmt.Sprintf(
"UPDATE users SET archived_at = NOW() WHERE %s AND id = ?",
sqlUserScope)
return nil, func() error {
_, err := db.Exec(stmt, r.ID)
return err
}()
}
*/
+79
View File
@@ -0,0 +1,79 @@
package service
import (
"context"
"github.com/crusttech/crust/sam/repository"
"github.com/crusttech/crust/sam/types"
)
type (
channel struct {
repository channelRepository
}
channelRepository interface {
FindById(context.Context, uint64) (*types.Channel, error)
Find(context.Context, *types.ChannelFilter) ([]*types.Channel, error)
Create(context.Context, *types.Channel) (*types.Channel, error)
Update(context.Context, *types.Channel) (*types.Channel, error)
deleter
archiver
}
)
func Channel() *channel {
return &channel{repository: repository.Channel()}
}
func (svc channel) FindById(ctx context.Context, id uint64) (*types.Channel, error) {
// @todo: permission check if current user can read channel
return svc.repository.FindById(ctx, id)
}
func (svc channel) Find(ctx context.Context, filter *types.ChannelFilter) ([]*types.Channel, error) {
// @todo: permission check to return only channels that channel has access to
// @todo: actual searching not just a full select
return svc.repository.Find(ctx, filter)
}
func (svc channel) Create(ctx context.Context, mod *types.Channel) (*types.Channel, error) {
// @todo: topic message/log entry
// @todo: channel name cmessage/log entry
// @todo: permission check if channel can add channel
return svc.repository.Create(ctx, mod)
}
func (svc channel) Update(ctx context.Context, mod *types.Channel) (*types.Channel, error) {
// @todo: topic change message/log entry
// @todo: channel name change message/log entry
// @todo: permission check if current user can edit channel
// @todo: make sure archived & deleted entries can not be edited
// @todo: handle channel movinga
// @todo: handle channel archiving
return svc.repository.Update(ctx, mod)
}
func (svc channel) Delete(ctx context.Context, id uint64) error {
// @todo: make history unavailable
// @todo: notify users that channel has been removed (remove from web UI)
// @todo: permissions check if current user can remove channel
return svc.repository.Delete(ctx, id)
}
func (svc channel) Archive(ctx context.Context, id uint64) error {
// @todo: make history unavailable
// @todo: notify users that channel has been removed (remove from web UI)
// @todo: permissions check if current user can remove channel
return svc.repository.Archive(ctx, id)
}
func (svc channel) Unarchive(ctx context.Context, id uint64) error {
// @todo: make history unavailable
// @todo: notify users that channel has been removed (remove from web UI)
// @todo: permissions check if current user can remove channel
return svc.repository.Unarchive(ctx, id)
}
+9
View File
@@ -0,0 +1,9 @@
package service
type (
serviceError string
)
func (e serviceError) Error() string {
return "crust.sam.service." + string(e)
}
+74
View File
@@ -0,0 +1,74 @@
package service
import (
"context"
"github.com/crusttech/crust/sam/repository"
"github.com/crusttech/crust/sam/types"
)
type (
organisation struct {
repository organisationRepository
}
organisationRepository interface {
FindById(context.Context, uint64) (*types.Organisation, error)
Find(context.Context, *types.OrganisationFilter) ([]*types.Organisation, error)
Create(context.Context, *types.Organisation) (*types.Organisation, error)
Update(context.Context, *types.Organisation) (*types.Organisation, error)
deleter
archiver
}
)
func Organisation() *organisation {
return &organisation{repository: repository.Organisation()}
}
func (svc organisation) FindById(ctx context.Context, id uint64) (*types.Organisation, error) {
// @todo: permission check if current user can read organisation
return svc.repository.FindById(ctx, id)
}
func (svc organisation) Find(ctx context.Context, filter *types.OrganisationFilter) ([]*types.Organisation, error) {
// @todo: permission check to return only organisations that organisation has access to
// @todo: actual searching not just a full select
return svc.repository.Find(ctx, filter)
}
func (svc organisation) Create(ctx context.Context, mod *types.Organisation) (*types.Organisation, error) {
// @todo: permission check if current user can add/edit organisation
// @todo: make sure archived & deleted entries can not be edited
return svc.repository.Create(ctx, mod)
}
func (svc organisation) Update(ctx context.Context, mod *types.Organisation) (*types.Organisation, error) {
// @todo: permission check if current user can add/edit organisation
// @todo: make sure archived & deleted entries can not be edited
return svc.repository.Update(ctx, mod)
}
func (svc organisation) Delete(ctx context.Context, id uint64) error {
// @todo: permissions check if current user can remove organisation
// @todo: make history unavailable
// @todo: notify users that organisation has been removed (remove from web UI)
return svc.repository.Delete(ctx, id)
}
func (svc organisation) Archive(ctx context.Context, id uint64) error {
// @todo: make history unavailable
// @todo: notify users that organisation has been removed (remove from web UI)
// @todo: permissions check if current user can archive organisation
return svc.repository.Archive(ctx, id)
}
func (svc organisation) Unarchive(ctx context.Context, id uint64) error {
// @todo: make history unavailable
// @todo: notify users that organisation has been removed (remove from web UI)
// @todo: permissions check if current user can unarchive organisation
return svc.repository.Unarchive(ctx, id)
}
+20
View File
@@ -0,0 +1,20 @@
package service
import (
"context"
)
type (
suspender interface {
Suspend(context.Context, uint64) error
Unsuspend(context.Context, uint64) error
}
archiver interface {
Archive(context.Context, uint64) error
Unarchive(context.Context, uint64) error
}
deleter interface {
Delete(context.Context, uint64) error
}
)
+85
View File
@@ -0,0 +1,85 @@
package service
import (
"context"
"github.com/crusttech/crust/sam/repository"
"github.com/crusttech/crust/sam/types"
)
type (
team struct {
repository teamRepository
}
teamRepository interface {
FindById(context.Context, uint64) (*types.Team, error)
Find(context.Context, *types.TeamFilter) ([]*types.Team, error)
Create(context.Context, *types.Team) (*types.Team, error)
Update(context.Context, *types.Team) (*types.Team, error)
Merge(context.Context, uint64, uint64) error
Move(context.Context, uint64, uint64) error
deleter
archiver
}
)
func Team() *team {
return &team{repository: repository.Team()}
}
func (svc team) FindById(ctx context.Context, id uint64) (*types.Team, error) {
// @todo: permission check if current user has access to this team
return svc.repository.FindById(ctx, id)
}
func (svc team) Find(ctx context.Context, filter *types.TeamFilter) ([]*types.Team, error) {
// @todo: permission check to return only teams that current user has access to
return svc.repository.Find(ctx, filter)
}
func (svc team) Create(ctx context.Context, mod *types.Team) (*types.Team, error) {
// @todo: permission check if current user can add/edit team
return svc.repository.Create(ctx, mod)
}
func (svc team) Update(ctx context.Context, mod *types.Team) (*types.Team, error) {
// @todo: permission check if current user can add/edit team
// @todo: make sure archived & deleted entries can not be edited
return svc.repository.Update(ctx, mod)
}
func (svc team) Delete(ctx context.Context, id uint64) error {
// @todo: make history unavailable
// @todo: notify users that team has been removed (remove from web UI)
// @todo: permissions check if current user can remove team
return svc.repository.Delete(ctx, id)
}
func (svc team) Archive(ctx context.Context, id uint64) error {
// @todo: make history unavailable
// @todo: notify users that team has been removed (remove from web UI)
// @todo: permissions check if current user can remove team
return svc.repository.Archive(ctx, id)
}
func (svc team) Unarchive(ctx context.Context, id uint64) error {
// @todo: permissions check if current user can unarchive team
// @todo: make history accessible
// @todo: notify users that team has been unarchived
return svc.repository.Unarchive(ctx, id)
}
func (svc team) Merge(ctx context.Context, id uint64, targetTeamId uint64) error {
// @todo: permission check if current user can merge team
return svc.repository.Merge(ctx, id, targetTeamId)
}
func (svc team) Move(ctx context.Context, id uint64, targetOrganisationId uint64) error {
// @todo: permission check if current user can move team to another organisation
return svc.repository.Move(ctx, id, targetOrganisationId)
}
+86 -16
View File
@@ -1,35 +1,105 @@
package service
import (
"golang.org/x/crypto/bcrypt"
"context"
"github.com/crusttech/crust/sam/repository"
"github.com/crusttech/crust/sam/types"
"golang.org/x/crypto/bcrypt"
)
type User struct {
*types.User
const (
ErrUserInvalidCredentials = serviceError("UserInvalidCredentials")
ErrUserLocked = serviceError("UserLocked")
)
type (
user struct {
repository userRepository
}
userRepository interface {
FindByUsername(context.Context, string) (*types.User, error)
FindById(context.Context, uint64) (*types.User, error)
Find(context.Context, *types.UserFilter) ([]*types.User, error)
Create(context.Context, *types.User) (*types.User, error)
Update(context.Context, *types.User) (*types.User, error)
deleter
suspender
}
)
func User() *user {
return &user{repository: repository.User()}
}
func (User) New() *User {
return &User{types.User{}.New()}
func (svc user) ValidateCredentials(ctx context.Context, username, password string) (*types.User, error) {
user, err := svc.repository.FindByUsername(ctx, username)
if err != nil {
return nil, err
}
if !svc.validatePassword(user, password) {
return nil, ErrUserInvalidCredentials
}
if !svc.canLogin(user) {
return nil, ErrUserLocked
}
return user, nil
}
func (u *User) Set(user *types.User) {
u.User = user
func (svc user) FindById(ctx context.Context, id uint64) (*types.User, error) {
return svc.repository.FindById(ctx, id)
}
func (u *User) CanLogin() bool {
return u.User != nil && u.User.ID > 0
func (svc user) Find(ctx context.Context, filter *types.UserFilter) ([]*types.User, error) {
return svc.repository.Find(ctx, filter)
}
func (u *User) GeneratePassword(value string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(value), bcrypt.DefaultCost)
func (svc user) Create(ctx context.Context, mod *types.User) (*types.User, error) {
return svc.repository.Create(ctx, mod)
}
func (u *User) ValidatePassword(value string) bool {
return u.User != nil && bcrypt.CompareHashAndPassword(u.User.Password, []byte(value)) == nil
func (svc user) Update(ctx context.Context, mod *types.User) (*types.User, error) {
return svc.repository.Update(ctx, mod)
}
func (u *User) ValidateUser() bool {
return u.User != nil && u.User.ID > 0
func (svc user) validatePassword(user *types.User, password string) bool {
return user != nil &&
bcrypt.CompareHashAndPassword(user.Password, []byte(password)) == nil
}
func (svc user) generatePassword(user *types.User, password string) error {
pwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
user.SetPassword(pwd)
return nil
}
func (svc user) canLogin(u *types.User) bool {
return u != nil && u.ID > 0 && u.SuspendedAt == nil && u.DeletedAt == nil
}
func (svc user) Delete(ctx context.Context, id uint64) error {
// @todo: permissions check if current user can delete this user
// @todo: notify users that user has been removed (remove from web UI)
return svc.repository.Delete(ctx, id)
}
func (svc user) Suspend(ctx context.Context, id uint64) error {
// @todo: permissions check if current user can suspend this user
// @todo: notify users that user has been supsended (remove from web UI)
return svc.repository.Suspend(ctx, id)
}
func (svc user) Unsuspend(ctx context.Context, id uint64) error {
// @todo: permissions check if current user can unsuspend this user
// @todo: notify users that user has been unsuspended
return svc.repository.Unsuspend(ctx, id)
}
+7
View File
@@ -0,0 +1,7 @@
package types
type (
ChannelFilter struct {
Query string
}
)
+7
View File
@@ -0,0 +1,7 @@
package types
type (
OrganisationFilter struct {
Query string
}
)
+7
View File
@@ -0,0 +1,7 @@
package types
type (
TeamFilter struct {
Query string
}
)
+29 -5
View File
@@ -22,11 +22,13 @@ import (
type (
// Users
User struct {
ID uint64 `db:"id"`
Username string `db:"username"`
Password []byte `json:"-" db:"password"`
SuspendedAt *time.Time `json:",omitempty" db:"suspended_at"`
DeletedAt *time.Time `json:",omitempty" db:"deleted_at"`
ID uint64 `db:"id"`
Username string `db:"username"`
Meta interface{} `json:"-" db:"meta"`
OrganisationID uint64 `db:"rel_organisation"`
Password []byte `json:"-" db:"password"`
SuspendedAt *time.Time `json:",omitempty" db:"suspended_at"`
DeletedAt *time.Time `json:",omitempty" db:"deleted_at"`
changed []string
}
@@ -60,6 +62,28 @@ func (u *User) SetUsername(value string) *User {
}
return u
}
func (u *User) GetMeta() interface{} {
return u.Meta
}
func (u *User) SetMeta(value interface{}) *User {
if u.Meta != value {
u.changed = append(u.changed, "Meta")
u.Meta = value
}
return u
}
func (u *User) GetOrganisationID() uint64 {
return u.OrganisationID
}
func (u *User) SetOrganisationID(value uint64) *User {
if u.OrganisationID != value {
u.changed = append(u.changed, "OrganisationID")
u.OrganisationID = value
}
return u
}
func (u *User) GetPassword() []byte {
return u.Password
}
+7
View File
@@ -0,0 +1,7 @@
package types
type (
UserFilter struct {
Query string
}
)