Pre graphql

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

View File

@@ -14,6 +14,7 @@ import (
)
func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
if r.Method != http.MethodPost {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
return
@@ -77,72 +78,21 @@ func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) {
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.
**/
func (api *API) upsertRequestedDevice(user models.User, r *http.Request) (models.Device, error) {
requestedDevice := deriveRequestedDevice(r)
requestedDevice.Type = deriveDeviceType(r)
requestedDevice.User = user
requestedDevice.UserUUID = user.UUID
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
}
foundDevice, err := api.DB.Device(models.Device{
foundDevice, err := api.DB.Device(&models.Device{
Base: models.Base{ UUID: requestedDevice.UUID },
User: user,
})
@@ -213,6 +163,60 @@ func deriveRequestedDevice(r *http.Request) models.Device {
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 {
if len(s) >= 2 {
if s[0] == '"' && s[len(s)-1] == '"' {

View File

@@ -1,6 +1,7 @@
package api
import (
"os"
"path"
"net/http"
)
@@ -17,6 +18,17 @@ func (api *API) mediaHandler(w http.ResponseWriter, r *http.Request) {
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
reqInfo := r.Context().Value("uuids").(map[string]string)
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)
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)
}

View File

@@ -3,20 +3,25 @@ package api
import (
"io"
"os"
"time"
"path"
"fmt"
"path"
"time"
"regexp"
"strings"
"errors"
"net/url"
"net/http"
"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"
"encoding/json"
"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
// - /api/v1/MediaItems/<GUID>
// - JSON Struct
@@ -34,14 +39,88 @@ func (api *API) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
// 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)
// 64MB limit (TODO: Change this - video)
r.ParseMultipartForm(64 << 20)
// Open form file
@@ -116,13 +195,13 @@ func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
// Add Additional MediaItem Fields
mediaItem.Base.UUID = mediaItemUUID
mediaItem.User.UUID, err = uuid.Parse(uid)
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)
err = api.DB.CreateMediaItem(mediaItem)
if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
@@ -131,14 +210,14 @@ func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
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)
entries, _, err := exif.GetFlatExifData(rawExif, nil)
decLong := float32(1)
decLat := float32(1)
var mediaItem models.MediaItem
mediaItem := &models.MediaItem{}
for _, v := range entries {
if v.TagName == "DateTimeOriginal" {
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.Longitude = decLong
mediaItem.Latitude = &decLat
mediaItem.Longitude = &decLong
return mediaItem, err
}
@@ -173,3 +252,76 @@ func mediaItemFromEXIFData(filePath string) (models.MediaItem, error) {
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

@@ -22,33 +22,45 @@ func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc {
func (api *API) authMiddleware(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Acquire Token
accessCookie, err := r.Cookie("AccessToken")
if err != nil {
log.Warn("[middleware] AccessToken not found")
w.WriteHeader(http.StatusUnauthorized)
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
return
}
// Validate JWT Tokens
accessToken, accessOK := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
accessToken, err := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
if accessOK {
// 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)
} else {
w.WriteHeader(http.StatusUnauthorized)
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
}
// 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)
})
}

View File

@@ -3,6 +3,8 @@ package api
import (
"encoding/json"
"net/http"
"reichard.io/imagini/internal/models"
)
func (api *API) registerRoutes() {
@@ -43,19 +45,23 @@ func (api *API) registerRoutes() {
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Logout", api.logoutHandler)
api.Router.HandleFunc("/api/v1/Login", api.loginHandler)
api.Router.HandleFunc("/api/v1/RefreshLogin", api.refreshLoginHandler)
api.Router.HandleFunc("/api/v1/Logout", api.logoutHandler)
api.Router.HandleFunc("/api/v1/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)
}
func responseJSON(w http.ResponseWriter, msg interface{}, 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})
json.NewEncoder(w).Encode(msg)
}
func successJSON(w http.ResponseWriter, msg string, code int) {