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 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/codeon/govips v0.0.0-20200329201227-415341c0ce33 // indirect | ||||
| 	github.com/dgrijalva/jwt-go v3.2.0+incompatible | ||||
| 	github.com/disintegration/imaging v1.6.2 // indirect | ||||
| 	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/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/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/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= | ||||
| 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) { | ||||
|     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] == '"' { | ||||
|  | ||||
| @ -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) | ||||
| } | ||||
|  | ||||
| @ -3,10 +3,16 @@ package api | ||||
| import ( | ||||
| 	"io" | ||||
| 	"os" | ||||
|     "time" | ||||
| 	"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" | ||||
| @ -16,7 +22,6 @@ import ( | ||||
| 	"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 | ||||
| } | ||||
|  | ||||
| @ -22,18 +22,33 @@ 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 err != nil && err.Error() == "exp not satisfied" { | ||||
|             log.Info("[middleware] Refreshing AccessToken") | ||||
|             accessToken, err = api.refreshAccessToken(w, r) | ||||
|             if err != nil { | ||||
|                 log.Warn("[middleware] Refreshing AccessToken failed: ", err) | ||||
|                 errorJSON(w, "Invalid token.", http.StatusUnauthorized) | ||||
|                 return | ||||
|             } | ||||
|             log.Info("[middleware] AccessToken Refreshed") | ||||
|         } else if err != nil { | ||||
|             log.Warn("[middleware] AccessToken failed to validate") | ||||
|             errorJSON(w, "Invalid token.", http.StatusUnauthorized) | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         if accessOK { | ||||
|         // Acquire UserID and DeviceID | ||||
|         reqInfo := make(map[string]string) | ||||
|         uid, _ := accessToken.Get("sub") | ||||
| @ -46,9 +61,6 @@ func (api *API) authMiddleware(next http.Handler) http.HandlerFunc { | ||||
|         sr := r.WithContext(ctx) | ||||
| 
 | ||||
|         next.ServeHTTP(w, sr) | ||||
|         } else { | ||||
|             w.WriteHeader(http.StatusUnauthorized) | ||||
|         } | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -3,6 +3,8 @@ package api | ||||
| import ( | ||||
|     "encoding/json" | ||||
|     "net/http" | ||||
| 
 | ||||
|     "reichard.io/imagini/internal/models" | ||||
| ) | ||||
| 
 | ||||
| func (api *API) registerRoutes() { | ||||
| @ -45,17 +47,21 @@ func (api *API) registerRoutes() { | ||||
| 
 | ||||
|     api.Router.HandleFunc("/api/v1/Logout", api.logoutHandler) | ||||
|     api.Router.HandleFunc("/api/v1/Login",  api.loginHandler) | ||||
|     api.Router.HandleFunc("/api/v1/RefreshLogin",  api.refreshLoginHandler) | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| // 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) { | ||||
|  | ||||
| @ -35,9 +35,9 @@ 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}) | ||||
|     foundUser, err := auth.DB.User(&models.User{Username: creds.User}) | ||||
|     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 | ||||
| @ -67,22 +67,22 @@ func (auth *AuthManager) getRole(user models.User) string { | ||||
|     return "User" | ||||
| } | ||||
| 
 | ||||
| func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, bool) { | ||||
| func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, error) { | ||||
|     byteRefreshJWT := []byte(refreshJWT) | ||||
| 
 | ||||
|     // Acquire Relevant Device | ||||
|     unverifiedToken, err := jwt.ParseBytes(byteRefreshJWT) | ||||
|     did, ok := unverifiedToken.Get("did") | ||||
|     if !ok { | ||||
|         return nil, false | ||||
|         return nil, errors.New("did does not exist") | ||||
|     } | ||||
|     deviceID, err := uuid.Parse(fmt.Sprintf("%v", did)) | ||||
|     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 { | ||||
|         return nil, false | ||||
|         return nil, err | ||||
|     } | ||||
| 
 | ||||
|     // Verify & Validate Token | ||||
| @ -92,22 +92,23 @@ func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, | ||||
|     ) | ||||
|     if err != nil { | ||||
|         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) | ||||
|     verifiedToken, err := jwt.ParseBytes(byteAccessJWT, | ||||
|         jwt.WithValidate(true), | ||||
|         jwt.WithVerify(jwa.HS256, []byte(auth.Config.JWTSecret)), | ||||
|     ) | ||||
| 
 | ||||
|     if err != nil { | ||||
|         fmt.Println("failed to parse payload: ", err) | ||||
|         return nil, false | ||||
|         return nil, err | ||||
|     } | ||||
|     return verifiedToken, true | ||||
| 
 | ||||
|     return verifiedToken, nil | ||||
| } | ||||
| 
 | ||||
| func (auth *AuthManager) CreateJWTRefreshToken(user models.User, device models.Device) (string, error) { | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| package db | ||||
| 
 | ||||
| import ( | ||||
|     "fmt" | ||||
|     "path" | ||||
|     "errors" | ||||
| 
 | ||||
|     "gorm.io/gorm" | ||||
|     // "gorm.io/gorm/logger" | ||||
| @ -52,7 +54,7 @@ func NewMgr(c *config.Config) *DBManager { | ||||
| 
 | ||||
| func (dbm *DBManager) bootstrapDatabase() { | ||||
|     log.Info("[query] Bootstrapping database.") | ||||
|     _, err := dbm.CreateUser(models.User{ | ||||
|     err := dbm.CreateUser(&models.User{ | ||||
|         Username: "admin", | ||||
|         Password: "admin", | ||||
|         AuthType: "Local", | ||||
| @ -62,3 +64,46 @@ func (dbm *DBManager) bootstrapDatabase() { | ||||
|         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" | ||||
| ) | ||||
| 
 | ||||
| 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) | ||||
|     device.RefreshKey = uuid.New().String() | ||||
|     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 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 *models.Device) error { | ||||
|     return nil | ||||
| } | ||||
| 
 | ||||
| func (dbm *DBManager) UpdateRefreshToken (device models.Device, refreshToken string) error { | ||||
| func (dbm *DBManager) UpdateRefreshToken (device *models.Device, refreshToken string) error { | ||||
|     return nil | ||||
| } | ||||
|  | ||||
| @ -6,30 +6,16 @@ import ( | ||||
|     "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) | ||||
|     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 | ||||
|     // db.Table("media_albums"). | ||||
|     //     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). | ||||
|     var count int64 | ||||
| 
 | ||||
| 
 | ||||
|     err := dbm.db. | ||||
|         //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) | ||||
|     err := dbm.db.Where(&mediaItemFilter).Find(&mediaItems).Count(&count).Error; | ||||
|     return mediaItems, count, err | ||||
| } | ||||
|  | ||||
| @ -7,19 +7,19 @@ import ( | ||||
|     "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) | ||||
|     hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) | ||||
|     if err != nil { | ||||
|         log.Error(err) | ||||
|         return user, err | ||||
|         return err | ||||
|     } | ||||
|     user.Password = string(hashedPassword) | ||||
|     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 count int64 | ||||
|     err := dbm.db.Where(&user).First(&foundUser).Count(&count).Error | ||||
|  | ||||
| @ -5,18 +5,20 @@ type APICredentials struct { | ||||
|     Password    string  `json:"password"` | ||||
| } | ||||
| 
 | ||||
| type APIData interface{} | ||||
| 
 | ||||
| type APIMeta struct { | ||||
|     Count   int     `json:"count"` | ||||
|     Page    int     `json:"page"` | ||||
|     Count   int64   `json:"count"` | ||||
|     Page    int64   `json:"page"` | ||||
| } | ||||
| 
 | ||||
| type APIError struct { | ||||
|     Message string  `json:"message"` | ||||
|     Code    int     `json:"code"` | ||||
|     Code    int64   `json:"code"` | ||||
| } | ||||
| 
 | ||||
| type APIResponse struct { | ||||
|     Data    []interface{}   `json:"data"` | ||||
|     Meta    APIMeta         `json:"meta"` | ||||
|     Error   APIError        `json:"error"` | ||||
|     Data    APIData     `json:"data,omitempty"` | ||||
|     Meta    *APIMeta     `json:"meta,omitempty"` | ||||
|     Error   *APIError    `json:"error,omitempty"` | ||||
| } | ||||
|  | ||||
| @ -2,16 +2,17 @@ package models | ||||
| 
 | ||||
| import ( | ||||
|     "time" | ||||
|     "strings" | ||||
|     "reflect" | ||||
|     "gorm.io/gorm" | ||||
|     "github.com/google/uuid" | ||||
| ) | ||||
| 
 | ||||
| // Base contains common columns for all tables. | ||||
| 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:"deleted_at"  gorm:"index"` | ||||
|     DeletedAt gorm.DeletedAt    `json:"-"           gorm:"index"` | ||||
| } | ||||
| 
 | ||||
| func (base *Base) BeforeCreate(tx *gorm.DB) (err error) { | ||||
| @ -28,7 +29,8 @@ type ServerSetting struct { | ||||
| 
 | ||||
| type Device struct { | ||||
|     Base | ||||
|     User         User    `json:"user"   gorm:"ForeignKey:UUID;not null"` // User UUID | ||||
|     UserUUID     uuid.UUID `json:"-"      gorm:"not null"` | ||||
|     User         User      `json:"user"   gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User | ||||
|     Name         string    `json:"name"   gorm:"not null"`                                     // Name of Device | ||||
|     Type         string    `json:"type"   gorm:"not null"`                                     // Android, iOS, Chrome, FireFox, Edge | ||||
|     RefreshKey   string    `json:"-"`                                                          // Device Specific Refresh Key | ||||
| @ -47,10 +49,11 @@ type User struct { | ||||
| 
 | ||||
| type MediaItem struct { | ||||
|     Base | ||||
|     User        User        `json:"user"        gorm:"ForeignKey:UUID;not null"` // User UUID | ||||
|     UserUUID    uuid.UUID   `json:"-"           gorm:"not null"` | ||||
|     User        User        `json:"-"           gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User | ||||
|     EXIFDate    time.Time   `json:"exif_date"`                                                       // EXIF Date | ||||
|     Latitude    float32     `json:"latitude"    gorm:"type:decimal(10,2)"`       // Decimal Latitude | ||||
|     Longitude   float32     `json:"longitude"   gorm:"type:decimal(10,2)"`       // Decimal Longitude | ||||
|     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 | ||||
| @ -67,3 +70,27 @@ 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