GraphQL Framework

This commit is contained in:
2021-02-02 15:34:10 -05:00
parent ecf981495e
commit 7e6454c593
28 changed files with 10764 additions and 616 deletions

View File

@@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) albumsHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/lestrrat-go/jwx/jwt"
"reichard.io/imagini/internal/models"
graphql "reichard.io/imagini/graph/model"
)
func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
@@ -81,47 +82,47 @@ func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) {
/**
* 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 graphql.User, r *http.Request) (graphql.Device, error) {
requestedDevice := deriveRequestedDevice(r)
requestedDevice.Type = deriveDeviceType(r)
requestedDevice.UserUUID = user.UUID
requestedDevice.User.ID = user.ID
if requestedDevice.UUID == uuid.Nil {
if *requestedDevice.ID == "" {
err := api.DB.CreateDevice(&requestedDevice)
createdDevice, err := api.DB.Device(&requestedDevice)
return createdDevice, err
}
foundDevice, err := api.DB.Device(&models.Device{
Base: models.Base{ UUID: requestedDevice.UUID },
User: user,
foundDevice, err := api.DB.Device(&graphql.Device{
ID: requestedDevice.ID,
User: &user,
})
return foundDevice, err
}
func deriveDeviceType(r *http.Request) string {
func deriveDeviceType(r *http.Request) graphql.DeviceType {
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
if strings.HasPrefix(userAgent, "ios-imagini"){
return "iOS"
return graphql.DeviceTypeIOs
} else if strings.HasPrefix(userAgent, "android-imagini"){
return "Android"
return graphql.DeviceTypeAndroid
} else if strings.HasPrefix(userAgent, "chrome"){
return "Chrome"
return graphql.DeviceTypeChrome
} else if strings.HasPrefix(userAgent, "firefox"){
return "Firefox"
return graphql.DeviceTypeFirefox
} else if strings.HasPrefix(userAgent, "msie"){
return "Internet Explorer"
return graphql.DeviceTypeInternetExplorer
} else if strings.HasPrefix(userAgent, "edge"){
return "Edge"
return graphql.DeviceTypeEdge
} else if strings.HasPrefix(userAgent, "safari"){
return "Safari"
return graphql.DeviceTypeSafari
}
return "Unknown"
return graphql.DeviceTypeUnknown
}
func deriveRequestedDevice(r *http.Request) models.Device {
deviceSkeleton := models.Device{}
func deriveRequestedDevice(r *http.Request) graphql.Device {
deviceSkeleton := graphql.Device{}
authHeader := r.Header.Get("X-Imagini-Authorization")
splitAuthInfo := strings.Split(authHeader, ",")
@@ -137,19 +138,20 @@ func deriveRequestedDevice(r *http.Request) models.Device {
// Derive Key
key := strings.ToLower(strings.TrimSpace(splitItem[0]))
if key != "deviceuuid" && key != "devicename" {
if key != "deviceid" && key != "devicename" {
continue
}
// Derive Value
val := trimQuotes(strings.TrimSpace(splitItem[1]))
if key == "deviceuuid" {
if key == "deviceid" {
parsedDeviceUUID, err := uuid.Parse(val)
if err != nil {
log.Warn("[auth] deriveRequestedDevice - Unable to parse requested DeviceUUID: ", val)
continue
}
deviceSkeleton.Base = models.Base{UUID: parsedDeviceUUID}
stringDeviceUUID := parsedDeviceUUID.String()
deviceSkeleton.ID = &stringDeviceUUID
} else if key == "devicename" {
deviceSkeleton.Name = val
}
@@ -157,7 +159,7 @@ func deriveRequestedDevice(r *http.Request) models.Device {
// If name not set, set to type
if deviceSkeleton.Name == "" {
deviceSkeleton.Name = deviceSkeleton.Type
deviceSkeleton.Name = deviceSkeleton.Type.String()
}
return deviceSkeleton
@@ -196,9 +198,12 @@ func (api *API) refreshAccessToken(w http.ResponseWriter, r *http.Request) (jwt.
return nil, err
}
stringUserUUID := userUUID.String()
stringDeviceUUID := deviceUUID.String()
// Device & User Skeleton
user := models.User{Base: models.Base{UUID: userUUID}}
device := models.Device{Base: models.Base{UUID: deviceUUID}}
user := graphql.User{ID: &stringUserUUID}
device := graphql.Device{ID: &stringDeviceUUID}
// Update token
accessTokenString, err := api.Auth.CreateJWTAccessToken(user, device)

View File

@@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) devicesHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) infoHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@@ -1,327 +0,0 @@
package api
import (
"io"
"os"
"fmt"
"path"
"time"
"regexp"
"strings"
"errors"
"net/url"
"net/http"
"encoding/json"
"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
// - /api/v1/MediaItems/<GUID>
// - JSON Struct
// - /api/v1/MediaItems/<GUID>/content
// - The raw file
func (api *API) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
// CREATE
api.mediaItemPOSTHandler(w, r)
} 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
api.mediaItemGETHandler(w, r)
} else {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
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) {
// 64MB limit (TODO: Change this - video)
r.ParseMultipartForm(64 << 20)
// Open form file
formFile, multipartFileHeader, err := r.FormFile("file")
if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
defer formFile.Close()
// File header placeholder
fileHeader := make([]byte, 64)
// Copy headers into the buffer
if _, err := formFile.Read(fileHeader); err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
// Reset position
if _, err := formFile.Seek(0, 0); err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
// Determine media type
fileMime := mimetype.Detect(fileHeader)
contentType := fileMime.String()
var mediaType string
if strings.HasPrefix(contentType, "image/") {
mediaType = "Image"
} else if strings.HasPrefix(contentType, "video/") {
mediaType = "Video"
} else {
errorJSON(w, "Invalid filetype.", http.StatusUnsupportedMediaType)
return
}
// Pull out UUIDs
reqInfo := r.Context().Value("uuids").(map[string]string)
uid := reqInfo["uid"]
// Derive Folder & File Path
mediaItemUUID := uuid.New()
fileName := mediaItemUUID.String() + fileMime.Extension()
folderPath := path.Join("/" + api.Config.DataPath + "/media/" + uid)
os.MkdirAll(folderPath, 0700)
filePath := path.Join(folderPath + "/" + fileName)
// Create File
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
log.Warn("[api] createMediaItem - Unable to open file: ", filePath)
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
defer f.Close()
// Copy data to file
_, err = io.Copy(f, formFile)
if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
// Create MediaItem From EXIF Data
mediaItem, err := mediaItemFromEXIFData(filePath)
if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
// Add Additional MediaItem Fields
mediaItem.Base.UUID = mediaItemUUID
mediaItem.UserUUID, err = uuid.Parse(uid)
mediaItem.MediaType = mediaType
mediaItem.FileName = fileName
mediaItem.OrigName = multipartFileHeader.Filename
// Create MediaItem in DB
err = api.DB.CreateMediaItem(mediaItem)
if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
successJSON(w, "Upload succeeded.", http.StatusCreated)
}
func mediaItemFromEXIFData(filePath string) (*models.MediaItem, error) {
rawExif, err := exif.SearchFileAndExtractExif(filePath)
entries, _, err := exif.GetFlatExifData(rawExif, nil)
decLong := float32(1)
decLat := float32(1)
mediaItem := &models.MediaItem{}
for _, v := range entries {
if v.TagName == "DateTimeOriginal" {
formattedTime, _ := time.Parse("2006:01:02 15:04:05", v.Formatted)
mediaItem.EXIFDate = formattedTime
} else if v.TagName == "GPSLatitude" {
latStruct := v.Value.([]exifcommon.Rational)
decLat *= deriveDecimalCoordinate(
latStruct[0].Numerator / latStruct[0].Denominator,
latStruct[1].Numerator / latStruct[1].Denominator,
float32(latStruct[2].Numerator) / float32(latStruct[2].Denominator),
)
} else if v.TagName == "GPSLongitude" {
longStruct := v.Value.([]exifcommon.Rational)
decLong *= deriveDecimalCoordinate(
longStruct[0].Numerator / longStruct[0].Denominator,
longStruct[1].Numerator / longStruct[1].Denominator,
float32(longStruct[2].Numerator) / float32(longStruct[2].Denominator),
)
} else if v.TagName == "GPSLatitudeRef" && v.Formatted == "S" {
decLat *= -1
} else if v.TagName == "GPSLongitudeRef" && v.Formatted == "W" {
decLong *= -1
}
}
mediaItem.Latitude = &decLat
mediaItem.Longitude = &decLong
return mediaItem, err
}
func deriveDecimalCoordinate(degrees, minutes uint32, seconds float32) float32 {
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

@@ -5,53 +5,27 @@ import (
"net/http"
"reichard.io/imagini/internal/models"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"reichard.io/imagini/graph"
"reichard.io/imagini/graph/generated"
)
func (api *API) registerRoutes() {
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
// TODO: Provide Authentication for srv
api.Router.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
api.Router.Handle("/query", srv)
api.Router.HandleFunc("/media/", multipleMiddleware(
api.mediaHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/MediaItems", multipleMiddleware(
api.mediaItemsHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Devices", multipleMiddleware(
api.devicesHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Upload", multipleMiddleware(
api.uploadHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Albums", multipleMiddleware(
api.albumsHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Users", multipleMiddleware(
api.usersHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Tags", multipleMiddleware(
api.tagsHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Info", multipleMiddleware(
api.infoHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Me", multipleMiddleware(
api.meHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Logout", api.logoutHandler)
api.Router.HandleFunc("/api/v1/Login", api.loginHandler)
api.Router.HandleFunc("/logout", api.logoutHandler)
api.Router.HandleFunc("/login", api.loginHandler)
}
// https://stackoverflow.com/a/59764037
func errorJSON(w http.ResponseWriter, err string, code int) {
errStruct := &models.APIResponse{Error: &models.APIError{Message: err, Code: int64(code)}}
responseJSON(w, errStruct, code)

View File

@@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) tagsHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) uploadHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@@ -1,30 +0,0 @@
package api
import (
"net/http"
// log "github.com/sirupsen/logrus"
)
func (api *API) 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 {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
return
}
}
func (api *API) meHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
return
}
}