diff options
Diffstat (limited to 'pkg/components')
-rw-r--r-- | pkg/components/auth/controller.go | 57 | ||||
-rw-r--r-- | pkg/components/auth/controller_test.go | 190 | ||||
-rw-r--r-- | pkg/components/auth/model.go | 32 | ||||
-rw-r--r-- | pkg/components/filesystem/controller.go | 89 | ||||
-rw-r--r-- | pkg/components/filesystem/model.go | 10 | ||||
-rw-r--r-- | pkg/components/media/model.go | 57 | ||||
-rw-r--r-- | pkg/components/settings/model.go | 15 |
7 files changed, 450 insertions, 0 deletions
diff --git a/pkg/components/auth/controller.go b/pkg/components/auth/controller.go new file mode 100644 index 0000000..4da6071 --- /dev/null +++ b/pkg/components/auth/controller.go @@ -0,0 +1,57 @@ +package auth + +import ( + "context" + + "golang.org/x/crypto/bcrypt" + + "git.sr.ht/~gabrielgio/img/pkg/ext" +) + +type Controller struct { + repository Repository + key []byte +} + +func NewController(repository Repository, key []byte) *Controller { + return &Controller{ + repository: repository, + key: key, + } +} + +func (c *Controller) Login(ctx context.Context, username, password []byte) ([]byte, error) { + id, err := c.repository.GetIDByUsername(ctx, string(username)) + if err != nil { + return nil, err + } + + hashedPassword, err := c.repository.GetPassword(ctx, id) + if err != nil { + return nil, err + } + + if err := bcrypt.CompareHashAndPassword(hashedPassword, password); err != nil { + return nil, err + } + + token := &ext.Token{ + UserID: id, + Username: string(username), + } + return ext.WriteToken(token, c.key) +} + +func (c *Controller) Register(ctx context.Context, username, password []byte) error { + hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost) + if err != nil { + return err + } + + _, err = c.repository.Create(ctx, &CreateUser{ + Username: string(username), + Password: hash, + }) + + return err +} diff --git a/pkg/components/auth/controller_test.go b/pkg/components/auth/controller_test.go new file mode 100644 index 0000000..33aa901 --- /dev/null +++ b/pkg/components/auth/controller_test.go @@ -0,0 +1,190 @@ +//go:build unit + +package auth + +import ( + "context" + "errors" + "testing" + + "github.com/samber/lo" + + "git.sr.ht/~gabrielgio/img/pkg/ext" + "git.sr.ht/~gabrielgio/img/pkg/testkit" +) + +type ( + scene struct { + ctx context.Context + mockRepository *MockUserRepository + controller Controller + } + + mockUser struct { + id uint + username string + password []byte + } + + MockUserRepository struct { + index uint + users []*mockUser + err error + } +) + +var ( + _ Repository = &MockUserRepository{} + key = []byte("6368616e676520746869732070617373") +) + +func setUp() *scene { + mockUserRepository := &MockUserRepository{} + return &scene{ + ctx: context.Background(), + mockRepository: mockUserRepository, + controller: *NewController(mockUserRepository, key), + } +} + +func TestRegisterAndLogin(t *testing.T) { + testCases := []struct { + name string + username string + password []byte + }{ + { + name: "Normal register", + username: "username", + password: []byte("password"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + scene := setUp() + + err := scene.controller.Register(scene.ctx, []byte(tc.username), tc.password) + testkit.TestFatalError(t, "Register", err) + + userID := scene.mockRepository.GetLastId() + + user, err := scene.mockRepository.Get(scene.ctx, userID) + testkit.TestFatalError(t, "Get", err) + testkit.TestValue(t, "Register", tc.username, user.Username) + + auth, err := scene.controller.Login(scene.ctx, []byte(tc.username), tc.password) + testkit.TestFatalError(t, "Login", err) + + token, err := ext.ReadToken(auth, key) + testkit.TestFatalError(t, "Login", err) + + testkit.TestValue(t, "Login", tc.username, token.Username) + testkit.TestValue(t, "Login", userID, token.UserID) + }) + } +} + +func toUser(m *mockUser, _ int) *User { + return &User{ + ID: m.id, + Username: m.username, + } +} + +func (m *MockUserRepository) GetLastId() uint { + return m.index +} + +func (m *MockUserRepository) List(ctx context.Context) ([]*User, error) { + if m.err != nil { + return nil, m.err + } + + return lo.Map(m.users, toUser), nil +} + +func (m *MockUserRepository) Get(ctx context.Context, id uint) (*User, error) { + if m.err != nil { + return nil, m.err + } + + for _, m := range m.users { + if m.id == id { + return toUser(m, 0), nil + } + } + return nil, errors.New("Item not found") +} + +func (m *MockUserRepository) GetIDByUsername(ctx context.Context, username string) (uint, error) { + if m.err != nil { + return 0, m.err + } + + for _, m := range m.users { + if m.username == username { + return m.id, nil + } + } + return 0, errors.New("Item not found") +} + +func (m *MockUserRepository) GetPassword(ctx context.Context, id uint) ([]byte, error) { + if m.err != nil { + return nil, m.err + } + + for _, m := range m.users { + if m.id == id { + return m.password, nil + } + } + return nil, errors.New("Item not found") +} + +func (m *MockUserRepository) Create(ctx context.Context, createUser *CreateUser) (uint, error) { + if m.err != nil { + return 0, m.err + } + + m.index++ + + m.users = append(m.users, &mockUser{ + id: m.index, + username: createUser.Username, + password: createUser.Password, + }) + + return m.index, nil +} + +func (m *MockUserRepository) Update(ctx context.Context, id uint, update *UpdateUser) error { + if m.err != nil { + return m.err + } + + for _, m := range m.users { + if m.id == id { + m.username = update.Username + } + } + return nil +} + +func remove[T any](slice []T, s int) []T { + return append(slice[:s], slice[s+1:]...) +} + +func (r *MockUserRepository) Delete(ctx context.Context, id uint) error { + if r.err != nil { + return r.err + } + + for i, m := range r.users { + if m.id == id { + r.users = remove(r.users, i) + } + } + return nil +} diff --git a/pkg/components/auth/model.go b/pkg/components/auth/model.go new file mode 100644 index 0000000..e46ef49 --- /dev/null +++ b/pkg/components/auth/model.go @@ -0,0 +1,32 @@ +package auth + +import "context" + +type ( + // TODO: move to user later + User struct { + ID uint + Username string + Name string + } + + // TODO: move to user later + UpdateUser struct { + Username string + Name string + } + + // TODO: move to user later + CreateUser struct { + Username string + Name string + Password []byte + } + + Repository interface { + GetIDByUsername(ctx context.Context, username string) (uint, error) + GetPassword(ctx context.Context, id uint) ([]byte, error) + // TODO: move to user later + Create(ctx context.Context, createUser *CreateUser) (uint, error) + } +) diff --git a/pkg/components/filesystem/controller.go b/pkg/components/filesystem/controller.go new file mode 100644 index 0000000..6b478a5 --- /dev/null +++ b/pkg/components/filesystem/controller.go @@ -0,0 +1,89 @@ +package filesystem + +import ( + "io/fs" + "net/url" + "path" + "strings" +) + +type ( + Controller struct { + repository Repository + } + + DirectoryParam struct { + Name string + UrlEncodedPath string + } + + FileParam struct { + UrlEncodedPath string + Info fs.FileInfo + } + + Page struct { + History []*DirectoryParam + Files []*FileParam + } +) + +func NewController(repository Repository) *Controller { + return &Controller{ + repository: repository, + } +} + +func getHistory(filepath string) []*DirectoryParam { + var ( + paths = strings.Split(filepath, "/") + result = make([]*DirectoryParam, 0, len(paths)) + acc = "" + ) + + // add root folder + result = append(result, &DirectoryParam{ + Name: "...", + UrlEncodedPath: "", + }) + + if len(paths) == 1 && paths[0] == "" { + return result + } + + for _, p := range paths { + acc = path.Join(acc, p) + result = append(result, &DirectoryParam{ + Name: p, + UrlEncodedPath: url.QueryEscape(acc), + }) + } + return result +} + +func (self *Controller) GetPage(filepath string) (*Page, error) { + decodedPath, err := url.QueryUnescape(filepath) + if err != nil { + return nil, err + } + + files, err := self.repository.List(decodedPath) + if err != nil { + return nil, err + } + + params := make([]*FileParam, 0, len(files)) + for _, info := range files { + fullPath := path.Join(decodedPath, info.Name()) + scapedFullPath := url.QueryEscape(fullPath) + params = append(params, &FileParam{ + Info: info, + UrlEncodedPath: scapedFullPath, + }) + } + + return &Page{ + Files: params, + History: getHistory(decodedPath), + }, nil +} diff --git a/pkg/components/filesystem/model.go b/pkg/components/filesystem/model.go new file mode 100644 index 0000000..2caed82 --- /dev/null +++ b/pkg/components/filesystem/model.go @@ -0,0 +1,10 @@ +package filesystem + +import "io/fs" + +type ( + Repository interface { + List(path string) ([]fs.FileInfo, error) + Stat(path string) (fs.FileInfo, error) + } +) diff --git a/pkg/components/media/model.go b/pkg/components/media/model.go new file mode 100644 index 0000000..f5c9ff6 --- /dev/null +++ b/pkg/components/media/model.go @@ -0,0 +1,57 @@ +package media + +import ( + "context" + "time" +) + +type ( + Media struct { + ID uint + Name string + Path string + PathHash string + MIMEType string + } + + MediaEXIF struct { + Description *string + Camera *string + Maker *string + Lens *string + DateShot *time.Time + Exposure *float64 + Aperture *float64 + Iso *int64 + FocalLength *float64 + Flash *int64 + Orientation *int64 + ExposureProgram *int64 + GPSLatitude *float64 + GPSLongitude *float64 + } + + Pagination struct { + Page int + Size int + } + + CreateMedia struct { + Name string + Path string + PathHash string + MIMEType string + } + + Repository interface { + Create(context.Context, *CreateMedia) error + Exists(context.Context, string) (bool, error) + List(context.Context, *Pagination) ([]*Media, error) + Get(context.Context, string) (*Media, error) + GetPath(context.Context, string) (string, error) + + GetEmptyEXIF(context.Context, *Pagination) ([]*Media, error) + GetEXIF(context.Context, uint) (*MediaEXIF, error) + CreateEXIF(context.Context, uint, *MediaEXIF) error + } +) diff --git a/pkg/components/settings/model.go b/pkg/components/settings/model.go new file mode 100644 index 0000000..da07f2c --- /dev/null +++ b/pkg/components/settings/model.go @@ -0,0 +1,15 @@ +package settings + +import "context" + +type ( + Settings struct { + ShowMode bool + ShowOwner bool + } + + Repository interface { + Save(context.Context, *Settings) error + Load(context.Context) (*Settings, error) + } +) |