diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 2e09f138..8ebfb4f5 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -69,6 +69,7 @@ type ComplexityRoot struct { Mutation struct { AuthorizeUser func(childComplexity int, username string, password string) int + DeleteShareToken func(childComplexity int, token string) int InitialSetupWizard func(childComplexity int, username string, password string, rootPath string) int RegisterUser func(childComplexity int, username string, password string, rootPath string) int ScanAll func(childComplexity int) int @@ -170,6 +171,7 @@ type MutationResolver interface { ScanUser(ctx context.Context, userID int) (*models.ScannerResult, error) ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) SharePhoto(ctx context.Context, photoID int, expire *time.Time, password *string) (*models.ShareToken, error) + DeleteShareToken(ctx context.Context, token string) (*models.ShareToken, error) } type PhotoResolver interface { Thumbnail(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) @@ -317,6 +319,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.AuthorizeUser(childComplexity, args["username"].(string), args["password"].(string)), true + case "Mutation.deleteShareToken": + if e.complexity.Mutation.DeleteShareToken == nil { + break + } + + args, err := ec.field_Mutation_deleteShareToken_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteShareToken(childComplexity, args["token"].(string)), true + case "Mutation.initialSetupWizard": if e.complexity.Mutation.InitialSetupWizard == nil { break @@ -875,6 +889,8 @@ type Mutation { shareAlbum(albumId: Int!, expire: Time, password: String): ShareToken "Generate share token for photo" sharePhoto(photoId: Int!, expire: Time, password: String): ShareToken + "Delete a share token by it's token value" + deleteShareToken(token: String!): ShareToken } type AuthorizeResult { @@ -1050,6 +1066,20 @@ func (ec *executionContext) field_Mutation_authorizeUser_args(ctx context.Contex return args, nil } +func (ec *executionContext) field_Mutation_deleteShareToken_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 { + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["token"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_initialSetupWizard_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -2064,6 +2094,47 @@ func (ec *executionContext) _Mutation_sharePhoto(ctx context.Context, field grap return ec.marshalOShareToken2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + ec.Tracer.EndFieldExecution(ctx) + }() + rctx := &graphql.ResolverContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_deleteShareToken_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteShareToken(rctx, args["token"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*models.ShareToken) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOShareToken2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) +} + func (ec *executionContext) _Photo_id(ctx context.Context, field graphql.CollectedField, obj *models.Photo) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { @@ -5328,6 +5399,8 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec._Mutation_shareAlbum(ctx, field) case "sharePhoto": out.Values[i] = ec._Mutation_sharePhoto(ctx, field) + case "deleteShareToken": + out.Values[i] = ec._Mutation_deleteShareToken(ctx, field) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/api/graphql/resolvers/photo.go b/api/graphql/resolvers/photo.go index 99d6ce59..14da2d4f 100644 --- a/api/graphql/resolvers/photo.go +++ b/api/graphql/resolvers/photo.go @@ -58,9 +58,12 @@ func (r *Resolver) Photo() api.PhotoResolver { } func (r *photoResolver) Shares(ctx context.Context, obj *models.Photo) ([]*models.ShareToken, error) { - log.Println("Photo: shares not implemented") - shares := make([]*models.ShareToken, 0) - return shares, nil + rows, err := r.Database.Query("SELECT * FROM share_token WHERE photo_id = ?", obj.PhotoID) + if err != nil { + return nil, err + } + + return models.NewShareTokensFromRows(rows) } func (r *photoResolver) Downloads(ctx context.Context, obj *models.Photo) ([]*models.PhotoDownload, error) { diff --git a/api/graphql/resolvers/share_token.go b/api/graphql/resolvers/share_token.go index 31d943e8..14e4034d 100644 --- a/api/graphql/resolvers/share_token.go +++ b/api/graphql/resolvers/share_token.go @@ -3,7 +3,7 @@ package resolvers import ( "context" "database/sql" - "log" + "errors" "time" api "github.com/viktorstrate/photoview/api/graphql" @@ -60,7 +60,7 @@ func (r *queryResolver) ShareToken(ctx context.Context, token string, password * result, err := models.NewShareTokenFromRow(row) if err != nil { if err == sql.ErrNoRows { - return nil, nil + return nil, errors.New("share not found") } else { return nil, err } @@ -69,13 +69,6 @@ func (r *queryResolver) ShareToken(ctx context.Context, token string, password * return result, nil } -func (r *queryResolver) PhotoShares(ctx context.Context, token string, password *string) ([]*models.ShareToken, error) { - log.Println("Query PhotoShares: not implemented") - - tokens := make([]*models.ShareToken, 0) - return tokens, nil -} - func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) { user := auth.UserFromContext(ctx) if user == nil { @@ -124,5 +117,73 @@ func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire * } func (r *mutationResolver) SharePhoto(ctx context.Context, photoID int, expire *time.Time, password *string) (*models.ShareToken, error) { - panic("not implemented") + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + rows, err := r.Database.Query("SELECT owner_id FROM album, photo WHERE photo.photo_id = ? AND photo.album_id = album.album_id AND album.owner_id = ?", photoID, user.UserID) + if err != nil { + return nil, err + } + if rows.Next() == false { + return nil, auth.ErrUnauthorized + } + rows.Close() + + var hashed_password *string = nil + if password != nil { + hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12) + if err != nil { + return nil, err + } + hashed_str := string(hashedPassBytes) + hashed_password = &hashed_str + } + + token := utils.GenerateToken() + res, err := r.Database.Exec("INSERT INTO share_token (value, owner_id, expire, password, photo_id) VALUES (?, ?, ?, ?, ?)", token, user.UserID, expire, hashed_password, photoID) + if err != nil { + return nil, err + } + + token_id, err := res.LastInsertId() + if err != nil { + return nil, err + } + + return &models.ShareToken{ + TokenID: int(token_id), + Value: token, + OwnerID: user.UserID, + Expire: expire, + Password: password, + AlbumID: nil, + PhotoID: &photoID, + }, nil +} + +func (r *mutationResolver) DeleteShareToken(ctx context.Context, tokenValue string) (*models.ShareToken, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + row := r.Database.QueryRow(` + SELECT share_token.* FROM share_token, user WHERE + share_token.value = ? AND + share_token.owner_id = user.user_id AND + (user.user_id = ? OR user.admin = TRUE) + `, tokenValue, user.UserID) + + token, err := models.NewShareTokenFromRow(row) + if err != nil { + return nil, err + } + + if _, err := r.Database.Exec("DELETE FROM share_token WHERE token_id = ?", token.TokenID); err != nil { + return nil, err + } + + return token, nil } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index a76c82c2..de007ef3 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -61,6 +61,8 @@ type Mutation { shareAlbum(albumId: Int!, expire: Time, password: String): ShareToken "Generate share token for photo" sharePhoto(photoId: Int!, expire: Time, password: String): ShareToken + "Delete a share token by it's token value" + deleteShareToken(token: String!): ShareToken } type AuthorizeResult { diff --git a/ui/src/Pages/SharePage/PhotoSharePage.js b/ui/src/Pages/SharePage/PhotoSharePage.js index 20186a5e..a1436940 100644 --- a/ui/src/Pages/SharePage/PhotoSharePage.js +++ b/ui/src/Pages/SharePage/PhotoSharePage.js @@ -20,7 +20,7 @@ const AlbumSharePage = ({ photo }) => { <>