Pre graphql

This commit is contained in:
Evan Reichard 2021-02-01 18:24:09 -05:00
parent dc56899b8b
commit ecf981495e
14 changed files with 414 additions and 156 deletions

1
go.mod
View File

@ -3,6 +3,7 @@ module reichard.io/imagini
go 1.15 go 1.15
require ( require (
github.com/codeon/govips v0.0.0-20200329201227-415341c0ce33 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2 // indirect github.com/disintegration/imaging v1.6.2 // indirect
github.com/dsoprea/go-exif/v3 v3.0.0-20201216222538-db167117f483 github.com/dsoprea/go-exif/v3 v3.0.0-20201216222538-db167117f483

2
go.sum
View File

@ -13,6 +13,8 @@ github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+Wji
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/codeon/govips v0.0.0-20200329201227-415341c0ce33 h1:/wmSAm0UZlQ/NmiwUlThjUUHe2cD36rjM61px1X6ccM=
github.com/codeon/govips v0.0.0-20200329201227-415341c0ce33/go.mod h1:kXwwWC7hMGnPrV6bw8gFi2k0ZLTiYTNMM+G3D9cUBt0=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@ -14,6 +14,7 @@ import (
) )
func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) { func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed) errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
return return
@ -77,72 +78,21 @@ func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) {
successJSON(w, "Logout success.", http.StatusOK) successJSON(w, "Logout success.", http.StatusOK)
} }
func (api *API) refreshLoginHandler(w http.ResponseWriter, r *http.Request) {
refreshCookie, err := r.Cookie("RefreshToken")
if err != nil {
log.Warn("[middleware] Cookie not found")
w.WriteHeader(http.StatusUnauthorized)
return
}
// Validate Refresh Token
refreshToken, ok := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value)
if !ok {
http.SetCookie(w, &http.Cookie{Name: "AccessToken", Expires: time.Unix(0, 0)})
http.SetCookie(w, &http.Cookie{Name: "RefreshToken", Expires: time.Unix(0, 0)})
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
return
}
// Acquire User & Device (Trusted)
did, ok := refreshToken.Get("did")
if !ok {
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
return
}
uid, ok := refreshToken.Get(jwt.SubjectKey)
if !ok {
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
return
}
deviceID, err := uuid.Parse(fmt.Sprintf("%v", did))
if err != nil {
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
return
}
userID, err := uuid.Parse(fmt.Sprintf("%v", uid))
if err != nil {
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
return
}
// Device Skeleton
user := models.User{Base: models.Base{UUID: userID}}
device := models.Device{Base: models.Base{UUID: deviceID}}
// Update token
accessToken, err := api.Auth.CreateJWTAccessToken(user, device)
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken}
http.SetCookie(w, &accessCookie)
// Response success
successJSON(w, "Refresh success.", http.StatusOK)
}
/** /**
* This will find or create the requested device based on ID and User. * This will find or create the requested device based on ID and User.
**/ **/
func (api *API) upsertRequestedDevice(user models.User, r *http.Request) (models.Device, error) { func (api *API) upsertRequestedDevice(user models.User, r *http.Request) (models.Device, error) {
requestedDevice := deriveRequestedDevice(r) requestedDevice := deriveRequestedDevice(r)
requestedDevice.Type = deriveDeviceType(r) requestedDevice.Type = deriveDeviceType(r)
requestedDevice.User = user requestedDevice.UserUUID = user.UUID
if requestedDevice.UUID == uuid.Nil { if requestedDevice.UUID == uuid.Nil {
createdDevice, err := api.DB.CreateDevice(requestedDevice) err := api.DB.CreateDevice(&requestedDevice)
createdDevice, err := api.DB.Device(&requestedDevice)
return createdDevice, err return createdDevice, err
} }
foundDevice, err := api.DB.Device(models.Device{ foundDevice, err := api.DB.Device(&models.Device{
Base: models.Base{ UUID: requestedDevice.UUID }, Base: models.Base{ UUID: requestedDevice.UUID },
User: user, User: user,
}) })
@ -213,6 +163,60 @@ func deriveRequestedDevice(r *http.Request) models.Device {
return deviceSkeleton return deviceSkeleton
} }
func (api *API) refreshAccessToken(w http.ResponseWriter, r *http.Request) (jwt.Token, error) {
refreshCookie, err := r.Cookie("RefreshToken")
if err != nil {
log.Warn("[middleware] RefreshToken not found")
return nil, err
}
// Validate Refresh Token
refreshToken, err := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value)
if err != nil {
http.SetCookie(w, &http.Cookie{Name: "AccessToken", Expires: time.Unix(0, 0)})
http.SetCookie(w, &http.Cookie{Name: "RefreshToken", Expires: time.Unix(0, 0)})
return nil, err
}
// Acquire User & Device (Trusted)
did, ok := refreshToken.Get("did")
if !ok {
return nil, err
}
uid, ok := refreshToken.Get(jwt.SubjectKey)
if !ok {
return nil, err
}
deviceUUID, err := uuid.Parse(fmt.Sprintf("%v", did))
if err != nil {
return nil, err
}
userUUID, err := uuid.Parse(fmt.Sprintf("%v", uid))
if err != nil {
return nil, err
}
// Device & User Skeleton
user := models.User{Base: models.Base{UUID: userUUID}}
device := models.Device{Base: models.Base{UUID: deviceUUID}}
// Update token
accessTokenString, err := api.Auth.CreateJWTAccessToken(user, device)
if err != nil {
return nil, err
}
accessCookie := http.Cookie{Name: "AccessToken", Value: accessTokenString}
http.SetCookie(w, &accessCookie)
// TODO: Update Refresh Key & Token
// Convert to jwt.Token
accessTokenBytes := []byte(accessTokenString)
accessToken, err := jwt.ParseBytes(accessTokenBytes)
return accessToken, err
}
func trimQuotes(s string) string { func trimQuotes(s string) string {
if len(s) >= 2 { if len(s) >= 2 {
if s[0] == '"' && s[len(s)-1] == '"' { if s[0] == '"' && s[len(s)-1] == '"' {

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"os"
"path" "path"
"net/http" "net/http"
) )
@ -17,6 +18,17 @@ func (api *API) mediaHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Acquire Width & Height Parameters
query := r.URL.Query()
width := query["width"]
height := query["height"]
_ = width
_ = height
// TODO: Caching & Resizing
// - If both, force resize with new scale
// - If one, scale resize proportionally
// Pull out UUIDs // Pull out UUIDs
reqInfo := r.Context().Value("uuids").(map[string]string) reqInfo := r.Context().Value("uuids").(map[string]string)
uid := reqInfo["uid"] uid := reqInfo["uid"]
@ -26,5 +38,13 @@ func (api *API) mediaHandler(w http.ResponseWriter, r *http.Request) {
folderPath := path.Join("/" + api.Config.DataPath + "/media/" + uid) folderPath := path.Join("/" + api.Config.DataPath + "/media/" + uid)
mediaPath := path.Join(folderPath + "/" + fileName) mediaPath := path.Join(folderPath + "/" + fileName)
// Check if File Exists
_, err := os.Stat(mediaPath)
if os.IsNotExist(err) {
// TODO: Different HTTP Response Code?
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
http.ServeFile(w, r, mediaPath) http.ServeFile(w, r, mediaPath)
} }

View File

@ -3,10 +3,16 @@ package api
import ( import (
"io" "io"
"os" "os"
"time" "fmt"
"path" "path"
"time"
"regexp"
"strings" "strings"
"errors"
"net/url"
"net/http" "net/http"
"encoding/json"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/dsoprea/go-exif/v3" "github.com/dsoprea/go-exif/v3"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -16,7 +22,6 @@ import (
"reichard.io/imagini/internal/models" "reichard.io/imagini/internal/models"
) )
// GET // GET
// - /api/v1/MediaItems/<GUID> // - /api/v1/MediaItems/<GUID>
// - JSON Struct // - JSON Struct
@ -34,14 +39,88 @@ func (api *API) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
// DELETE // DELETE
} else if r.Method == http.MethodGet { } else if r.Method == http.MethodGet {
// GET // GET
api.mediaItemGETHandler(w, r)
} else { } else {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed) errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
return return
} }
} }
// Paging:
// - Regular Pagination:
// - /api/v1/MediaItems?page[limit]=50&page=2
// - Meta Count Only
// - /api/v1/MediaItems?page[limit]=0
// Sorting:
// - Ascending Sort:
// - /api/v1/MediaItems?sort=created_at
// - Descending Sort:
// - /api/v1/MediaItems?sort=-created_at
// Filters:
// - Greater Than / Less Than (created_at, updated_at, exif_date)
// - /api/v1/MediaItems?filter[created_at]>=2020-01-01&filter[created_at]<=2021-01-01
// - Long / Lat Range (latitude, longitude)
// - /api/v1/MediaItems?filter[latitude]>=71.1827&filter[latitude]<=72.0000&filter[longitude]>=100.000&filter[longitude]<=101.0000
// - Image / Video (media_type)
// - /api/v1/MediaItems?filter[media_type]=Image
// - Tags (tags)
// - /api/v1/MediaItems?filter[tags]=id1,id2,id3
// - Albums (albums)
// - /api/v1/MediaItems?filter[albums]=id1
func (api *API) mediaItemGETHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
// Handle error
}
testObj := models.MediaItem{}
json.NewDecoder().Decode(&testObj)
fmt.Printf("Result: %+v\n", testObj)
// allParams, err := json.Marshal(r.Form)
// if err != nil {
// // Handle error
// }
// filter := &models.MediaItem{}
// if err = json.Unmarshal(allParams, filter); err != nil {
// // Handle error
// fmt.Printf("Fuck: %s\n", err)
// }
// fmt.Printf("Result: %+v\n", filter)
// err = normalizeForm(r.Form, models.MediaItem{})
// if err != nil {
// fmt.Printf("Error: %s\n", err)
// }
// var testItems []models.MediaItem
// api.DB.QueryBuilder(&testItems, allParams)
// fmt.Printf("\n\nItems: %+v", testItems)
// Pull out UUIDs
reqInfo := r.Context().Value("uuids").(map[string]string)
uid := reqInfo["uid"]
userUUID, _ := uuid.Parse(uid)
// TODO: Can apply multiple filters based on query parameters
mediaItemFilter := &models.MediaItem{UserUUID: userUUID}
mediaItemFilter.UserUUID = userUUID
mediaItems, count, _ := api.DB.MediaItems(mediaItemFilter)
response := &models.APIResponse{
Data: &mediaItems,
Meta: &models.APIMeta{Count: count},
}
responseJSON(w, &response, http.StatusOK)
}
func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) { func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
// 64MB limit (TODO: Change this) // 64MB limit (TODO: Change this - video)
r.ParseMultipartForm(64 << 20) r.ParseMultipartForm(64 << 20)
// Open form file // Open form file
@ -116,13 +195,13 @@ func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
// Add Additional MediaItem Fields // Add Additional MediaItem Fields
mediaItem.Base.UUID = mediaItemUUID mediaItem.Base.UUID = mediaItemUUID
mediaItem.User.UUID, err = uuid.Parse(uid) mediaItem.UserUUID, err = uuid.Parse(uid)
mediaItem.MediaType = mediaType mediaItem.MediaType = mediaType
mediaItem.FileName = fileName mediaItem.FileName = fileName
mediaItem.OrigName = multipartFileHeader.Filename mediaItem.OrigName = multipartFileHeader.Filename
// Create MediaItem in DB // Create MediaItem in DB
_, err = api.DB.CreateMediaItem(mediaItem) err = api.DB.CreateMediaItem(mediaItem)
if err != nil { if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError) errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return return
@ -131,14 +210,14 @@ func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
successJSON(w, "Upload succeeded.", http.StatusCreated) successJSON(w, "Upload succeeded.", http.StatusCreated)
} }
func mediaItemFromEXIFData(filePath string) (models.MediaItem, error) { func mediaItemFromEXIFData(filePath string) (*models.MediaItem, error) {
rawExif, err := exif.SearchFileAndExtractExif(filePath) rawExif, err := exif.SearchFileAndExtractExif(filePath)
entries, _, err := exif.GetFlatExifData(rawExif, nil) entries, _, err := exif.GetFlatExifData(rawExif, nil)
decLong := float32(1) decLong := float32(1)
decLat := float32(1) decLat := float32(1)
var mediaItem models.MediaItem mediaItem := &models.MediaItem{}
for _, v := range entries { for _, v := range entries {
if v.TagName == "DateTimeOriginal" { if v.TagName == "DateTimeOriginal" {
formattedTime, _ := time.Parse("2006:01:02 15:04:05", v.Formatted) formattedTime, _ := time.Parse("2006:01:02 15:04:05", v.Formatted)
@ -164,8 +243,8 @@ func mediaItemFromEXIFData(filePath string) (models.MediaItem, error) {
} }
} }
mediaItem.Latitude = decLat mediaItem.Latitude = &decLat
mediaItem.Longitude = decLong mediaItem.Longitude = &decLong
return mediaItem, err return mediaItem, err
} }
@ -173,3 +252,76 @@ func mediaItemFromEXIFData(filePath string) (models.MediaItem, error) {
func deriveDecimalCoordinate(degrees, minutes uint32, seconds float32) float32 { func deriveDecimalCoordinate(degrees, minutes uint32, seconds float32) float32 {
return float32(degrees) + (float32(minutes) / 60) + (seconds / 3600) return float32(degrees) + (float32(minutes) / 60) + (seconds / 3600)
} }
// {
// filters: [
// { field: "", operator: ""},
// { field: "", operator: ""},
// { field: "", operator: ""},
// ],
// sort: ""
// page: {}
// }
func normalizeForm(form url.Values, typeObj interface{}) error {
allowedFields := models.JSONFields(typeObj)
for key, val := range form {
key = strings.ToLower(key)
re := regexp.MustCompile(`^(filter|page)\[(\w*)]($|>|<)$`)
matches := re.FindStringSubmatch(key)
if len(matches) == 4 {
cmd := strings.ToLower(matches[1])
field := strings.ToLower(matches[2])
operator := strings.ToLower(matches[3])
if cmd == "page" && field == "limit" {
fmt.Printf("cmd: %s field: %s op: %s\n", cmd, field, operator)
continue
}
// Validate field
_, ok := allowedFields[field]
if !ok {
return errors.New("Invalid field.")
}
// Val assertions
tempObj := make(map[string]string)
tempObj[field] = val[0]
mi, err := json.Marshal(tempObj)
if err != nil {
// Handle error
fmt.Printf("1 Type Assertion Failed For Field: [%s] with value: [%s]\n", field, val)
}
fmt.Printf("String JSON: %s", string(mi))
refObj := &models.MediaItem{}
if err = json.Unmarshal(mi, refObj); err != nil {
// Handle error
fmt.Printf("2 Type Assertion Failed For Field: [%s] with value: [%s]\n", field, val[0])
fmt.Println(err)
}
fmt.Printf("Result: %+v\n", refObj)
fmt.Printf("cmd: %s field: %s op: %s\n", cmd, field, operator)
} else if key == "sort" {
field := strings.ToLower(val[0])
// Validate field
_, ok := allowedFields[field]
if !ok {
return errors.New("Invalid field.")
}
// TODO: Validate val
fmt.Printf("cmd: %s\n", key)
} else {
return errors.New("Invalid parameter(s)")
}
}
return nil
}

View File

@ -22,18 +22,33 @@ func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc {
func (api *API) authMiddleware(next http.Handler) http.HandlerFunc { func (api *API) authMiddleware(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Acquire Token // Acquire Token
accessCookie, err := r.Cookie("AccessToken") accessCookie, err := r.Cookie("AccessToken")
if err != nil { if err != nil {
log.Warn("[middleware] AccessToken not found") log.Warn("[middleware] AccessToken not found")
w.WriteHeader(http.StatusUnauthorized) errorJSON(w, "Invalid token.", http.StatusUnauthorized)
return return
} }
// Validate JWT Tokens // Validate JWT Tokens
accessToken, accessOK := api.Auth.ValidateJWTAccessToken(accessCookie.Value) accessToken, err := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
if err != nil && err.Error() == "exp not satisfied" {
log.Info("[middleware] Refreshing AccessToken")
accessToken, err = api.refreshAccessToken(w, r)
if err != nil {
log.Warn("[middleware] Refreshing AccessToken failed: ", err)
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
return
}
log.Info("[middleware] AccessToken Refreshed")
} else if err != nil {
log.Warn("[middleware] AccessToken failed to validate")
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
return
}
if accessOK {
// Acquire UserID and DeviceID // Acquire UserID and DeviceID
reqInfo := make(map[string]string) reqInfo := make(map[string]string)
uid, _ := accessToken.Get("sub") uid, _ := accessToken.Get("sub")
@ -46,9 +61,6 @@ func (api *API) authMiddleware(next http.Handler) http.HandlerFunc {
sr := r.WithContext(ctx) sr := r.WithContext(ctx)
next.ServeHTTP(w, sr) next.ServeHTTP(w, sr)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
}) })
} }

View File

@ -3,6 +3,8 @@ package api
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"reichard.io/imagini/internal/models"
) )
func (api *API) registerRoutes() { func (api *API) registerRoutes() {
@ -45,17 +47,21 @@ func (api *API) registerRoutes() {
api.Router.HandleFunc("/api/v1/Logout", api.logoutHandler) api.Router.HandleFunc("/api/v1/Logout", api.logoutHandler)
api.Router.HandleFunc("/api/v1/Login", api.loginHandler) api.Router.HandleFunc("/api/v1/Login", api.loginHandler)
api.Router.HandleFunc("/api/v1/RefreshLogin", api.refreshLoginHandler)
} }
// https://stackoverflow.com/a/59764037 // https://stackoverflow.com/a/59764037
func errorJSON(w http.ResponseWriter, err string, code int) { func errorJSON(w http.ResponseWriter, err string, code int) {
errStruct := &models.APIResponse{Error: &models.APIError{Message: err, Code: int64(code)}}
responseJSON(w, errStruct, code)
}
func responseJSON(w http.ResponseWriter, msg interface{}, code int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(code) w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]interface{}{"error": err}) json.NewEncoder(w).Encode(msg)
} }
func successJSON(w http.ResponseWriter, msg string, code int) { func successJSON(w http.ResponseWriter, msg string, code int) {

View File

@ -35,9 +35,9 @@ func NewMgr(db *db.DBManager, c *config.Config) *AuthManager {
func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, models.User) { func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, models.User) {
// By Username // By Username
foundUser, err := auth.DB.User(models.User{Username: creds.User}) foundUser, err := auth.DB.User(&models.User{Username: creds.User})
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
foundUser, err = auth.DB.User(models.User{Email: creds.User}) foundUser, err = auth.DB.User(&models.User{Email: creds.User})
} }
// Error Checking // Error Checking
@ -67,22 +67,22 @@ func (auth *AuthManager) getRole(user models.User) string {
return "User" return "User"
} }
func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, bool) { func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, error) {
byteRefreshJWT := []byte(refreshJWT) byteRefreshJWT := []byte(refreshJWT)
// Acquire Relevant Device // Acquire Relevant Device
unverifiedToken, err := jwt.ParseBytes(byteRefreshJWT) unverifiedToken, err := jwt.ParseBytes(byteRefreshJWT)
did, ok := unverifiedToken.Get("did") did, ok := unverifiedToken.Get("did")
if !ok { if !ok {
return nil, false return nil, errors.New("did does not exist")
} }
deviceID, err := uuid.Parse(fmt.Sprintf("%v", did)) deviceID, err := uuid.Parse(fmt.Sprintf("%v", did))
if err != nil { if err != nil {
return nil, false return nil, errors.New("did does not parse")
} }
device, err := auth.DB.Device(models.Device{Base: models.Base{UUID: deviceID}}) device, err := auth.DB.Device(&models.Device{Base: models.Base{UUID: deviceID}})
if err != nil { if err != nil {
return nil, false return nil, err
} }
// Verify & Validate Token // Verify & Validate Token
@ -92,22 +92,23 @@ func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token,
) )
if err != nil { if err != nil {
fmt.Println("failed to parse payload: ", err) fmt.Println("failed to parse payload: ", err)
return nil, false return nil, err
} }
return verifiedToken, true return verifiedToken, nil
} }
func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, bool) { func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, error) {
byteAccessJWT := []byte(accessJWT) byteAccessJWT := []byte(accessJWT)
verifiedToken, err := jwt.ParseBytes(byteAccessJWT, verifiedToken, err := jwt.ParseBytes(byteAccessJWT,
jwt.WithValidate(true), jwt.WithValidate(true),
jwt.WithVerify(jwa.HS256, []byte(auth.Config.JWTSecret)), jwt.WithVerify(jwa.HS256, []byte(auth.Config.JWTSecret)),
) )
if err != nil { if err != nil {
fmt.Println("failed to parse payload: ", err) return nil, err
return nil, false
} }
return verifiedToken, true
return verifiedToken, nil
} }
func (auth *AuthManager) CreateJWTRefreshToken(user models.User, device models.Device) (string, error) { func (auth *AuthManager) CreateJWTRefreshToken(user models.User, device models.Device) (string, error) {

View File

@ -1,7 +1,9 @@
package db package db
import ( import (
"fmt"
"path" "path"
"errors"
"gorm.io/gorm" "gorm.io/gorm"
// "gorm.io/gorm/logger" // "gorm.io/gorm/logger"
@ -52,7 +54,7 @@ func NewMgr(c *config.Config) *DBManager {
func (dbm *DBManager) bootstrapDatabase() { func (dbm *DBManager) bootstrapDatabase() {
log.Info("[query] Bootstrapping database.") log.Info("[query] Bootstrapping database.")
_, err := dbm.CreateUser(models.User{ err := dbm.CreateUser(&models.User{
Username: "admin", Username: "admin",
Password: "admin", Password: "admin",
AuthType: "Local", AuthType: "Local",
@ -62,3 +64,46 @@ func (dbm *DBManager) bootstrapDatabase() {
log.Fatal("[query] Unable to bootstrap database.") log.Fatal("[query] Unable to bootstrap database.")
} }
} }
func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, error) {
// TODO:
// - Where Filters
// - Sort Filters
// - Paging Filters
objType := fmt.Sprintf("%T", dest)
if objType == "*[]models.MediaItem" {
// TODO: Validate MediaItem Type
} else {
// Return Error
return 0, errors.New("Invalid type")
}
var count int64
err := dbm.db.Find(dest).Count(&count).Error;
return count, err
// Paging:
// - Regular Pagination:
// - /api/v1/MediaItems?page[limit]=50&page=2
// - Meta Count Only
// - /api/v1/MediaItems?page[limit]=0
// Sorting:
// - Ascending Sort:
// - /api/v1/MediaItems?sort=created_at
// - Descending Sort:
// - /api/v1/MediaItems?sort=-created_at
// Filters:
// - Greater Than / Less Than (created_at, updated_at, exif_date)
// - /api/v1/MediaItems?filter[created_at]>=2020-01-01&filter[created_at]<=2021-01-01
// - Long / Lat Range (latitude, longitude)
// - /api/v1/MediaItems?filter[latitude]>=71.1827&filter[latitude]<=72.0000&filter[longitude]>=100.000&filter[longitude]<=101.0000
// - Image / Video (media_type)
// - /api/v1/MediaItems?filter[media_type]=Image
// - Tags (tags)
// - /api/v1/MediaItems?filter[tags]=id1,id2,id3
// - Albums (albums)
// - /api/v1/MediaItems?filter[albums]=id1
}

View File

@ -7,24 +7,24 @@ import (
"reichard.io/imagini/internal/models" "reichard.io/imagini/internal/models"
) )
func (dbm *DBManager) CreateDevice (device models.Device) (models.Device, error) { func (dbm *DBManager) CreateDevice (device *models.Device) error {
log.Info("[db] Creating device: ", device.Name) log.Info("[db] Creating device: ", device.Name)
device.RefreshKey = uuid.New().String() device.RefreshKey = uuid.New().String()
err := dbm.db.Create(&device).Error err := dbm.db.Create(&device).Error
return device, err return err
} }
func (dbm *DBManager) Device (device models.Device) (models.Device, error) { func (dbm *DBManager) Device (device *models.Device) (models.Device, error) {
var foundDevice models.Device var foundDevice models.Device
var count int64 var count int64
err := dbm.db.Where(&device).First(&foundDevice).Count(&count).Error err := dbm.db.Where(&device).First(&foundDevice).Count(&count).Error
return foundDevice, err return foundDevice, err
} }
func (dbm *DBManager) DeleteDevice (user models.Device) error { func (dbm *DBManager) DeleteDevice (user *models.Device) error {
return nil return nil
} }
func (dbm *DBManager) UpdateRefreshToken (device models.Device, refreshToken string) error { func (dbm *DBManager) UpdateRefreshToken (device *models.Device, refreshToken string) error {
return nil return nil
} }

View File

@ -6,30 +6,16 @@ import (
"reichard.io/imagini/internal/models" "reichard.io/imagini/internal/models"
) )
func (dbm *DBManager) CreateMediaItem (mediaItem models.MediaItem) (models.MediaItem, error) { func (dbm *DBManager) CreateMediaItem (mediaItem *models.MediaItem) error {
log.Info("[db] Creating media item: ", mediaItem.FileName) log.Info("[db] Creating media item: ", mediaItem.FileName)
err := dbm.db.Create(&mediaItem).Error err := dbm.db.Create(&mediaItem).Error
return mediaItem, err return err
} }
func (dbm *DBManager) MediaItemsFromAlbum(user models.User, album models.Album) ([]models.MediaItem, error) { func (dbm *DBManager) MediaItems(mediaItemFilter *models.MediaItem) ([]models.MediaItem, int64, error) {
var mediaItems []models.MediaItem var mediaItems []models.MediaItem
// db.Table("media_albums"). var count int64
// Select("media_item.*").
// Joins("INNER JOIN media_items ON media_albums.ID = media_items.Albums").
// Where("media_albums.album_id = ? AND media_items.User = ?", albumID, userID).
err := dbm.db.Where(&mediaItemFilter).Find(&mediaItems).Count(&count).Error;
err := dbm.db. return mediaItems, count, err
//Where("album = ? AND user = ?", albumID, userID).
Find(&mediaItems).Error
return mediaItems, err
// db.Raw(`
// SELECT
// MediaItems.*
// FROM
// MediaAlbums
// INNER JOIN MediaItems ON MediaAlbums.mediaID = MediaItems.mediaID
// WHERE MediaAlbums.albumID = ? AND MediaItems.userID = ?`, albumID, userID)
} }

View File

@ -7,19 +7,19 @@ import (
"reichard.io/imagini/internal/models" "reichard.io/imagini/internal/models"
) )
func (dbm *DBManager) CreateUser(user models.User) (models.User, error) { func (dbm *DBManager) CreateUser(user *models.User) error {
log.Info("[db] Creating user: ", user.Username) log.Info("[db] Creating user: ", user.Username)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return user, err return err
} }
user.Password = string(hashedPassword) user.Password = string(hashedPassword)
err = dbm.db.Create(&user).Error err = dbm.db.Create(&user).Error
return user, err return err
} }
func (dbm *DBManager) User (user models.User) (models.User, error) { func (dbm *DBManager) User (user *models.User) (models.User, error) {
var foundUser models.User var foundUser models.User
var count int64 var count int64
err := dbm.db.Where(&user).First(&foundUser).Count(&count).Error err := dbm.db.Where(&user).First(&foundUser).Count(&count).Error

View File

@ -5,18 +5,20 @@ type APICredentials struct {
Password string `json:"password"` Password string `json:"password"`
} }
type APIData interface{}
type APIMeta struct { type APIMeta struct {
Count int `json:"count"` Count int64 `json:"count"`
Page int `json:"page"` Page int64 `json:"page"`
} }
type APIError struct { type APIError struct {
Message string `json:"message"` Message string `json:"message"`
Code int `json:"code"` Code int64 `json:"code"`
} }
type APIResponse struct { type APIResponse struct {
Data []interface{} `json:"data"` Data APIData `json:"data,omitempty"`
Meta APIMeta `json:"meta"` Meta *APIMeta `json:"meta,omitempty"`
Error APIError `json:"error"` Error *APIError `json:"error,omitempty"`
} }

View File

@ -2,16 +2,17 @@ package models
import ( import (
"time" "time"
"strings"
"reflect"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/google/uuid" "github.com/google/uuid"
) )
// Base contains common columns for all tables.
type Base struct { type Base struct {
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;primarykey"` UUID uuid.UUID `json:"uuid" gorm:"type:uuid;primarykey"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
} }
func (base *Base) BeforeCreate(tx *gorm.DB) (err error) { func (base *Base) BeforeCreate(tx *gorm.DB) (err error) {
@ -28,7 +29,8 @@ type ServerSetting struct {
type Device struct { type Device struct {
Base Base
User User `json:"user" gorm:"ForeignKey:UUID;not null"` // User UUID UserUUID uuid.UUID `json:"-" gorm:"not null"`
User User `json:"user" gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User
Name string `json:"name" gorm:"not null"` // Name of Device Name string `json:"name" gorm:"not null"` // Name of Device
Type string `json:"type" gorm:"not null"` // Android, iOS, Chrome, FireFox, Edge Type string `json:"type" gorm:"not null"` // Android, iOS, Chrome, FireFox, Edge
RefreshKey string `json:"-"` // Device Specific Refresh Key RefreshKey string `json:"-"` // Device Specific Refresh Key
@ -47,10 +49,11 @@ type User struct {
type MediaItem struct { type MediaItem struct {
Base Base
User User `json:"user" gorm:"ForeignKey:UUID;not null"` // User UUID UserUUID uuid.UUID `json:"-" gorm:"not null"`
User User `json:"-" gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User
EXIFDate time.Time `json:"exif_date"` // EXIF Date EXIFDate time.Time `json:"exif_date"` // EXIF Date
Latitude float32 `json:"latitude" gorm:"type:decimal(10,2)"` // Decimal Latitude Latitude *float32 `json:"latitude" gorm:"type:decimal(10,2)"` // Decimal Latitude
Longitude float32 `json:"longitude" gorm:"type:decimal(10,2)"` // Decimal Longitude Longitude *float32 `json:"longitude" gorm:"type:decimal(10,2)"` // Decimal Longitude
MediaType string `json:"media_type" gorm:"default:Image;not null"` // Image, Video MediaType string `json:"media_type" gorm:"default:Image;not null"` // Image, Video
OrigName string `json:"orig_name" gorm:"not null"` // Original Name OrigName string `json:"orig_name" gorm:"not null"` // Original Name
FileName string `json:"file_name" gorm:"not null"` // File Name FileName string `json:"file_name" gorm:"not null"` // File Name
@ -67,3 +70,27 @@ type Album struct {
Base Base
Name string `json:"name" gorm:"not null"` // Album Name Name string `json:"name" gorm:"not null"` // Album Name
} }
func JSONFields(model interface{}) map[string]struct{} {
jsonFields := make(map[string]struct{})
val := reflect.ValueOf(model)
t := val.Type()
for i := 0; i < t.NumField(); i++ {
jsonField := strings.TrimSpace(t.Field(i).Tag.Get("json"))
if jsonField == "" {
continue
}
jsonSplit := strings.Split(jsonField, ",")
fieldVal := strings.TrimSpace(jsonSplit[0])
if fieldVal == "" || fieldVal == "-" {
continue
}
jsonFields[fieldVal] = struct{}{}
}
return jsonFields
}