From 3d9ae530af4bbdf7f26e127749b01e9df8fbb7df Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Wed, 3 Mar 2021 15:55:55 +0100 Subject: [PATCH] Load shared albums recursively This solves #143 and #229 --- api/graphql/generated.go | 153 +++++++++++++++-------- api/graphql/models/album.go | 15 ++- api/graphql/models/generated.go | 6 + api/graphql/resolvers/album.go | 27 +++- api/graphql/resolvers/media.go | 14 ++- api/graphql/resolvers/share_token.go | 14 +-- api/graphql/resolvers/user.go | 2 +- api/graphql/schema.graphql | 24 +++- ui/src/Pages/SharePage/AlbumSharePage.js | 115 +++++++++++++---- ui/src/Pages/SharePage/SharePage.js | 142 +++++++++++---------- ui/src/Pages/SharePage/SharePage.test.js | 54 +++++--- ui/src/apolloClient.js | 2 +- 12 files changed, 390 insertions(+), 178 deletions(-) diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 8d130f6c..0094165e 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -174,10 +174,10 @@ type ComplexityRoot struct { } Query struct { - Album func(childComplexity int, id int) int + Album func(childComplexity int, id int, tokenCredentials *models.ShareTokenCredentials) int FaceGroup func(childComplexity int, id int) int MapboxToken func(childComplexity int) int - Media func(childComplexity int, id int) int + Media func(childComplexity int, id int, tokenCredentials *models.ShareTokenCredentials) int MediaList func(childComplexity int, ids []int) int MyAlbums func(childComplexity int, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) int MyFaceGroups func(childComplexity int, paginate *models.Pagination) int @@ -186,8 +186,8 @@ type ComplexityRoot struct { MyTimeline func(childComplexity int, paginate *models.Pagination, onlyFavorites *bool) int MyUser func(childComplexity int) int Search func(childComplexity int, query string, limitMedia *int, limitAlbums *int) int - ShareToken func(childComplexity int, token string, password *string) int - ShareTokenValidatePassword func(childComplexity int, token string, password *string) int + ShareToken func(childComplexity int, credentials models.ShareTokenCredentials) int + ShareTokenValidatePassword func(childComplexity int, credentials models.ShareTokenCredentials) int SiteInfo func(childComplexity int) int User func(childComplexity int, order *models.Ordering, paginate *models.Pagination) int } @@ -312,15 +312,15 @@ type QueryResolver interface { User(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.User, error) MyUser(ctx context.Context) (*models.User, error) MyAlbums(ctx context.Context, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) - Album(ctx context.Context, id int) (*models.Album, error) + Album(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Album, error) MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) - Media(ctx context.Context, id int) (*models.Media, error) + Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) MediaList(ctx context.Context, ids []int) ([]*models.Media, error) MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool) ([]*models.TimelineGroup, error) MyMediaGeoJSON(ctx context.Context) (interface{}, error) MapboxToken(ctx context.Context) (*string, error) - ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error) - ShareTokenValidatePassword(ctx context.Context, token string, password *string) (bool, error) + ShareToken(ctx context.Context, credentials models.ShareTokenCredentials) (*models.ShareToken, error) + ShareTokenValidatePassword(ctx context.Context, credentials models.ShareTokenCredentials) (bool, error) Search(ctx context.Context, query string, limitMedia *int, limitAlbums *int) (*models.SearchResult, error) MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error) FaceGroup(ctx context.Context, id int) (*models.FaceGroup, error) @@ -1073,7 +1073,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.Album(childComplexity, args["id"].(int)), true + return e.complexity.Query.Album(childComplexity, args["id"].(int), args["tokenCredentials"].(*models.ShareTokenCredentials)), true case "Query.faceGroup": if e.complexity.Query.FaceGroup == nil { @@ -1104,7 +1104,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.Media(childComplexity, args["id"].(int)), true + return e.complexity.Query.Media(childComplexity, args["id"].(int), args["tokenCredentials"].(*models.ShareTokenCredentials)), true case "Query.mediaList": if e.complexity.Query.MediaList == nil { @@ -1202,7 +1202,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.ShareToken(childComplexity, args["token"].(string), args["password"].(*string)), true + return e.complexity.Query.ShareToken(childComplexity, args["credentials"].(models.ShareTokenCredentials)), true case "Query.shareTokenValidatePassword": if e.complexity.Query.ShareTokenValidatePassword == nil { @@ -1214,7 +1214,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.ShareTokenValidatePassword(childComplexity, args["token"].(string), args["password"].(*string)), true + return e.complexity.Query.ShareTokenValidatePassword(childComplexity, args["credentials"].(models.ShareTokenCredentials)), true case "Query.siteInfo": if e.complexity.Query.SiteInfo == nil { @@ -1595,6 +1595,12 @@ input Ordering { order_direction: OrderDirection } +"Credentials used to identify and authenticate a share token" +input ShareTokenCredentials { + token: String! + password: String +} + type Query { siteInfo: SiteInfo! @@ -1614,13 +1620,19 @@ type Query { "Show only albums having favorites" onlyWithFavorites: Boolean ): [Album!]! - "Get album by id, user must own the album or be admin" - album(id: ID!): Album! + """ + Get album by id, user must own the album or be admin + If valid tokenCredentials are provided, the album may be retrived without further authentication + """ + album(id: ID!, tokenCredentials: ShareTokenCredentials): Album! "List of media owned by the logged in user" myMedia(order: Ordering, paginate: Pagination): [Media!]! - "Get media by id, user must own the media or be admin" - media(id: ID!): Media! + """ + Get media by id, user must own the media or be admin. + If valid tokenCredentials are provided, the media may be retrived without further authentication + """ + media(id: ID!, tokenCredentials: ShareTokenCredentials): Media! "Get a list of media by their ids, user must own the media or be admin" mediaList(ids: [ID!]!): [Media!]! @@ -1632,8 +1644,8 @@ type Query { "Get the mapbox api token, returns null if mapbox is not enabled" mapboxToken: String - shareToken(token: String!, password: String): ShareToken! - shareTokenValidatePassword(token: String!, password: String): Boolean! + shareToken(credentials: ShareTokenCredentials!): ShareToken! + shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean! search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult! @@ -2491,6 +2503,15 @@ func (ec *executionContext) field_Query_album_args(ctx context.Context, rawArgs } } args["id"] = arg0 + var arg1 *models.ShareTokenCredentials + if tmp, ok := rawArgs["tokenCredentials"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("tokenCredentials")) + arg1, err = ec.unmarshalOShareTokenCredentials2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx, tmp) + if err != nil { + return nil, err + } + } + args["tokenCredentials"] = arg1 return args, nil } @@ -2536,6 +2557,15 @@ func (ec *executionContext) field_Query_media_args(ctx context.Context, rawArgs } } args["id"] = arg0 + var arg1 *models.ShareTokenCredentials + if tmp, ok := rawArgs["tokenCredentials"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("tokenCredentials")) + arg1, err = ec.unmarshalOShareTokenCredentials2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx, tmp) + if err != nil { + return nil, err + } + } + args["tokenCredentials"] = arg1 return args, nil } @@ -2689,48 +2719,30 @@ func (ec *executionContext) field_Query_search_args(ctx context.Context, rawArgs func (ec *executionContext) field_Query_shareTokenValidatePassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - var arg0 string - if tmp, ok := rawArgs["token"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token")) - arg0, err = ec.unmarshalNString2string(ctx, tmp) + var arg0 models.ShareTokenCredentials + if tmp, ok := rawArgs["credentials"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("credentials")) + arg0, err = ec.unmarshalNShareTokenCredentials2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx, tmp) if err != nil { return nil, err } } - args["token"] = arg0 - var arg1 *string - if tmp, ok := rawArgs["password"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) - arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) - if err != nil { - return nil, err - } - } - args["password"] = arg1 + args["credentials"] = arg0 return args, nil } func (ec *executionContext) field_Query_shareToken_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - var arg0 string - if tmp, ok := rawArgs["token"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token")) - arg0, err = ec.unmarshalNString2string(ctx, tmp) + var arg0 models.ShareTokenCredentials + if tmp, ok := rawArgs["credentials"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("credentials")) + arg0, err = ec.unmarshalNShareTokenCredentials2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx, tmp) if err != nil { return nil, err } } - args["token"] = arg0 - var arg1 *string - if tmp, ok := rawArgs["password"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) - arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) - if err != nil { - return nil, err - } - } - args["password"] = arg1 + args["credentials"] = arg0 return args, nil } @@ -6177,7 +6189,7 @@ func (ec *executionContext) _Query_album(ctx context.Context, field graphql.Coll fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Album(rctx, args["id"].(int)) + return ec.resolvers.Query().Album(rctx, args["id"].(int), args["tokenCredentials"].(*models.ShareTokenCredentials)) }) if err != nil { ec.Error(ctx, err) @@ -6261,7 +6273,7 @@ func (ec *executionContext) _Query_media(ctx context.Context, field graphql.Coll fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Media(rctx, args["id"].(int)) + return ec.resolvers.Query().Media(rctx, args["id"].(int), args["tokenCredentials"].(*models.ShareTokenCredentials)) }) if err != nil { ec.Error(ctx, err) @@ -6454,7 +6466,7 @@ func (ec *executionContext) _Query_shareToken(ctx context.Context, field graphql fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().ShareToken(rctx, args["token"].(string), args["password"].(*string)) + return ec.resolvers.Query().ShareToken(rctx, args["credentials"].(models.ShareTokenCredentials)) }) if err != nil { ec.Error(ctx, err) @@ -6496,7 +6508,7 @@ func (ec *executionContext) _Query_shareTokenValidatePassword(ctx context.Contex fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().ShareTokenValidatePassword(rctx, args["token"].(string), args["password"].(*string)) + return ec.resolvers.Query().ShareTokenValidatePassword(rctx, args["credentials"].(models.ShareTokenCredentials)) }) if err != nil { ec.Error(ctx, err) @@ -9208,6 +9220,34 @@ func (ec *executionContext) unmarshalInputPagination(ctx context.Context, obj in return it, nil } +func (ec *executionContext) unmarshalInputShareTokenCredentials(ctx context.Context, obj interface{}) (models.ShareTokenCredentials, error) { + var it models.ShareTokenCredentials + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "token": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token")) + it.Token, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "password": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) + it.Password, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** @@ -11300,6 +11340,11 @@ func (ec *executionContext) marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋpho return ec._ShareToken(ctx, sel, v) } +func (ec *executionContext) unmarshalNShareTokenCredentials2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx context.Context, v interface{}) (models.ShareTokenCredentials, error) { + res, err := ec.unmarshalInputShareTokenCredentials(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNSiteInfo2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSiteInfo(ctx context.Context, sel ast.SelectionSet, v models.SiteInfo) graphql.Marshaler { return ec._SiteInfo(ctx, sel, &v) } @@ -11843,6 +11888,14 @@ func (ec *executionContext) marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋpho return ec._ShareToken(ctx, sel, v) } +func (ec *executionContext) unmarshalOShareTokenCredentials2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenCredentials(ctx context.Context, v interface{}) (*models.ShareTokenCredentials, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputShareTokenCredentials(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/api/graphql/models/album.go b/api/graphql/models/album.go index 5f7e666a..f6543d4a 100644 --- a/api/graphql/models/album.go +++ b/api/graphql/models/album.go @@ -29,7 +29,16 @@ func (a *Album) BeforeSave(tx *gorm.DB) (err error) { return nil } -func (a *Album) GetChildren(db *gorm.DB) (children []*Album, err error) { +// GetChildren performs a recursive query to get all the children of the album. +// An optional filter can be provided that can be used to modify the query on the children. +func (a *Album) GetChildren(db *gorm.DB, filter func(*gorm.DB) *gorm.DB) (children []*Album, err error) { + // SELECT * FROM sub_albums + query := db.Model(&Album{}).Table("sub_albums") + + if filter != nil { + query = filter(query) + } + err = db.Raw(` WITH recursive sub_albums AS ( SELECT * FROM albums AS root WHERE id = ? @@ -37,8 +46,8 @@ func (a *Album) GetChildren(db *gorm.DB) (children []*Album, err error) { SELECT child.* FROM albums AS child JOIN sub_albums ON child.parent_album_id = sub_albums.id ) - SELECT * FROM sub_albums - `, a.ID).Find(&children).Error + ? + `, a.ID, query).Find(&children).Error return children, err } diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go index 920c080a..bfffd556 100644 --- a/api/graphql/models/generated.go +++ b/api/graphql/models/generated.go @@ -55,6 +55,12 @@ type SearchResult struct { Media []*Media `json:"media"` } +// Credentials used to identify and authenticate a share token +type ShareTokenCredentials struct { + Token string `json:"token"` + Password *string `json:"password"` +} + type TimelineGroup struct { Album *Album `json:"album"` Media []*Media `json:"media"` diff --git a/api/graphql/resolvers/album.go b/api/graphql/resolvers/album.go index 04ab7cfe..d5b1f2c0 100644 --- a/api/graphql/resolvers/album.go +++ b/api/graphql/resolvers/album.go @@ -2,11 +2,11 @@ package resolvers import ( "context" - "errors" api "github.com/photoview/photoview/api/graphql" "github.com/photoview/photoview/api/graphql/auth" "github.com/photoview/photoview/api/graphql/models" + "github.com/pkg/errors" "gorm.io/gorm" ) @@ -60,7 +60,30 @@ func (r *queryResolver) MyAlbums(ctx context.Context, order *models.Ordering, pa return albums, nil } -func (r *queryResolver) Album(ctx context.Context, id int) (*models.Album, error) { +func (r *queryResolver) Album(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Album, error) { + if tokenCredentials != nil { + + shareToken, err := r.ShareToken(ctx, *tokenCredentials) + if err != nil { + return nil, err + } + + if shareToken.Album != nil { + if *shareToken.AlbumID == id { + return shareToken.Album, nil + } + + subAlbum, err := shareToken.Album.GetChildren(r.Database, func(query *gorm.DB) *gorm.DB { return query.Where("sub_albums.id = ?", id) }) + if err != nil { + return nil, errors.Wrapf(err, "find sub album of share token (%s)", tokenCredentials.Token) + } + + if len(subAlbum) > 0 { + return subAlbum[0], nil + } + } + } + user := auth.UserFromContext(ctx) if user == nil { return nil, auth.ErrUnauthorized diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go index b32117fb..c8ab24fc 100644 --- a/api/graphql/resolvers/media.go +++ b/api/graphql/resolvers/media.go @@ -42,7 +42,19 @@ func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, pag return media, nil } -func (r *queryResolver) Media(ctx context.Context, id int) (*models.Media, error) { +func (r *queryResolver) Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) { + if tokenCredentials != nil { + + shareToken, err := r.ShareToken(ctx, *tokenCredentials) + if err != nil { + return nil, err + } + + if *shareToken.MediaID == id { + return shareToken.Media, nil + } + } + user := auth.UserFromContext(ctx) if user == nil { return nil, auth.ErrUnauthorized diff --git a/api/graphql/resolvers/share_token.go b/api/graphql/resolvers/share_token.go index 16586532..6817d010 100644 --- a/api/graphql/resolvers/share_token.go +++ b/api/graphql/resolvers/share_token.go @@ -40,10 +40,10 @@ func (r *shareTokenResolver) HasPassword(ctx context.Context, obj *models.ShareT return hasPassword, nil } -func (r *queryResolver) ShareToken(ctx context.Context, tokenValue string, password *string) (*models.ShareToken, error) { +func (r *queryResolver) ShareToken(ctx context.Context, credentials models.ShareTokenCredentials) (*models.ShareToken, error) { var token models.ShareToken - if err := r.Database.Preload(clause.Associations).Where("value = ?", tokenValue).First(&token).Error; err != nil { + if err := r.Database.Preload(clause.Associations).Where("value = ?", credentials.Token).First(&token).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("share not found") } else { @@ -52,7 +52,7 @@ func (r *queryResolver) ShareToken(ctx context.Context, tokenValue string, passw } if token.Password != nil { - if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*password)); err != nil { + if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*credentials.Password)); err != nil { if err == bcrypt.ErrMismatchedHashAndPassword { return nil, errors.New("unauthorized") } else { @@ -64,9 +64,9 @@ func (r *queryResolver) ShareToken(ctx context.Context, tokenValue string, passw return &token, nil } -func (r *queryResolver) ShareTokenValidatePassword(ctx context.Context, tokenValue string, password *string) (bool, error) { +func (r *queryResolver) ShareTokenValidatePassword(ctx context.Context, credentials models.ShareTokenCredentials) (bool, error) { var token models.ShareToken - if err := r.Database.Where("value = ?", tokenValue).First(&token).Error; err != nil { + if err := r.Database.Where("value = ?", credentials.Token).First(&token).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return false, errors.New("share not found") } else { @@ -78,11 +78,11 @@ func (r *queryResolver) ShareTokenValidatePassword(ctx context.Context, tokenVal return true, nil } - if password == nil { + if credentials.Password == nil { return false, nil } - if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*password)); err != nil { + if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*credentials.Password)); err != nil { if err == bcrypt.ErrMismatchedHashAndPassword { return false, nil } else { diff --git a/api/graphql/resolvers/user.go b/api/graphql/resolvers/user.go index 8ba1ff31..11e6bc85 100644 --- a/api/graphql/resolvers/user.go +++ b/api/graphql/resolvers/user.go @@ -326,7 +326,7 @@ func (r *mutationResolver) UserRemoveRootAlbum(ctx context.Context, userID int, return err } - children, err := album.GetChildren(tx) + children, err := album.GetChildren(tx, nil) if err != nil { return err } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 6a845225..15042adf 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -18,6 +18,12 @@ input Ordering { order_direction: OrderDirection } +"Credentials used to identify and authenticate a share token" +input ShareTokenCredentials { + token: String! + password: String +} + type Query { siteInfo: SiteInfo! @@ -37,13 +43,19 @@ type Query { "Show only albums having favorites" onlyWithFavorites: Boolean ): [Album!]! - "Get album by id, user must own the album or be admin" - album(id: ID!): Album! + """ + Get album by id, user must own the album or be admin + If valid tokenCredentials are provided, the album may be retrived without further authentication + """ + album(id: ID!, tokenCredentials: ShareTokenCredentials): Album! "List of media owned by the logged in user" myMedia(order: Ordering, paginate: Pagination): [Media!]! - "Get media by id, user must own the media or be admin" - media(id: ID!): Media! + """ + Get media by id, user must own the media or be admin. + If valid tokenCredentials are provided, the media may be retrived without further authentication + """ + media(id: ID!, tokenCredentials: ShareTokenCredentials): Media! "Get a list of media by their ids, user must own the media or be admin" mediaList(ids: [ID!]!): [Media!]! @@ -55,8 +67,8 @@ type Query { "Get the mapbox api token, returns null if mapbox is not enabled" mapboxToken: String - shareToken(token: String!, password: String): ShareToken! - shareTokenValidatePassword(token: String!, password: String): Boolean! + shareToken(credentials: ShareTokenCredentials!): ShareToken! + shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean! search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult! diff --git a/ui/src/Pages/SharePage/AlbumSharePage.js b/ui/src/Pages/SharePage/AlbumSharePage.js index 060ef156..5f9f255a 100644 --- a/ui/src/Pages/SharePage/AlbumSharePage.js +++ b/ui/src/Pages/SharePage/AlbumSharePage.js @@ -1,51 +1,114 @@ import PropTypes from 'prop-types' import React from 'react' -import { Route, Switch } from 'react-router-dom' -import RouterPropTypes from 'react-router-prop-types' import Layout from '../../Layout' import AlbumGallery from '../../components/albumGallery/AlbumGallery' import styled from 'styled-components' +import { gql, useQuery } from '@apollo/client' + +export const SHARE_ALBUM_QUERY = gql` + query shareAlbumQuery( + $id: ID! + $token: String! + $password: String + $limit: Int + $offset: Int + ) { + album(id: $id, tokenCredentials: { token: $token, password: $password }) { + id + title + subAlbums(order: { order_by: "title" }) { + id + title + thumbnail { + thumbnail { + url + } + } + } + media(paginate: { limit: $limit, offset: $offset }) { + id + title + type + thumbnail { + url + width + height + } + downloads { + title + mediaUrl { + url + width + height + fileSize + } + } + highRes { + url + width + height + } + videoWeb { + url + } + exif { + camera + maker + lens + dateShot + exposure + aperture + iso + focalLength + flash + exposureProgram + } + } + } + } +` const AlbumSharePageWrapper = styled.div` height: 100%; ` -const AlbumSharePage = ({ album, match }) => { - const SubAlbumRoute = subProps => { - const subAlbumId = subProps.match.params.subAlbum - const subAlbum = album.subAlbums.find(x => x.id == subAlbumId) +const AlbumSharePage = ({ albumID, token, password }) => { + const { data, loading, error } = useQuery(SHARE_ALBUM_QUERY, { + variables: { + id: albumID, + token, + password, + limit: 200, + offset: 0, + }, + }) - if (!subAlbum) { - return
Subalbum was not found
- } - - return + if (error) { + return error.message } - SubAlbumRoute.propTypes = { - ...RouterPropTypes, + if (loading) { + return 'Loading...' } - const customAlbumLink = albumId => { - return `${match.url}/${albumId}` - } + const album = data.album + return ( - - - - - - - - + + `/share/${token}/${albumId}`} + /> + ) } AlbumSharePage.propTypes = { - album: PropTypes.object.isRequired, - match: RouterPropTypes.match, + albumID: PropTypes.string.isRequired, + token: PropTypes.string.isRequired, + password: PropTypes.string, } export default AlbumSharePage diff --git a/ui/src/Pages/SharePage/SharePage.js b/ui/src/Pages/SharePage/SharePage.js index 328930ab..5541f06d 100644 --- a/ui/src/Pages/SharePage/SharePage.js +++ b/ui/src/Pages/SharePage/SharePage.js @@ -11,90 +11,70 @@ import MediaSharePage from './MediaSharePage' export const SHARE_TOKEN_QUERY = gql` query SharePageToken($token: String!, $password: String) { - shareToken(token: $token, password: $password) { + shareToken(credentials: { token: $token, password: $password }) { token album { - ...AlbumProps - subAlbums { - ...AlbumProps - subAlbums { - ...AlbumProps - } - } + id } media { - ...MediaProps + id + title + type + thumbnail { + url + width + height + } + downloads { + title + mediaUrl { + url + width + height + fileSize + } + } + highRes { + url + width + height + } + videoWeb { + url + } + exif { + camera + maker + lens + dateShot + exposure + aperture + iso + focalLength + flash + exposureProgram + } } } } - - fragment AlbumProps on Album { - id - title - thumbnail { - thumbnail { - url - } - } - media(order: { order_by: "title", order_direction: DESC }) { - ...MediaProps - } - } - - fragment MediaProps on Media { - id - title - type - thumbnail { - url - width - height - } - downloads { - title - mediaUrl { - url - width - height - fileSize - } - } - highRes { - url - width - height - } - videoWeb { - url - } - exif { - camera - maker - lens - dateShot - exposure - aperture - iso - focalLength - flash - exposureProgram - } - } ` export const VALIDATE_TOKEN_PASSWORD_QUERY = gql` query ShareTokenValidatePassword($token: String!, $password: String) { - shareTokenValidatePassword(token: $token, password: $password) + shareTokenValidatePassword( + credentials: { token: $token, password: $password } + ) } ` const AuthorizedTokenRoute = ({ match }) => { const token = match.params.token + const password = getSharePassword(token) const { loading, error, data } = useQuery(SHARE_TOKEN_QUERY, { variables: { token, - password: getSharePassword(token), + password, }, }) @@ -102,7 +82,39 @@ const AuthorizedTokenRoute = ({ match }) => { if (loading) return 'Loading...' if (data.shareToken.album) { - return + console.log('match', match) + + const SharedSubAlbumPage = ({ match }) => { + console.log('subalbum match', match) + return ( + + ) + } + + SharedSubAlbumPage.propTypes = { + match: PropTypes.any, + } + + return ( + + + + + + + ) } if (data.shareToken.media) { diff --git a/ui/src/Pages/SharePage/SharePage.test.js b/ui/src/Pages/SharePage/SharePage.test.js index ae8232ae..c894c16b 100644 --- a/ui/src/Pages/SharePage/SharePage.test.js +++ b/ui/src/Pages/SharePage/SharePage.test.js @@ -16,6 +16,7 @@ import SharePage, { } from './SharePage' import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownload' +import { SHARE_ALBUM_QUERY } from './AlbumSharePage' describe('load correct share page, based on graphql query', () => { const token = 'TOKEN123' @@ -48,13 +49,13 @@ describe('load correct share page, based on graphql query', () => { request: { query: SIDEBAR_DOWNLOAD_QUERY, variables: { - mediaId: 1, + mediaId: '1', }, }, result: { data: { media: { - id: 1, + id: '1', downloads: [], }, }, @@ -77,7 +78,7 @@ describe('load correct share page, based on graphql query', () => { token: token, album: null, media: { - id: 1, + id: '1', title: 'shared_image.jpg', type: 'photo', highRes: { @@ -114,20 +115,42 @@ describe('load correct share page, based on graphql query', () => { }) test('load album share page', async () => { - const albumPageMock = { - request: { - query: SHARE_TOKEN_QUERY, - variables: { - token, - password: null, + const albumPageMock = [ + { + request: { + query: SHARE_TOKEN_QUERY, + variables: { + token, + password: null, + }, + }, + result: { + data: { + shareToken: { + token: token, + album: { + id: '1', + }, + media: null, + }, + }, }, }, - result: { - data: { - shareToken: { + { + request: { + query: SHARE_ALBUM_QUERY, + variables: { + id: '1', token: token, + password: null, + limit: 200, + offset: 0, + }, + }, + result: { + data: { album: { - id: 1, + id: '1', title: 'album_title', subAlbums: [], thumbnail: { @@ -135,15 +158,14 @@ describe('load correct share page, based on graphql query', () => { }, media: [], }, - media: null, }, }, }, - } + ] render( { if (graphQLErrors.find(x => x.message == 'unauthorized')) { console.log('Unauthorized, clearing token cookie') clearTokenCookie() - location.reload() + // location.reload() } }