aboutsummaryrefslogtreecommitdiff
path: root/pkg/components
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/components')
-rw-r--r--pkg/components/auth/controller.go57
-rw-r--r--pkg/components/auth/controller_test.go190
-rw-r--r--pkg/components/auth/model.go32
-rw-r--r--pkg/components/filesystem/controller.go89
-rw-r--r--pkg/components/filesystem/model.go10
-rw-r--r--pkg/components/media/model.go57
-rw-r--r--pkg/components/settings/model.go15
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)
+ }
+)