This commit is contained in:
Evan Reichard 2021-01-11 23:48:32 -05:00
parent 96b0c888ed
commit bc3b437ebc
22 changed files with 339 additions and 186 deletions

View File

@ -1,13 +1,8 @@
package cmd package cmd
import ( import (
"fmt"
"errors"
"reichard.io/imagini/routes" "reichard.io/imagini/routes"
"reichard.io/imagini/internal/db" "reichard.io/imagini/internal/context"
"reichard.io/imagini/internal/auth"
"reichard.io/imagini/internal/models"
"reichard.io/imagini/internal/config"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"net/http" "net/http"
@ -21,67 +16,72 @@ var CmdServe = cli.Command{
Action: serveWeb, Action: serveWeb,
} }
var CmdDBTest = cli.Command{ // var CmdDBTest = cli.Command{
Name: "test", // Name: "test",
Aliases: []string{"t"}, // Aliases: []string{"t"},
Usage: "test db.", // Usage: "test db.",
Action: testDatabase, // Action: testDatabase,
} // }
func serveWeb(ctx *cli.Context) error { func serveWeb(cliCtx *cli.Context) error {
log.Info("Serving Web") 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) 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 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
// }

Binary file not shown.

View File

@ -3,20 +3,21 @@ package auth
import ( import (
"errors" "errors"
"gorm.io/gorm" "gorm.io/gorm"
"reichard.io/imagini/internal/db" "reichard.io/imagini/internal/query"
"reichard.io/imagini/internal/models"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func AuthenticateUser(userIdentifier string, userPassword string) bool { func AuthenticateUser(db *gorm.DB, creds models.APICredentials) bool {
// By Username // 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) { 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 // Error Checking
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn("[auth] User not found: ", userIdentifier) log.Warn("[auth] User not found: ", creds.User)
return false return false
} else if err != nil { } else if err != nil {
log.Error(err) log.Error(err)
@ -28,9 +29,9 @@ func AuthenticateUser(userIdentifier string, userPassword string) bool {
// Determine Type // Determine Type
switch foundUser.AuthType { switch foundUser.AuthType {
case "Local": case "Local":
return authenticateLocalUser(foundUser, userPassword) return authenticateLocalUser(foundUser, creds.Password)
case "LDAP": case "LDAP":
return authenticateLDAPUser(foundUser, userPassword) return authenticateLDAPUser(foundUser, creds.Password)
default: default:
return false return false
} }

View File

@ -1,9 +1,9 @@
package auth package auth
import ( 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 return false
} }

View File

@ -5,23 +5,24 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
log "github.com/sirupsen/logrus" 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) bPassword :=[]byte(pw)
err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), bPassword) err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), bPassword)
if err == nil { if err == nil {
log.Info("[local] Authentication successfull: ", user.Username) log.Info("[auth] Authentication successfull: ", user.Username)
return true return true
} }
log.Warn("[local] Authentication failed: ", user.Username) log.Warn("[auth] Authentication failed: ", user.Username)
return false return false
} }
func CreateUser(user db.User, pw string) error { func CreateUser(db *gorm.DB, user models.User, pw string) error {
log.Info("[local] Creating user: ", user.Username) log.Info("[auth] Creating user: ", user.Username)
_, err := db.GetUser(user) _, err := query.User(db, user)
if !errors.Is(err, gorm.ErrRecordNotFound) { if !errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn("[auth] User already exists: ", user.Username) log.Warn("[auth] User already exists: ", user.Username)
return errors.New("User already exists") return errors.New("User already exists")
@ -33,5 +34,5 @@ func CreateUser(user db.User, pw string) error {
return err return err
} }
user.HashedPassword = string(hashedPassword) user.HashedPassword = string(hashedPassword)
return db.CreateUser(user) return query.CreateUser(db, user)
} }

View File

@ -11,6 +11,7 @@ type Config struct {
DataPath string DataPath string
ConfigPath string ConfigPath string
JWTSecret string JWTSecret string
ListenPort string
} }
func NewConfig() *Config { func NewConfig() *Config {
@ -21,6 +22,7 @@ func NewConfig() *Config {
ConfigPath: getEnv("CONFIG_PATH", "/config"), ConfigPath: getEnv("CONFIG_PATH", "/config"),
DataPath: getEnv("DATA_PATH", "/data"), DataPath: getEnv("DATA_PATH", "/data"),
JWTSecret: getEnv("JWT_SECRET", "58b9340c0472cf045db226bc445966524e780cd38bc3dd707afce80c95d4de6f"), JWTSecret: getEnv("JWT_SECRET", "58b9340c0472cf045db226bc445966524e780cd38bc3dd707afce80c95d4de6f"),
ListenPort: getEnv("LISTEN_PORT", "8484"),
} }
} }

View File

@ -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,
}
}

22
internal/models/api.go Normal file
View File

@ -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"`
}

45
internal/models/db.go Normal file
View File

@ -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"`
}

View File

@ -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
}

View File

@ -1,4 +1,4 @@
package db package query
import ( import (
"log" "log"
@ -13,13 +13,12 @@ import (
"reichard.io/imagini/internal/models" "reichard.io/imagini/internal/models"
) )
var db *gorm.DB func NewDB(c *config.Config) *gorm.DB {
func ConnectDB(c *config.Config) {
gormConfig := &gorm.Config{ gormConfig := &gorm.Config{
PrepareStmt: true, PrepareStmt: true,
Logger: logger.Default.LogMode(logger.Silent), Logger: logger.Default.LogMode(logger.Silent),
} }
var db *gorm.DB
if c.DBType == "SQLite" { if c.DBType == "SQLite" {
dbLocation := path.Join(c.ConfigPath, "imagini.db") dbLocation := path.Join(c.ConfigPath, "imagini.db")
@ -34,9 +33,10 @@ func ConnectDB(c *config.Config) {
db.AutoMigrate(&models.MediaItem{}) db.AutoMigrate(&models.MediaItem{})
db.AutoMigrate(&models.Tag{}) db.AutoMigrate(&models.Tag{})
db.AutoMigrate(&models.Album{}) 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 var mediaItems []models.MediaItem
// db.Table("media_albums"). // db.Table("media_albums").
// Select("media_item.*"). // Select("media_item.*").

View File

@ -1,4 +1,4 @@
package db package query
import "errors" import "errors"

View File

@ -1,15 +1,16 @@
package db package query
import ( import (
"gorm.io/gorm"
"reichard.io/imagini/internal/models" "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 err := db.Create(&user).Error
return err 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 foundUser models.User
var count int64 var count int64
err := db.Where(&user).First(&foundUser).Count(&count).Error err := db.Where(&user).First(&foundUser).Count(&count).Error

View File

@ -30,7 +30,7 @@ func main() {
Usage: "A self hosted photo library.", Usage: "A self hosted photo library.",
Commands: []*cli.Command{ Commands: []*cli.Command{
&cmd.CmdServe, &cmd.CmdServe,
&cmd.CmdDBTest, // &cmd.CmdDBTest,
}, },
} }
err := app.Run(os.Args) err := app.Run(os.Args)

View File

@ -4,6 +4,6 @@ import (
"net/http" "net/http"
) )
func albumsHandler(w http.ResponseWriter, r *http.Request) { func (ctx *ImaginiContext) albumsHandler(w http.ResponseWriter, r *http.Request) {
} }

View File

@ -1,13 +1,70 @@
package routes package routes
import ( import (
"time"
"encoding/json"
"net/http" "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)
} }

View File

@ -4,6 +4,6 @@ import (
"net/http" "net/http"
) )
func infoHandler(w http.ResponseWriter, r *http.Request) { func (ctx *ImaginiContext) infoHandler(w http.ResponseWriter, r *http.Request) {
} }

View File

@ -4,6 +4,6 @@ import (
"net/http" "net/http"
) )
func mediaItemsHandler(w http.ResponseWriter, r *http.Request) { func (ctx *ImaginiContext) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
} }

View File

@ -1,60 +1,82 @@
package routes package routes
import ( import (
"encoding/json"
"net/http" "net/http"
"reichard.io/imagini/internal/context"
) )
func RegisterRoutes() { type ImaginiContext struct {
http.HandleFunc("/MediaItems", mediaItemsHandler) *context.ImaginiContext
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)
} }
// Examples: func RegisterRoutes(cctx *context.ImaginiContext) {
// [POST] /Login { user: <USER_OR_EMAIL>, password: <PASSWORD> } ctx := &ImaginiContext{cctx}
// [POST] /Logout http.HandleFunc("/MediaItems", ctx.mediaItemsHandler)
// [GET] /MediaItems 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{ // https://stackoverflow.com/a/59764037
// logMiddleware, func JSONError(w http.ResponseWriter, err string, code int) {
// authMiddleware, w.Header().Set("Content-Type", "application/json; charset=utf-8")
// } w.Header().Set("X-Content-Type-Options", "nosniff")
// http.Handle("/Users", MultipleMiddleware(usersHandler, commonMiddleware...)) w.WriteHeader(code)
// http.Handle("/Uploads/", MultipleMiddleware(uploadsHandler, commonMiddleware...)) json.NewEncoder(w).Encode(map[string]interface{}{"error": err})
}
// // http.HandleFunc("/uploads/", uploadsHandler()) func JSONSuccess(w http.ResponseWriter, msg string, code int) {
// http.Handle("/Uploads/", func(next http.Handler) http.Handler { w.Header().Set("Content-Type", "application/json; charset=utf-8")
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff")
// _, ok := ValidateUserToken(r) w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]interface{}{"success": msg})
}
// if ok { // METHOD:
// next.ServeHTTP(w, r) // switch r.Method {
// } else { // case http.MethodGet:
// w.WriteHeader(http.StatusUnauthorized) // // Serve the resource.
// } // case http.MethodPost:
// }) // // Create a new record.
// }(http.StripPrefix("/Uploads/", tusHandler))) // 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: // Filter Example:
// query := r.URL.Query() // query := r.URL.Query()
// filters, present := query["filters"] // 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 { // func uploadsHandler() http.Handler {
// store := filestore.FileStore{ // store := filestore.FileStore{
// Path: "./Uploads", // Path: "./Uploads",

View File

@ -4,6 +4,6 @@ import (
"net/http" "net/http"
) )
func tagsHandler(w http.ResponseWriter, r *http.Request) { func (ctx *ImaginiContext) tagsHandler(w http.ResponseWriter, r *http.Request) {
} }

View File

@ -4,6 +4,6 @@ import (
"net/http" "net/http"
) )
func uploadHandler(w http.ResponseWriter, r *http.Request) { func (ctx *ImaginiContext) uploadHandler(w http.ResponseWriter, r *http.Request) {
} }

View File

@ -2,12 +2,38 @@ package routes
import ( import (
"net/http" "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)
} }