Pre graphql
This commit is contained in:
parent
dc56899b8b
commit
ecf981495e
1
go.mod
1
go.mod
@ -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
2
go.sum
@ -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=
|
||||||
|
@ -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] == '"' {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -3,20 +3,25 @@ package api
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
"time"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
"net/http"
|
"net/http"
|
||||||
"github.com/google/uuid"
|
"encoding/json"
|
||||||
"github.com/dsoprea/go-exif/v3"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/gabriel-vasile/mimetype"
|
|
||||||
"github.com/dsoprea/go-exif/v3/common"
|
|
||||||
|
|
||||||
"reichard.io/imagini/internal/models"
|
"github.com/google/uuid"
|
||||||
|
"github.com/dsoprea/go-exif/v3"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"github.com/dsoprea/go-exif/v3/common"
|
||||||
|
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
@ -22,33 +22,45 @@ 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 accessOK {
|
if err != nil && err.Error() == "exp not satisfied" {
|
||||||
// Acquire UserID and DeviceID
|
log.Info("[middleware] Refreshing AccessToken")
|
||||||
reqInfo := make(map[string]string)
|
accessToken, err = api.refreshAccessToken(w, r)
|
||||||
uid, _ := accessToken.Get("sub")
|
if err != nil {
|
||||||
did, _ := accessToken.Get("did")
|
log.Warn("[middleware] Refreshing AccessToken failed: ", err)
|
||||||
reqInfo["uid"] = uid.(string)
|
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
|
||||||
reqInfo["did"] = did.(string)
|
return
|
||||||
|
}
|
||||||
// Add context
|
log.Info("[middleware] AccessToken Refreshed")
|
||||||
ctx := context.WithValue(r.Context(), "uuids", reqInfo)
|
} else if err != nil {
|
||||||
sr := r.WithContext(ctx)
|
log.Warn("[middleware] AccessToken failed to validate")
|
||||||
|
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
|
||||||
next.ServeHTTP(w, sr)
|
return
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Acquire UserID and DeviceID
|
||||||
|
reqInfo := make(map[string]string)
|
||||||
|
uid, _ := accessToken.Get("sub")
|
||||||
|
did, _ := accessToken.Get("did")
|
||||||
|
reqInfo["uid"] = uid.(string)
|
||||||
|
reqInfo["did"] = did.(string)
|
||||||
|
|
||||||
|
// Add context
|
||||||
|
ctx := context.WithValue(r.Context(), "uuids", reqInfo)
|
||||||
|
sr := r.WithContext(ctx)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, sr)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
@ -43,19 +45,23 @@ func (api *API) registerRoutes() {
|
|||||||
api.authMiddleware,
|
api.authMiddleware,
|
||||||
))
|
))
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
@ -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,10 +29,11 @@ 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"`
|
||||||
Name string `json:"name" gorm:"not null"` // Name of Device
|
User User `json:"user" gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User
|
||||||
Type string `json:"type" gorm:"not null"` // Android, iOS, Chrome, FireFox, Edge
|
Name string `json:"name" gorm:"not null"` // Name of Device
|
||||||
RefreshKey string `json:"-"` // Device Specific Refresh Key
|
Type string `json:"type" gorm:"not null"` // Android, iOS, Chrome, FireFox, Edge
|
||||||
|
RefreshKey string `json:"-"` // Device Specific Refresh Key
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@ -47,15 +49,16 @@ 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"`
|
||||||
EXIFDate time.Time `json:"exif_date"` // EXIF Date
|
User User `json:"-" gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User
|
||||||
Latitude float32 `json:"latitude" gorm:"type:decimal(10,2)"` // Decimal Latitude
|
EXIFDate time.Time `json:"exif_date"` // EXIF Date
|
||||||
Longitude float32 `json:"longitude" gorm:"type:decimal(10,2)"` // Decimal Longitude
|
Latitude *float32 `json:"latitude" gorm:"type:decimal(10,2)"` // Decimal Latitude
|
||||||
MediaType string `json:"media_type" gorm:"default:Image;not null"` // Image, Video
|
Longitude *float32 `json:"longitude" gorm:"type:decimal(10,2)"` // Decimal Longitude
|
||||||
OrigName string `json:"orig_name" gorm:"not null"` // Original Name
|
MediaType string `json:"media_type" gorm:"default:Image;not null"` // Image, Video
|
||||||
FileName string `json:"file_name" gorm:"not null"` // File Name
|
OrigName string `json:"orig_name" gorm:"not null"` // Original Name
|
||||||
Tags []Tag `json:"tags" gorm:"many2many:media_tags;"` // Associated Tag UUIDs
|
FileName string `json:"file_name" gorm:"not null"` // File Name
|
||||||
Albums []Album `json:"albums" gorm:"many2many:media_albums;"` // Associated Album UUIDs
|
Tags []Tag `json:"tags" gorm:"many2many:media_tags;"` // Associated Tag UUIDs
|
||||||
|
Albums []Album `json:"albums" gorm:"many2many:media_albums;"` // Associated Album UUIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user