diff --git a/cmd/cmd.go b/cmd/cmd.go index c2a4761..ced6e2b 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,13 +1,8 @@ package cmd import ( - "fmt" - "errors" "reichard.io/imagini/routes" - "reichard.io/imagini/internal/db" - "reichard.io/imagini/internal/auth" - "reichard.io/imagini/internal/models" - "reichard.io/imagini/internal/config" + "reichard.io/imagini/internal/context" "github.com/urfave/cli/v2" "net/http" @@ -21,67 +16,72 @@ var CmdServe = cli.Command{ Action: serveWeb, } -var CmdDBTest = cli.Command{ - Name: "test", - Aliases: []string{"t"}, - Usage: "test db.", - Action: testDatabase, -} +// var CmdDBTest = cli.Command{ +// Name: "test", +// Aliases: []string{"t"}, +// Usage: "test db.", +// Action: testDatabase, +// } -func serveWeb(ctx *cli.Context) error { +func serveWeb(cliCtx *cli.Context) error { log.Info("Serving Web") - routes.RegisterRoutes() - if err := http.ListenAndServe(":8080", nil); err != nil { + + ctx := context.NewImaginiContext() + routes.RegisterRoutes(ctx) + //listener, _ := net.Listen("tcp", ctx.Config.ListenPort) + + if err := http.ListenAndServe(":" + ctx.Config.ListenPort, nil); err != nil { log.Fatal(err) } - return nil -} - -func testDatabase(ctx *cli.Context) error { - log.Info("Testing Database") - c := config.NewConfig() - db.ConnectDB(c) - - err := auth.CreateUser(models.User{ - Username: "User12346", - Email: "user26@evan.pub", - FirstName: "User", - LastName: "Reichard", - AuthType: "Local", - }, "myPassword123") - - if err != nil { - fmt.Println(err) - } - - resp := auth.AuthenticateUser("User123", "myPassword123") - if resp == true { - log.Info("USER SUCCESSFULLY AUTHENTICATED BY USERNAME") - }else { - log.Info("USER NOT AUTHENTICATED") - } - - resp = auth.AuthenticateUser("user@evan.pub", "myPassword123") - if resp == true { - log.Info("USER SUCCESSFULLY AUTHENTICATED BY EMAIL") - }else { - log.Info("USER NOT AUTHENTICATED") - } - - resp = auth.AuthenticateUser("user@evan.pub", "myPassword12") - if resp == true { - log.Info("USER SUCCESSFULLY AUTHENTICATED BY EMAIL") - }else { - log.Info("USER NOT AUTHENTICATED") - } - - // foundUser, err := db.GetUser(db.User{Username: "User123"}) - - // if errors.Is(err, gorm.ErrRecordNotFound) { - // log.Warn("RECORD NOT FOUND") - // } else { - // log.Info("FOUND USER", foundUser) - // } return nil } + +// func testDatabase(cliCtx *cli.Context) error { +// log.Info("Testing Database") +// c := config.NewConfig() +// db.ConnectDB(c) +// +// err := auth.CreateUser(models.User{ +// Username: "User123", +// Email: "user26@evan.pub", +// FirstName: "User", +// LastName: "Reichard", +// AuthType: "Local", +// }, "myPassword123") +// +// if err != nil { +// fmt.Println(err) +// } +// +// resp := auth.AuthenticateUser(models.APICredentials{User:"User123", Password: "myPassword123"}) +// if resp == true { +// log.Info("USER SUCCESSFULLY AUTHENTICATED BY USERNAME") +// }else { +// log.Info("USER NOT AUTHENTICATED") +// } +// +// resp = auth.AuthenticateUser(models.APICredentials{User:"user26@evan.pub", Password: "myPassword123"}) +// if resp == true { +// log.Info("USER SUCCESSFULLY AUTHENTICATED BY EMAIL") +// }else { +// log.Info("USER NOT AUTHENTICATED") +// } +// +// resp = auth.AuthenticateUser(models.APICredentials{User:"user@evan.pub", Password: "myPassword12"}) +// if resp == true { +// log.Info("USER SUCCESSFULLY AUTHENTICATED BY EMAIL") +// }else { +// log.Info("USER NOT AUTHENTICATED") +// } +// +// // foundUser, err := db.GetUser(db.User{Username: "User123"}) +// +// // if errors.Is(err, gorm.ErrRecordNotFound) { +// // log.Warn("RECORD NOT FOUND") +// // } else { +// // log.Info("FOUND USER", foundUser) +// // } +// +// return nil +// } diff --git a/imagini.db b/imagini.db index 8efa76a..5e3b750 100644 Binary files a/imagini.db and b/imagini.db differ diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 0cd271a..7d15aca 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -3,20 +3,21 @@ package auth import ( "errors" "gorm.io/gorm" - "reichard.io/imagini/internal/db" + "reichard.io/imagini/internal/query" + "reichard.io/imagini/internal/models" log "github.com/sirupsen/logrus" ) -func AuthenticateUser(userIdentifier string, userPassword string) bool { +func AuthenticateUser(db *gorm.DB, creds models.APICredentials) bool { // By Username - foundUser, err := db.GetUser(db.User{Username: userIdentifier}) + foundUser, err := query.User(db, models.User{Username: creds.User}) if errors.Is(err, gorm.ErrRecordNotFound) { - foundUser, err = db.GetUser(db.User{Email: userIdentifier}) + foundUser, err = query.User(db, models.User{Email: creds.User}) } // Error Checking if errors.Is(err, gorm.ErrRecordNotFound) { - log.Warn("[auth] User not found: ", userIdentifier) + log.Warn("[auth] User not found: ", creds.User) return false } else if err != nil { log.Error(err) @@ -28,9 +29,9 @@ func AuthenticateUser(userIdentifier string, userPassword string) bool { // Determine Type switch foundUser.AuthType { case "Local": - return authenticateLocalUser(foundUser, userPassword) + return authenticateLocalUser(foundUser, creds.Password) case "LDAP": - return authenticateLDAPUser(foundUser, userPassword) + return authenticateLDAPUser(foundUser, creds.Password) default: return false } diff --git a/internal/auth/ldap.go b/internal/auth/ldap.go index 505d5dc..db9a9e6 100644 --- a/internal/auth/ldap.go +++ b/internal/auth/ldap.go @@ -1,9 +1,9 @@ package auth import ( - "reichard.io/imagini/internal/db" + "reichard.io/imagini/internal/models" ) -func authenticateLDAPUser(user db.User, pw string) bool { +func authenticateLDAPUser(user models.User, pw string) bool { return false } diff --git a/internal/auth/local.go b/internal/auth/local.go index 84b391e..080584e 100644 --- a/internal/auth/local.go +++ b/internal/auth/local.go @@ -5,23 +5,24 @@ import ( "gorm.io/gorm" "golang.org/x/crypto/bcrypt" log "github.com/sirupsen/logrus" - "reichard.io/imagini/internal/db" + "reichard.io/imagini/internal/query" + "reichard.io/imagini/internal/models" ) -func authenticateLocalUser(user db.User, pw string) bool { +func authenticateLocalUser(user models.User, pw string) bool { bPassword :=[]byte(pw) err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), bPassword) if err == nil { - log.Info("[local] Authentication successfull: ", user.Username) + log.Info("[auth] Authentication successfull: ", user.Username) return true } - log.Warn("[local] Authentication failed: ", user.Username) + log.Warn("[auth] Authentication failed: ", user.Username) return false } -func CreateUser(user db.User, pw string) error { - log.Info("[local] Creating user: ", user.Username) - _, err := db.GetUser(user) +func CreateUser(db *gorm.DB, user models.User, pw string) error { + log.Info("[auth] Creating user: ", user.Username) + _, err := query.User(db, user) if !errors.Is(err, gorm.ErrRecordNotFound) { log.Warn("[auth] User already exists: ", user.Username) return errors.New("User already exists") @@ -33,5 +34,5 @@ func CreateUser(user db.User, pw string) error { return err } user.HashedPassword = string(hashedPassword) - return db.CreateUser(user) + return query.CreateUser(db, user) } diff --git a/internal/config/config.go b/internal/config/config.go index b734947..b4e0d07 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,7 @@ type Config struct { DataPath string ConfigPath string JWTSecret string + ListenPort string } func NewConfig() *Config { @@ -21,6 +22,7 @@ func NewConfig() *Config { ConfigPath: getEnv("CONFIG_PATH", "/config"), DataPath: getEnv("DATA_PATH", "/data"), JWTSecret: getEnv("JWT_SECRET", "58b9340c0472cf045db226bc445966524e780cd38bc3dd707afce80c95d4de6f"), + ListenPort: getEnv("LISTEN_PORT", "8484"), } } diff --git a/internal/context/context.go b/internal/context/context.go new file mode 100644 index 0000000..8243c5d --- /dev/null +++ b/internal/context/context.go @@ -0,0 +1,21 @@ +package context + +import ( + "gorm.io/gorm" + "reichard.io/imagini/internal/query" + "reichard.io/imagini/internal/config" +) + +type ImaginiContext struct { + DB *gorm.DB + Config *config.Config +} + +func NewImaginiContext() *ImaginiContext { + c := config.NewConfig() + gormDB := query.NewDB(c) + return &ImaginiContext{ + DB: gormDB, + Config: c, + } +} diff --git a/internal/models/api.go b/internal/models/api.go new file mode 100644 index 0000000..6f51a45 --- /dev/null +++ b/internal/models/api.go @@ -0,0 +1,22 @@ +package models + +type APICredentials struct { + User string `json:"user"` + Password string `json:"password"` +} + +type APIMeta struct { + Count int `json:"count"` + Page int `json:"page"` +} + +type APIError struct { + Message string `json:"message"` + Code int `json:"code"` +} + +type APIResponse struct { + Data []interface{} `json:"data"` + Meta APIMeta `json:"meta"` + Error APIError `json:"error"` +} diff --git a/internal/models/db.go b/internal/models/db.go new file mode 100644 index 0000000..fa6f1c1 --- /dev/null +++ b/internal/models/db.go @@ -0,0 +1,45 @@ +package models + +import ( + "gorm.io/gorm" + "time" +) + +type ServerSetting struct { + gorm.Model + Name string `json:"name"` + Description string `json:"description"` + Value string `json:"value"` +} + +type User struct { + gorm.Model + Email string `json:"email" gorm:"unique;not null"` + Username string `json:"username" gorm:"unique;not null"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + AuthType string `json:"auth_type"` + HashedPassword string `json:"hashed_password"` +} + +type MediaItem struct { + gorm.Model + User User `json:"user" gorm:"ForeignKey:ID"` + EXIFDate time.Time `json:"exif_date"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + MediaType uint `json:"media_type"` + RelPath string `json:"rel_path"` + Tags []Tag `json:"tags" gorm:"many2many:media_tags;"` + Albums []Album `json:"albums" gorm:"many2many:media_albums;"` +} + +type Tag struct { + gorm.Model + Name string `json:"name"` +} + +type Album struct { + gorm.Model + Name string `json:"name"` +} diff --git a/internal/models/models.go b/internal/models/models.go deleted file mode 100644 index 98b3540..0000000 --- a/internal/models/models.go +++ /dev/null @@ -1,45 +0,0 @@ -package models - -import ( - "gorm.io/gorm" - "time" -) - -type ServerSetting struct { - gorm.Model - Name string - Description string - Value string -} - -type User struct { - gorm.Model - Email string `gorm:"unique;not null"` - Username string `gorm:"unique;not null"` - FirstName string - LastName string - AuthType string - HashedPassword string -} - -type MediaItem struct { - gorm.Model - User User `gorm:"ForeignKey:ID"` - EXIFDate time.Time - Latitude string - Longitude string - MediaType uint - RelPath string - Tags []Tag `gorm:"many2many:media_tags;"` - Albums []Album `gorm:"many2many:media_albums;"` -} - -type Tag struct { - gorm.Model - Name string -} - -type Album struct { - gorm.Model - Name string -} diff --git a/internal/db/db.go b/internal/query/db.go similarity index 95% rename from internal/db/db.go rename to internal/query/db.go index 430a089..fad854e 100644 --- a/internal/db/db.go +++ b/internal/query/db.go @@ -1,4 +1,4 @@ -package db +package query import ( "log" @@ -13,13 +13,12 @@ import ( "reichard.io/imagini/internal/models" ) -var db *gorm.DB - -func ConnectDB(c *config.Config) { +func NewDB(c *config.Config) *gorm.DB { gormConfig := &gorm.Config{ PrepareStmt: true, Logger: logger.Default.LogMode(logger.Silent), } + var db *gorm.DB if c.DBType == "SQLite" { dbLocation := path.Join(c.ConfigPath, "imagini.db") @@ -34,9 +33,10 @@ func ConnectDB(c *config.Config) { db.AutoMigrate(&models.MediaItem{}) db.AutoMigrate(&models.Tag{}) db.AutoMigrate(&models.Album{}) + return db } -func ItemsFromAlbum(user models.User, album models.Album) []models.MediaItem { +func ItemsFromAlbum(db *gorm.DB, user models.User, album models.Album) []models.MediaItem { var mediaItems []models.MediaItem // db.Table("media_albums"). // Select("media_item.*"). diff --git a/internal/db/errors.go b/internal/query/errors.go similarity index 86% rename from internal/db/errors.go rename to internal/query/errors.go index 035b3c1..5c9b824 100644 --- a/internal/db/errors.go +++ b/internal/query/errors.go @@ -1,4 +1,4 @@ -package db +package query import "errors" diff --git a/internal/db/users.go b/internal/query/users.go similarity index 70% rename from internal/db/users.go rename to internal/query/users.go index d2d2557..5fd586c 100644 --- a/internal/db/users.go +++ b/internal/query/users.go @@ -1,15 +1,16 @@ -package db +package query import ( + "gorm.io/gorm" "reichard.io/imagini/internal/models" ) -func CreateUser (user models.User) error { +func CreateUser (db *gorm.DB, user models.User) error { err := db.Create(&user).Error return err } -func User (user models.User) (models.User, error) { +func User (db *gorm.DB, user models.User) (models.User, error) { var foundUser models.User var count int64 err := db.Where(&user).First(&foundUser).Count(&count).Error diff --git a/main.go b/main.go index f58013a..aa0669b 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ func main() { Usage: "A self hosted photo library.", Commands: []*cli.Command{ &cmd.CmdServe, - &cmd.CmdDBTest, +// &cmd.CmdDBTest, }, } err := app.Run(os.Args) diff --git a/routes/albums.go b/routes/albums.go index ffc835b..faec9f0 100644 --- a/routes/albums.go +++ b/routes/albums.go @@ -4,6 +4,6 @@ import ( "net/http" ) -func albumsHandler(w http.ResponseWriter, r *http.Request) { +func (ctx *ImaginiContext) albumsHandler(w http.ResponseWriter, r *http.Request) { } diff --git a/routes/auth.go b/routes/auth.go index ab25e04..ae72337 100644 --- a/routes/auth.go +++ b/routes/auth.go @@ -1,13 +1,70 @@ package routes import ( + "time" + "encoding/json" "net/http" + + "reichard.io/imagini/internal/auth" + "reichard.io/imagini/internal/models" + // log "github.com/sirupsen/logrus" ) -func loginHandler(w http.ResponseWriter, r *http.Request) { +func (ctx *ImaginiContext) loginHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + JSONError(w, "Method is not supported.", http.StatusMethodNotAllowed) + return + } + // Decode into Struct + var creds models.APICredentials + err := json.NewDecoder(r.Body).Decode(&creds) + if err != nil { + JSONError(w, "Invalid parameters.", http.StatusBadRequest) + return + } + + // Validate + if creds.User == "" || creds.Password == "" { + JSONError(w, "Invalid parameters.", http.StatusBadRequest) + return + } + + // TODO: Is user already logged in? If so refresh token, if different user, kill session and log in new user? + + // Do login + resp := auth.AuthenticateUser(ctx.DB, creds) + if resp == true { + // Return Success + cookie := http.Cookie{ + Name: "Token", + Value: "testToken", + } + http.SetCookie(w, &cookie) + JSONSuccess(w, "Login success.", http.StatusOK) + }else { + // Return Failure + JSONError(w, "Invalid credentials.", http.StatusUnauthorized) + } } -func logoutHandler(w http.ResponseWriter, r *http.Request) { +func (ctx *ImaginiContext) logoutHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed) + return + } + // Do logout + + // TODO: Clear Session Server Side + + // Tell Client to Expire Token + cookie := &http.Cookie{ + Name: "Token", + Value: "", + Path: "/", + Expires: time.Unix(0, 0), + HttpOnly: true, + } + http.SetCookie(w, cookie) } diff --git a/routes/info.go b/routes/info.go index 2e3a759..b488b18 100644 --- a/routes/info.go +++ b/routes/info.go @@ -4,6 +4,6 @@ import ( "net/http" ) -func infoHandler(w http.ResponseWriter, r *http.Request) { +func (ctx *ImaginiContext) infoHandler(w http.ResponseWriter, r *http.Request) { } diff --git a/routes/media_items.go b/routes/media_items.go index 6c7e572..f09b5b4 100644 --- a/routes/media_items.go +++ b/routes/media_items.go @@ -4,6 +4,6 @@ import ( "net/http" ) -func mediaItemsHandler(w http.ResponseWriter, r *http.Request) { +func (ctx *ImaginiContext) mediaItemsHandler(w http.ResponseWriter, r *http.Request) { } diff --git a/routes/routes.go b/routes/routes.go index 21f0107..27140f2 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -1,60 +1,82 @@ package routes import ( + "encoding/json" "net/http" + "reichard.io/imagini/internal/context" ) -func RegisterRoutes() { - http.HandleFunc("/MediaItems", mediaItemsHandler) - http.HandleFunc("/Upload", uploadHandler) - http.HandleFunc("/Albums", albumsHandler) - http.HandleFunc("/Logout", logoutHandler) - http.HandleFunc("/Login", loginHandler) - http.HandleFunc("/Users", usersHandler) - http.HandleFunc("/Tags", tagsHandler) - http.HandleFunc("/Info", infoHandler) - http.HandleFunc("/Me", meHandler) +type ImaginiContext struct { + *context.ImaginiContext } - // Examples: - // [POST] /Login { user: , password: } - // [POST] /Logout - // [GET] /MediaItems +func RegisterRoutes(cctx *context.ImaginiContext) { + ctx := &ImaginiContext{cctx} + http.HandleFunc("/MediaItems", ctx.mediaItemsHandler) + http.HandleFunc("/Upload", ctx.uploadHandler) + http.HandleFunc("/Albums", ctx.albumsHandler) + http.HandleFunc("/Logout", ctx.logoutHandler) + http.HandleFunc("/Login", ctx.loginHandler) + http.HandleFunc("/Users", ctx.usersHandler) + http.HandleFunc("/Tags", ctx.tagsHandler) + http.HandleFunc("/Info", ctx.infoHandler) + http.HandleFunc("/Me", ctx.meHandler) +} - // commonMiddleware := []Middleware{ - // logMiddleware, - // authMiddleware, - // } - // http.Handle("/Users", MultipleMiddleware(usersHandler, commonMiddleware...)) - // http.Handle("/Uploads/", MultipleMiddleware(uploadsHandler, commonMiddleware...)) +// https://stackoverflow.com/a/59764037 +func JSONError(w http.ResponseWriter, err string, code int) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(code) + json.NewEncoder(w).Encode(map[string]interface{}{"error": err}) +} - // // http.HandleFunc("/uploads/", uploadsHandler()) - // http.Handle("/Uploads/", func(next http.Handler) http.Handler { - // return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // _, ok := ValidateUserToken(r) +func JSONSuccess(w http.ResponseWriter, msg string, code int) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(code) + json.NewEncoder(w).Encode(map[string]interface{}{"success": msg}) +} - // if ok { - // next.ServeHTTP(w, r) - // } else { - // w.WriteHeader(http.StatusUnauthorized) - // } - // }) - // }(http.StripPrefix("/Uploads/", tusHandler))) +// METHOD: +// switch r.Method { +// case http.MethodGet: +// // Serve the resource. +// case http.MethodPost: +// // Create a new record. +// case http.MethodPut: +// // Update an existing record. +// case http.MethodDelete: +// // Remove the record. +// default: +// // Give an error message. +// } + + +// commonMiddleware := []Middleware{ +// logMiddleware, +// authMiddleware, +// } +// http.Handle("/Users", MultipleMiddleware(usersHandler, commonMiddleware...)) +// http.Handle("/Uploads/", MultipleMiddleware(uploadsHandler, commonMiddleware...)) + +// // http.HandleFunc("/uploads/", uploadsHandler()) +// http.Handle("/Uploads/", func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// _, ok := ValidateUserToken(r) + +// if ok { +// next.ServeHTTP(w, r) +// } else { +// w.WriteHeader(http.StatusUnauthorized) +// } +// }) +// }(http.StripPrefix("/Uploads/", tusHandler))) // Filter Example: // query := r.URL.Query() // filters, present := query["filters"] -// HTTP Errors -// if r.Method != "GET" { -// http.Error(w, "Method is not supported.", http.StatusNotFound) -// return -// } -// if r.URL.Path != "/hello" { -// http.Error(w, "404 not found.", http.StatusNotFound) -// return -// } - // func uploadsHandler() http.Handler { // store := filestore.FileStore{ // Path: "./Uploads", diff --git a/routes/tags.go b/routes/tags.go index 4e81579..93dd07a 100644 --- a/routes/tags.go +++ b/routes/tags.go @@ -4,6 +4,6 @@ import ( "net/http" ) -func tagsHandler(w http.ResponseWriter, r *http.Request) { +func (ctx *ImaginiContext) tagsHandler(w http.ResponseWriter, r *http.Request) { } diff --git a/routes/upload.go b/routes/upload.go index 85fd2a6..91abd32 100644 --- a/routes/upload.go +++ b/routes/upload.go @@ -4,6 +4,6 @@ import ( "net/http" ) -func uploadHandler(w http.ResponseWriter, r *http.Request) { +func (ctx *ImaginiContext) uploadHandler(w http.ResponseWriter, r *http.Request) { } diff --git a/routes/users.go b/routes/users.go index 96de876..a9213b6 100644 --- a/routes/users.go +++ b/routes/users.go @@ -2,12 +2,38 @@ package routes import ( "net/http" + log "github.com/sirupsen/logrus" ) -func usersHandler(w http.ResponseWriter, r *http.Request) { - +func (ctx *ImaginiContext) usersHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + // CREATE + } else if r.Method == http.MethodPut { + // UPDATE / REPLACE + } else if r.Method == http.MethodPatch { + // UPDATE / MODIFY + } else if r.Method == http.MethodDelete { + // DELETE + } else if r.Method == http.MethodGet { + // GET + } else { + JSONError(w, "Method is not supported.", http.StatusMethodNotAllowed) + return + } } -func meHandler(w http.ResponseWriter, r *http.Request) { +func (ctx *ImaginiContext) meHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + JSONError(w, "Method is not supported.", http.StatusMethodNotAllowed) + return + } + // Get Authenticated User & Return Object + authCookie, err := r.Cookie("Token") + if err != nil { + log.Error("[routes] ", err) + return + } + + log.Info("[routes] INFO: ", authCookie) }