GraphQL Framework
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) albumsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) devicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) infoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) tagsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) uploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
|
||||
"reichard.io/imagini/internal/db"
|
||||
"reichard.io/imagini/internal/config"
|
||||
|
||||
graphql "reichard.io/imagini/graph/model"
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/internal/session"
|
||||
)
|
||||
@@ -33,11 +35,16 @@ func NewMgr(db *db.DBManager, c *config.Config) *AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, models.User) {
|
||||
// By Username
|
||||
foundUser, err := auth.DB.User(&models.User{Username: creds.User})
|
||||
func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, graphql.User) {
|
||||
// Search Objects
|
||||
userByName := &graphql.User{}
|
||||
userByName.Username = creds.User
|
||||
|
||||
foundUser, err := auth.DB.User(userByName)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
foundUser, err = auth.DB.User(&models.User{Email: creds.User})
|
||||
userByEmail := &graphql.User{}
|
||||
userByEmail.Email = creds.User
|
||||
foundUser, err = auth.DB.User(userByEmail)
|
||||
}
|
||||
|
||||
// Error Checking
|
||||
@@ -62,7 +69,7 @@ func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, mo
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *AuthManager) getRole(user models.User) string {
|
||||
func (auth *AuthManager) getRole(user graphql.User) string {
|
||||
// TODO: Lookup role of user
|
||||
return "User"
|
||||
}
|
||||
@@ -80,7 +87,8 @@ func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token,
|
||||
if err != nil {
|
||||
return nil, errors.New("did does not parse")
|
||||
}
|
||||
device, err := auth.DB.Device(&models.Device{Base: models.Base{UUID: deviceID}})
|
||||
stringDeviceID := deviceID.String()
|
||||
device, err := auth.DB.Device(&graphql.Device{ID: &stringDeviceID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -88,7 +96,7 @@ func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token,
|
||||
// Verify & Validate Token
|
||||
verifiedToken, err := jwt.ParseBytes(byteRefreshJWT,
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithVerify(jwa.HS256, []byte(device.RefreshKey)),
|
||||
jwt.WithVerify(jwa.HS256, []byte(*device.RefreshKey)),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println("failed to parse payload: ", err)
|
||||
@@ -111,17 +119,17 @@ func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, er
|
||||
return verifiedToken, nil
|
||||
}
|
||||
|
||||
func (auth *AuthManager) CreateJWTRefreshToken(user models.User, device models.Device) (string, error) {
|
||||
func (auth *AuthManager) CreateJWTRefreshToken(user graphql.User, device graphql.Device) (string, error) {
|
||||
// Acquire Refresh Key
|
||||
byteKey := []byte(device.RefreshKey)
|
||||
byteKey := []byte(*device.RefreshKey)
|
||||
|
||||
// Create New Token
|
||||
tm := time.Now()
|
||||
t := jwt.New()
|
||||
t.Set(`did`, device.UUID.String()) // Device ID
|
||||
t.Set(jwt.SubjectKey, user.UUID.String()) // User ID
|
||||
t.Set(jwt.AudienceKey, `imagini`) // App ID
|
||||
t.Set(jwt.IssuedAtKey, tm) // Issued At
|
||||
t.Set(`did`, device.ID) // Device ID
|
||||
t.Set(jwt.SubjectKey, user.ID) // User ID
|
||||
t.Set(jwt.AudienceKey, `imagini`) // App ID
|
||||
t.Set(jwt.IssuedAtKey, tm) // Issued At
|
||||
|
||||
// iOS & Android = Never Expiring Refresh Token
|
||||
if device.Type != "iOS" && device.Type != "Android" {
|
||||
@@ -146,16 +154,16 @@ func (auth *AuthManager) CreateJWTRefreshToken(user models.User, device models.D
|
||||
return string(signed), nil
|
||||
}
|
||||
|
||||
func (auth *AuthManager) CreateJWTAccessToken(user models.User, device models.Device) (string, error) {
|
||||
func (auth *AuthManager) CreateJWTAccessToken(user graphql.User, device graphql.Device) (string, error) {
|
||||
// Create New Token
|
||||
tm := time.Now()
|
||||
t := jwt.New()
|
||||
t.Set(`did`, device.UUID.String()) // Device ID
|
||||
t.Set(`role`, auth.getRole(user)) // User Role (Admin / User)
|
||||
t.Set(jwt.SubjectKey, user.UUID.String()) // User ID
|
||||
t.Set(jwt.AudienceKey, `imagini`) // App ID
|
||||
t.Set(jwt.IssuedAtKey, tm) // Issued At
|
||||
t.Set(jwt.ExpirationKey, tm.Add(time.Hour * 2)) // 2 Hour Access Key
|
||||
t.Set(`did`, device.ID) // Device ID
|
||||
t.Set(`role`, auth.getRole(user)) // User Role (Admin / User)
|
||||
t.Set(jwt.SubjectKey, user.ID) // User ID
|
||||
t.Set(jwt.AudienceKey, `imagini`) // App ID
|
||||
t.Set(jwt.IssuedAtKey, tm) // Issued At
|
||||
t.Set(jwt.ExpirationKey, tm.Add(time.Hour * 2)) // 2 Hour Access Key
|
||||
|
||||
// Validate Token Creation
|
||||
_, err := json.MarshalIndent(t, "", " ")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func authenticateLDAPUser(user models.User, pw string) bool {
|
||||
func authenticateLDAPUser(user model.User, pw string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package auth
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func authenticateLocalUser(user models.User, pw string) bool {
|
||||
func authenticateLocalUser(user model.User, pw string) bool {
|
||||
bPassword :=[]byte(pw)
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), bPassword)
|
||||
err := bcrypt.CompareHashAndPassword([]byte(*user.Password), bPassword)
|
||||
if err == nil {
|
||||
log.Info("[auth] Authentication successfull: ", user.Username)
|
||||
return true
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"reichard.io/imagini/internal/config"
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
type DBManager struct {
|
||||
@@ -35,16 +35,15 @@ func NewMgr(c *config.Config) *DBManager {
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
dbm.db.AutoMigrate(&models.ServerSetting{})
|
||||
dbm.db.AutoMigrate(&models.Device{})
|
||||
dbm.db.AutoMigrate(&models.User{})
|
||||
dbm.db.AutoMigrate(&models.MediaItem{})
|
||||
dbm.db.AutoMigrate(&models.Tag{})
|
||||
dbm.db.AutoMigrate(&models.Album{})
|
||||
dbm.db.AutoMigrate(&model.Device{})
|
||||
dbm.db.AutoMigrate(&model.User{})
|
||||
dbm.db.AutoMigrate(&model.MediaItem{})
|
||||
dbm.db.AutoMigrate(&model.Tag{})
|
||||
dbm.db.AutoMigrate(&model.Album{})
|
||||
|
||||
// Determine whether to bootstrap
|
||||
var count int64
|
||||
dbm.db.Model(&models.User{}).Count(&count)
|
||||
dbm.db.Model(&model.User{}).Count(&count)
|
||||
if count == 0 {
|
||||
dbm.bootstrapDatabase()
|
||||
}
|
||||
@@ -54,17 +53,23 @@ func NewMgr(c *config.Config) *DBManager {
|
||||
|
||||
func (dbm *DBManager) bootstrapDatabase() {
|
||||
log.Info("[query] Bootstrapping database.")
|
||||
err := dbm.CreateUser(&models.User{
|
||||
|
||||
password := "admin"
|
||||
user := &model.User{
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
AuthType: "Local",
|
||||
})
|
||||
Password: &password,
|
||||
}
|
||||
|
||||
err := dbm.CreateUser(user)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("[query] Unable to bootstrap database.")
|
||||
}
|
||||
}
|
||||
|
||||
// func (dmb *DBManager) {}
|
||||
|
||||
func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, error) {
|
||||
// TODO:
|
||||
// - Where Filters
|
||||
@@ -72,7 +77,7 @@ func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, erro
|
||||
// - Paging Filters
|
||||
|
||||
objType := fmt.Sprintf("%T", dest)
|
||||
if objType == "*[]models.MediaItem" {
|
||||
if objType == "*[]model.MediaItem" {
|
||||
// TODO: Validate MediaItem Type
|
||||
} else {
|
||||
// Return Error
|
||||
|
||||
@@ -4,27 +4,28 @@ import (
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (dbm *DBManager) CreateDevice (device *models.Device) error {
|
||||
func (dbm *DBManager) CreateDevice (device *model.Device) error {
|
||||
log.Info("[db] Creating device: ", device.Name)
|
||||
device.RefreshKey = uuid.New().String()
|
||||
refreshKey := uuid.New().String()
|
||||
device.RefreshKey = &refreshKey
|
||||
err := dbm.db.Create(&device).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) Device (device *models.Device) (models.Device, error) {
|
||||
var foundDevice models.Device
|
||||
func (dbm *DBManager) Device (device *model.Device) (model.Device, error) {
|
||||
var foundDevice model.Device
|
||||
var count int64
|
||||
err := dbm.db.Where(&device).First(&foundDevice).Count(&count).Error
|
||||
return foundDevice, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) DeleteDevice (user *models.Device) error {
|
||||
func (dbm *DBManager) DeleteDevice (user *model.Device) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbm *DBManager) UpdateRefreshToken (device *models.Device, refreshToken string) error {
|
||||
func (dbm *DBManager) UpdateRefreshToken (device *model.Device, refreshToken string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@ package db
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (dbm *DBManager) CreateMediaItem (mediaItem *models.MediaItem) error {
|
||||
func (dbm *DBManager) CreateMediaItem (mediaItem *model.MediaItem) error {
|
||||
log.Info("[db] Creating media item: ", mediaItem.FileName)
|
||||
err := dbm.db.Create(&mediaItem).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) MediaItems(mediaItemFilter *models.MediaItem) ([]models.MediaItem, int64, error) {
|
||||
var mediaItems []models.MediaItem
|
||||
func (dbm *DBManager) MediaItems(mediaItemFilter *model.MediaItem) ([]model.MediaItem, int64, error) {
|
||||
var mediaItems []model.MediaItem
|
||||
var count int64
|
||||
|
||||
err := dbm.db.Where(&mediaItemFilter).Find(&mediaItems).Count(&count).Error;
|
||||
|
||||
@@ -4,32 +4,33 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (dbm *DBManager) CreateUser(user *models.User) error {
|
||||
func (dbm *DBManager) CreateUser(user *model.User) error {
|
||||
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 {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
user.Password = string(hashedPassword)
|
||||
stringHashedPassword := string(hashedPassword)
|
||||
user.Password = &stringHashedPassword
|
||||
err = dbm.db.Create(&user).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) User (user *models.User) (models.User, error) {
|
||||
var foundUser models.User
|
||||
func (dbm *DBManager) User (user *model.User) (model.User, error) {
|
||||
var foundUser model.User
|
||||
var count int64
|
||||
err := dbm.db.Where(&user).First(&foundUser).Count(&count).Error
|
||||
return foundUser, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) DeleteUser (user models.User) error {
|
||||
func (dbm *DBManager) DeleteUser (user model.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbm *DBManager) UpdatePassword (user models.User, pw string) {
|
||||
func (dbm *DBManager) UpdatePassword (user model.User, pw string) {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"strings"
|
||||
"reflect"
|
||||
"gorm.io/gorm"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;primarykey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
func (base *Base) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
base.UUID = uuid.New()
|
||||
return
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
Base
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Description string `json:"description" gorm:"not null"`
|
||||
Value string `json:"value" gorm:"not null"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
Base
|
||||
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
|
||||
Type string `json:"type" gorm:"not null"` // Android, iOS, Chrome, FireFox, Edge
|
||||
RefreshKey string `json:"-"` // Device Specific Refresh Key
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Base
|
||||
Email string `json:"email" gorm:"unique"` // Email
|
||||
Username string `json:"username" gorm:"unique"` // Username
|
||||
FirstName string `json:"first_name"` // First Name
|
||||
LastName string `json:"last_name"` // Last Name
|
||||
Role string `json:"role"` // Role
|
||||
AuthType string `json:"auth_type" gorm:"default:Local;not null"` // Auth Type (E.g. Local, LDAP)
|
||||
Password string `json:"-"` // Hased & Salted Password
|
||||
}
|
||||
|
||||
type MediaItem struct {
|
||||
Base
|
||||
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
|
||||
Latitude *float32 `json:"latitude" gorm:"type:decimal(10,2)"` // Decimal Latitude
|
||||
Longitude *float32 `json:"longitude" gorm:"type:decimal(10,2)"` // Decimal Longitude
|
||||
MediaType string `json:"media_type" gorm:"default:Image;not null"` // Image, Video
|
||||
OrigName string `json:"orig_name" gorm:"not null"` // Original Name
|
||||
FileName string `json:"file_name" gorm:"not null"` // File Name
|
||||
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 {
|
||||
Base
|
||||
Name string `json:"name" gorm:"not null"` // Tag Name
|
||||
}
|
||||
|
||||
type Album struct {
|
||||
Base
|
||||
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