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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

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 (
"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.*").

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: <USER_OR_EMAIL>, password: <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",

View File

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

View File

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

View File

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