From efcb2dab443080e8bfc72427b5543f26cdb78922 Mon Sep 17 00:00:00 2001 From: Peter Sanchez Date: Tue, 14 May 2024 14:58:26 -0600 Subject: [PATCH] Using hash ID's for viewing public links to avoid scraping. References: https://todo.code.netlandish.com/~netlandish/links/74 --- api/api_test.go | 14 ++- api/graph/generated.go | 153 ++++++++++++++++----------------- api/graph/schema.graphqls | 4 +- api/graph/schema.resolvers.go | 60 ++++--------- core/import.go | 24 +++--- core/routes.go | 63 +++++++------- core/routes_test.go | 8 +- core/samples/detail_link.json | 1 + migrations/0001_initial.up.sql | 2 +- models/models.go | 4 +- models/org_link.go | 28 +++--- models/schema.sql | 4 +- templates/link_detail.html | 4 +- 13 files changed, 176 insertions(+), 193 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 5df50d8..089a95d 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -625,13 +625,21 @@ func TestAPI(t *testing.T) { c.NoError(err) org := orgs[0] + // We know this has already been saved earlier. We need the hash to fetch it so + // let's query the db + ols, err := models.GetOrgLinks(dbCtx, + &database.FilterOptions{Filter: sq.Eq{"ol.id": 3}, Limit: 1}, + ) + c.NoError(err) + ol := ols[0] + type GraphQLResponse struct { Link models.OrgLink `json:"getOrgLink"` } var result GraphQLResponse op := gqlclient.NewOperation( - `query getOrgLink($id: Int!) { - getOrgLink(id: $id) { + `query getOrgLink($hash: String!) { + getOrgLink(hash: $hash) { id title url @@ -651,7 +659,7 @@ func TestAPI(t *testing.T) { } } }`) - op.Var("id", 3) + op.Var("hash", ol.Hash) err = links.Execute(ctx, op, &result) c.NoError(err) c.Equal("New title", result.Link.Title) diff --git a/api/graph/generated.go b/api/graph/generated.go index 6c74c6c..ebb41d9 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -259,8 +259,8 @@ type ComplexityRoot struct { BaseURLID func(childComplexity int) int CreatedOn func(childComplexity int) int Description func(childComplexity int) int + Hash func(childComplexity int) int ID func(childComplexity int) int - NoteHash func(childComplexity int) int OrgID func(childComplexity int) int OrgSlug func(childComplexity int) int Starred func(childComplexity int) int @@ -368,7 +368,7 @@ type ComplexityRoot struct { GetListing func(childComplexity int, input *model.GetListingDetailInput) int GetListingLink func(childComplexity int, slug string, id int, domainID int) int GetListings func(childComplexity int, input *model.GetListingInput) int - GetOrgLink func(childComplexity int, id *int, hash *string) int + GetOrgLink func(childComplexity int, hash string) int GetOrgLinks func(childComplexity int, input *model.GetLinkInput) int GetOrgMembers func(childComplexity int, orgSlug string) int GetOrganization func(childComplexity int, id int) int @@ -477,7 +477,7 @@ type QueryResolver interface { GetOrganization(ctx context.Context, id int) (*models.Organization, error) GetPaymentHistory(ctx context.Context, input *model.GetPaymentInput) (*model.PaymentCursor, error) GetPopularLinks(ctx context.Context, input *model.PopularLinksInput) ([]*models.BaseURL, error) - GetOrgLink(ctx context.Context, id *int, hash *string) (*models.OrgLink, error) + GetOrgLink(ctx context.Context, hash string) (*models.OrgLink, error) GetOrgLinks(ctx context.Context, input *model.GetLinkInput) (*model.OrgLinkCursor, error) GetOrgMembers(ctx context.Context, orgSlug string) ([]*models.User, error) GetDomains(ctx context.Context, orgSlug *string, service *int) ([]*models.Domain, error) @@ -1625,19 +1625,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.OrgLink.Description(childComplexity), true - case "OrgLink.id": - if e.complexity.OrgLink.ID == nil { + case "OrgLink.hash": + if e.complexity.OrgLink.Hash == nil { break } - return e.complexity.OrgLink.ID(childComplexity), true + return e.complexity.OrgLink.Hash(childComplexity), true - case "OrgLink.noteHash": - if e.complexity.OrgLink.NoteHash == nil { + case "OrgLink.id": + if e.complexity.OrgLink.ID == nil { break } - return e.complexity.OrgLink.NoteHash(childComplexity), true + return e.complexity.OrgLink.ID(childComplexity), true case "OrgLink.orgId": if e.complexity.OrgLink.OrgID == nil { @@ -2232,7 +2232,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.GetOrgLink(childComplexity, args["id"].(*int), args["hash"].(*string)), true + return e.complexity.Query.GetOrgLink(childComplexity, args["hash"].(string)), true case "Query.getOrgLinks": if e.complexity.Query.GetOrgLinks == nil { @@ -3405,24 +3405,15 @@ func (ec *executionContext) field_Query_getListings_args(ctx context.Context, ra func (ec *executionContext) field_Query_getOrgLink_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - var arg0 *int - if tmp, ok := rawArgs["id"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) - arg0, err = ec.unmarshalOInt2ᚖint(ctx, tmp) - if err != nil { - return nil, err - } - } - args["id"] = arg0 - var arg1 *string + var arg0 string if tmp, ok := rawArgs["hash"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("hash")) - arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + arg0, err = ec.unmarshalNString2string(ctx, tmp) if err != nil { return nil, err } } - args["hash"] = arg1 + args["hash"] = arg0 return args, nil } @@ -8899,6 +8890,8 @@ func (ec *executionContext) fieldContext_Mutation_addLink(ctx context.Context, f return ec.fieldContext_OrgLink_description(ctx, field) case "url": return ec.fieldContext_OrgLink_url(ctx, field) + case "hash": + return ec.fieldContext_OrgLink_hash(ctx, field) case "baseUrlId": return ec.fieldContext_OrgLink_baseUrlId(ctx, field) case "orgId": @@ -8915,8 +8908,6 @@ func (ec *executionContext) fieldContext_Mutation_addLink(ctx context.Context, f return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) - case "noteHash": - return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -9022,6 +9013,8 @@ func (ec *executionContext) fieldContext_Mutation_updateLink(ctx context.Context return ec.fieldContext_OrgLink_description(ctx, field) case "url": return ec.fieldContext_OrgLink_url(ctx, field) + case "hash": + return ec.fieldContext_OrgLink_hash(ctx, field) case "baseUrlId": return ec.fieldContext_OrgLink_baseUrlId(ctx, field) case "orgId": @@ -9038,8 +9031,6 @@ func (ec *executionContext) fieldContext_Mutation_updateLink(ctx context.Context return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) - case "noteHash": - return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -9234,6 +9225,8 @@ func (ec *executionContext) fieldContext_Mutation_addNote(ctx context.Context, f return ec.fieldContext_OrgLink_description(ctx, field) case "url": return ec.fieldContext_OrgLink_url(ctx, field) + case "hash": + return ec.fieldContext_OrgLink_hash(ctx, field) case "baseUrlId": return ec.fieldContext_OrgLink_baseUrlId(ctx, field) case "orgId": @@ -9250,8 +9243,6 @@ func (ec *executionContext) fieldContext_Mutation_addNote(ctx context.Context, f return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) - case "noteHash": - return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -12114,6 +12105,50 @@ func (ec *executionContext) fieldContext_OrgLink_url(ctx context.Context, field return fc, nil } +func (ec *executionContext) _OrgLink_hash(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_OrgLink_hash(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Hash, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_OrgLink_hash(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "OrgLink", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _OrgLink_baseUrlId(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) { fc, err := ec.fieldContext_OrgLink_baseUrlId(ctx, field) if err != nil { @@ -12609,47 +12644,6 @@ func (ec *executionContext) fieldContext_OrgLink_type(ctx context.Context, field return fc, nil } -func (ec *executionContext) _OrgLink_noteHash(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_OrgLink_noteHash(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.NoteHash, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalOString2string(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_OrgLink_noteHash(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "OrgLink", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} - func (ec *executionContext) _OrgLink_tags(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) { fc, err := ec.fieldContext_OrgLink_tags(ctx, field) if err != nil { @@ -13003,6 +12997,8 @@ func (ec *executionContext) fieldContext_OrgLinkCursor_result(ctx context.Contex return ec.fieldContext_OrgLink_description(ctx, field) case "url": return ec.fieldContext_OrgLink_url(ctx, field) + case "hash": + return ec.fieldContext_OrgLink_hash(ctx, field) case "baseUrlId": return ec.fieldContext_OrgLink_baseUrlId(ctx, field) case "orgId": @@ -13019,8 +13015,6 @@ func (ec *executionContext) fieldContext_OrgLinkCursor_result(ctx context.Contex return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) - case "noteHash": - return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -16286,7 +16280,7 @@ func (ec *executionContext) _Query_getOrgLink(ctx context.Context, field graphql resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().GetOrgLink(rctx, fc.Args["id"].(*int), fc.Args["hash"].(*string)) + return ec.resolvers.Query().GetOrgLink(rctx, fc.Args["hash"].(string)) } directive1 := func(ctx context.Context) (interface{}, error) { scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "LINKS") @@ -16343,6 +16337,8 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f return ec.fieldContext_OrgLink_description(ctx, field) case "url": return ec.fieldContext_OrgLink_url(ctx, field) + case "hash": + return ec.fieldContext_OrgLink_hash(ctx, field) case "baseUrlId": return ec.fieldContext_OrgLink_baseUrlId(ctx, field) case "orgId": @@ -16359,8 +16355,6 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f return ec.fieldContext_OrgLink_archiveUrl(ctx, field) case "type": return ec.fieldContext_OrgLink_type(ctx, field) - case "noteHash": - return ec.fieldContext_OrgLink_noteHash(ctx, field) case "tags": return ec.fieldContext_OrgLink_tags(ctx, field) case "author": @@ -24794,6 +24788,13 @@ func (ec *executionContext) _OrgLink(ctx context.Context, sel ast.SelectionSet, out.Values[i] = ec._OrgLink_url(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + case "hash": + + out.Values[i] = ec._OrgLink_hash(ctx, field, obj) + if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } @@ -24863,10 +24864,6 @@ func (ec *executionContext) _OrgLink(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } - case "noteHash": - - out.Values[i] = ec._OrgLink_noteHash(ctx, field, obj) - case "tags": out.Values[i] = ec._OrgLink_tags(ctx, field, obj) diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index 26bfa54..5a891d1 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -163,6 +163,7 @@ type OrgLink { title: String! description: String! url: String! + hash: String! baseUrlId: NullInt! @access(scope: LINKS, kind: RO) orgId: Int! @access(scope: LINKS, kind: RO) userId: Int @access(scope: LINKS, kind: RO) @@ -171,7 +172,6 @@ type OrgLink { starred: Boolean! @access(scope: LINKS, kind: RO) archiveUrl: String! type: Int! - noteHash: String tags: [Tag]! author: String! orgSlug: String! @access(scope: LINKS, kind: RO) @@ -688,7 +688,7 @@ type Query { getPopularLinks(input: PopularLinksInput): [BaseURL]! @access(scope: LINKS, kind: RO) "Returns a specific organization link" - getOrgLink(id: Int, hash: String): OrgLink @access(scope: LINKS, kind: RO) + getOrgLink(hash: String!): OrgLink @access(scope: LINKS, kind: RO) "Returns an array of organization links" getOrgLinks(input: GetLinkInput): OrgLinkCursor! @access(scope: LINKS, kind: RO) diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 10c3b25..1c1ca89 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -269,6 +269,7 @@ func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput) URL: input.URL, UserID: int(user.ID), Type: models.OrgLinkType, + Hash: ksuid.New().String(), } if input.Description != nil { OrgLink.Description = *input.Description @@ -595,7 +596,8 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput) return nil, valid.ErrAuthorization } user := tokenUser.User.(*models.User) - lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user) + c := server.EchoForContext(ctx) + lang := links.GetLangFromRequest(c.Request(), user) lt := localizer.GetLocalizer(lang) validator := valid.New(ctx) @@ -650,7 +652,6 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput) WithCode(valid.ErrNotFoundCode) return nil, nil } - org := orgs[0] // If the owner if not creating the note, add extra validation @@ -672,7 +673,6 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput) } // Create the note url - c := server.EchoForContext(ctx) noteURL, noteHash := links.CreateNoteURL(c) // If the note is public we create a based link @@ -701,7 +701,7 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput) URL: noteURL, UserID: int(user.ID), Type: models.NoteType, - NoteHash: noteHash, + Hash: noteHash, } err = OrgLinkNote.Store(ctx) @@ -4533,7 +4533,7 @@ func (r *queryResolver) GetPopularLinks(ctx context.Context, input *model.Popula } // GetOrgLink is the resolver for the getOrgLink field. -func (r *queryResolver) GetOrgLink(ctx context.Context, id *int, hash *string) (*models.OrgLink, error) { +func (r *queryResolver) GetOrgLink(ctx context.Context, hash string) (*models.OrgLink, error) { tokenUser := oauth2.ForContext(ctx) if tokenUser == nil { return nil, valid.ErrAuthorization @@ -4544,49 +4544,27 @@ func (r *queryResolver) GetOrgLink(ctx context.Context, id *int, hash *string) ( ctx = timezone.Context(ctx, links.GetUserTZ(user)) - if id == nil && hash == nil { - validator := valid.New(ctx) - validator.Error(lt.Translate("No identifier id or hash was provided")). - WithCode(valid.ErrValidationGlobalCode) - return nil, nil - } - opts := &database.FilterOptions{ - Filter: sq.Or{ - sq.Eq{"o.owner_id": user.ID}, // If the user is the org owner - // if the user is a member of the org is able to see private and public links - sq.And{ - sq.Eq{"ou.user_id": user.ID}, - sq.Eq{"ou.is_active": true}, - sq.Eq{"ol.visibility": []int{ - models.OrgLinkVisibilityPublic, - models.OrgLinkVisibilityPrivate}, + Filter: sq.And{ + sq.Eq{"ol.hash": hash}, + sq.Or{ + sq.Eq{"o.owner_id": user.ID}, // If the user is the org owner + // if the user is a member of the org is able to see private and public links + sq.And{ + sq.Eq{"ou.user_id": user.ID}, + sq.Eq{"ou.is_active": true}, + sq.Eq{"ol.visibility": []int{ + models.OrgLinkVisibilityPublic, + models.OrgLinkVisibilityPrivate}, + }, }, + // Otherwise the user is only able to see public links + sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic}, }, - // Otherwise the user is only able to see public links - sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic}, }, Limit: 1, } - if id != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.And{ - sq.Eq{"ol.id": *id}, - sq.Eq{"ol.type": models.OrgLinkType}, - }, - } - } else { - opts.Filter = sq.And{ - opts.Filter, - sq.And{ - sq.Eq{"ol.note_hash": *hash}, - sq.Eq{"ol.type": models.NoteType}, - }, - } - } - orgLinks, err := models.GetOrgLinks(ctx, opts) if err != nil { return nil, err diff --git a/core/import.go b/core/import.go index 5a91a50..10c6da1 100644 --- a/core/import.go +++ b/core/import.go @@ -36,7 +36,7 @@ const ( // import helper funcs type importObj interface { GetURL() string - GetNoteHash() string + GetHash() string GetTitle() string GetDescription() string IsPublic() bool @@ -44,7 +44,7 @@ type importObj interface { IsNote() bool SetURL(string) SetIsNote(bool) - SetNoteHash(string) + SetHash(string) } // Especific pinboard object representation @@ -60,8 +60,8 @@ type pinBoardObj struct { Tags string // We are not parsing this data from the json - noteHash string - isNote bool + hash string + isNote bool } func (p pinBoardObj) GetURL() string { @@ -92,12 +92,12 @@ func (p *pinBoardObj) SetURL(url string) { p.Href = url } -func (p *pinBoardObj) SetNoteHash(hash string) { - p.noteHash = hash +func (p *pinBoardObj) SetHash(hash string) { + p.hash = hash } -func (p pinBoardObj) GetNoteHash() string { - return p.noteHash +func (p pinBoardObj) GetHash() string { + return p.hash } func (p pinBoardObj) IsUnread() bool { @@ -140,12 +140,12 @@ func (h *htmlObj) SetURL(_ string) { // method importer } -func (h *htmlObj) SetNoteHash(_ string) { +func (h *htmlObj) SetHash(_ string) { // NOTE we don't need to implement this // method importer } -func (h htmlObj) GetNoteHash() string { +func (h htmlObj) GetHash() string { return "" } @@ -188,7 +188,7 @@ func processBaseURLs(obj importObj, tmpURLMap map[string]bool, urlList []string, noteURL, noteHash := links.CreateNoteURL(c) obj.SetURL(noteURL) obj.SetIsNote(true) - obj.SetNoteHash(noteHash) + obj.SetHash(noteHash) } // Get a list of base url to make a query // of existing ones @@ -277,7 +277,7 @@ func processOrgLinks(obj importObj, baseURLMap map[string]int, UserID: int(user.ID), Type: linkType, Visibility: vis, - NoteHash: obj.GetNoteHash(), + Hash: obj.GetHash(), Unread: obj.IsUnread(), } } diff --git a/core/routes.go b/core/routes.go index ff8c0af..bf95e01 100644 --- a/core/routes.go +++ b/core/routes.go @@ -65,8 +65,8 @@ func (s *Service) RegisterRoutes() { s.eg.GET("/q/:hash", s.QRRedirect).Name = s.RouteName("qr_redirect") s.eg.GET("/tour", s.FeatureTour).Name = s.RouteName("feature_tour") s.eg.GET("/note/:hash", s.NoteDetail).Name = s.RouteName("note_detail") - s.eg.GET("/link/:id", s.OrgLinkDetail).Name = s.RouteName("link_detail") - s.eg.GET("/click/:id", s.OrgLinkRedirect).Name = s.RouteName("link_redirect") + s.eg.GET("/link/:hash", s.OrgLinkDetail).Name = s.RouteName("link_detail") + s.eg.GET("/click/:hash", s.OrgLinkRedirect).Name = s.RouteName("link_redirect") s.eg.GET("/tag-autocomplete", s.TagAutocomplete).Name = s.RouteName("tag_autocomplete") s.eg.Use(auth.AuthRequired()) @@ -1298,7 +1298,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error { archive: $archive, orgSlug: $slug, tags: $tags}) { - id + hash } }`) op.Var("title", form.Title) @@ -1329,7 +1329,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error { if form.Redirect != "" { redirect = form.Redirect } else { - redirect = c.Echo().Reverse(s.RouteName("link_detail"), result.Link.ID) + redirect = c.Echo().Reverse(s.RouteName("link_detail"), result.Link.Hash) } return c.Redirect(http.StatusMovedPermanently, redirect) } @@ -1382,16 +1382,16 @@ func (s *Service) OrgLinksCreate(c echo.Context) error { } } else { // OrgLink - id, err = strconv.Atoi(c.QueryParam("linkid")) - if err == nil { + hash := c.QueryParam("linkid") + if hash != "" { type GraphQLResponse struct { Link models.OrgLink `json:"getOrgLink"` } var result GraphQLResponse op := gqlclient.NewOperation( - `query getOrgLink($id: Int) { - getOrgLink(id: $id) { + `query getOrgLink($hash: String!) { + getOrgLink(hash: $hash) { title description url @@ -1402,7 +1402,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error { } } }`) - op.Var("id", id) + op.Var("hash", hash) err = links.Execute(c.Request().Context(), op, &result) if err != nil { if graphError, ok := err.(*gqlclient.Error); ok { @@ -1674,7 +1674,7 @@ func (s *Service) UserFeed(c echo.Context) error { desc = l.BaseURLData.Meta.Description } item := links.Item{ - GUID: fmt.Sprintf("%s%s", domain, c.Echo().Reverse(s.RouteName("link_detail"), l.ID)), + GUID: fmt.Sprintf("%s%s", domain, c.Echo().Reverse(s.RouteName("link_detail"), l.Hash)), Title: l.Title, Description: desc, Link: l.URL, @@ -1764,7 +1764,7 @@ func (s *Service) OrgLinksList(c echo.Context) error { starred archiveUrl type - noteHash + hash tags { id name @@ -1997,7 +1997,7 @@ func (s *Service) OrgLinksList(c echo.Context) error { desc = l.BaseURLData.Meta.Description } item := links.Item{ - GUID: fmt.Sprintf("%s%s", domain, c.Echo().Reverse(s.RouteName("link_detail"), l.ID)), + GUID: fmt.Sprintf("%s%s", domain, c.Echo().Reverse(s.RouteName("link_detail"), l.Hash)), Title: l.Title, Description: desc, Link: l.URL, @@ -2072,8 +2072,8 @@ func (s *Service) OrgLinksList(c echo.Context) error { } func (s *Service) OrgLinkRedirect(c echo.Context) error { - id, err := strconv.Atoi(c.Param("id")) - if err != nil { + hash := c.Param("hash") + if hash == "" { return echo.NotFoundHandler(c) } type GraphQLResponse struct { @@ -2082,14 +2082,14 @@ func (s *Service) OrgLinkRedirect(c echo.Context) error { var result GraphQLResponse op := gqlclient.NewOperation( - `query getOrgLink($id: Int) { - getOrgLink(id: $id) { - id + `query getOrgLink($hash: String!) { + getOrgLink(hash: $hash) { + hash url } }`) - op.Var("id", id) - err = links.Execute(c.Request().Context(), op, &result) + op.Var("hash", hash) + err := links.Execute(c.Request().Context(), op, &result) if err != nil { if graphError, ok := err.(*gqlclient.Error); ok { err = links.ParseInputErrors(c, graphError, gobwebs.Map{}) @@ -2101,8 +2101,8 @@ func (s *Service) OrgLinkRedirect(c echo.Context) error { // OrgLinkDetail ... func (s *Service) OrgLinkDetail(c echo.Context) error { - id, err := strconv.Atoi(c.Param("id")) - if err != nil { + hash := c.Param("hash") + if hash == "" { return echo.NotFoundHandler(c) } type GraphQLResponse struct { @@ -2111,14 +2111,15 @@ func (s *Service) OrgLinkDetail(c echo.Context) error { var result GraphQLResponse op := gqlclient.NewOperation( - `query getOrgLink($id: Int) { - getOrgLink(id: $id) { + `query getOrgLink($hash: String!) { + getOrgLink(hash: $hash) { id title description url userId type + hash createdOn tags { id @@ -2127,8 +2128,8 @@ func (s *Service) OrgLinkDetail(c echo.Context) error { } } }`) - op.Var("id", id) - err = links.Execute(c.Request().Context(), op, &result) + op.Var("hash", hash) + err := links.Execute(c.Request().Context(), op, &result) if err != nil { if graphError, ok := err.(*gqlclient.Error); ok { err = links.ParseInputErrors(c, graphError, gobwebs.Map{}) @@ -2367,7 +2368,7 @@ func (s *Service) OrgLinkUpdate(c echo.Context) error { } messages.Success(c, lt.Translate("Link successfully updated.")) return c.Redirect(http.StatusMovedPermanently, - c.Echo().Reverse(s.RouteName("link_detail"), result.Link.ID)) + c.Echo().Reverse(s.RouteName("link_detail"), result.Link.Hash)) } var canEdit bool @@ -3218,7 +3219,7 @@ func (s *Service) NoteDetail(c echo.Context) error { var result GraphQLResponse op := gqlclient.NewOperation( - `query getOrgLink($hash: String) { + `query getOrgLink($hash: String!) { getOrgLink(hash: $hash) { id title @@ -3375,15 +3376,15 @@ func (s *Service) NoteCreate(c echo.Context) error { OrgSlug: formSlug, } - id := c.QueryParam("linkid") - if id != "" { + hash := c.QueryParam("linkid") + if hash != "" { type GraphQLResponse struct { Link models.OrgLink `json:"getOrgLink"` } var result GraphQLResponse op := gqlclient.NewOperation( - `query getOrgLink($hash: String) { + `query getOrgLink($hash: String!) { getOrgLink(hash: $hash) { title description @@ -3394,7 +3395,7 @@ func (s *Service) NoteCreate(c echo.Context) error { } } }`) - op.Var("hash", id) + op.Var("hash", hash) err = links.Execute(c.Request().Context(), op, &result) if err != nil { if graphError, ok := err.(*gqlclient.Error); ok { diff --git a/core/routes_test.go b/core/routes_test.go index b35da28..5dbfee9 100644 --- a/core/routes_test.go +++ b/core/routes_test.go @@ -302,14 +302,14 @@ func TestHandlers(t *testing.T) { Context: e.NewContext(request, recorder), User: loggedInUser, } - ctx.SetPath("/link/:id/edit") - ctx.SetParamNames("id") - ctx.SetParamValues("1") + ctx.SetPath("/link/:hash") + ctx.SetParamNames("hash") + ctx.SetParamValues("abcdefg") // link hash is "abcdefg" in mock json file // We get the error from the API call err = test.MakeRequestWithDomain(srv, coreService.OrgLinkDetail, ctx, domains[0]) c.NoError(err) htmlBody := recorder.Body.String() - c.True(strings.Contains(htmlBody, "/click/30")) // link ID is 30 in mock json file + c.True(strings.Contains(htmlBody, "/click/abcdefg")) }) t.Run("link delete error", func(t *testing.T) { diff --git a/core/samples/detail_link.json b/core/samples/detail_link.json index 10d5cd9..4570b2a 100644 --- a/core/samples/detail_link.json +++ b/core/samples/detail_link.json @@ -2,6 +2,7 @@ "data": { "getOrgLink": { "id": 30, + "hash": "abcdefg", "title": "Detail org", "url": "https://www.detail.org", "baseUrlId": { diff --git a/migrations/0001_initial.up.sql b/migrations/0001_initial.up.sql index b23d6b6..352d3a3 100644 --- a/migrations/0001_initial.up.sql +++ b/migrations/0001_initial.up.sql @@ -100,7 +100,7 @@ CREATE TABLE org_links ( url TEXT NOT NULL, description TEXT DEFAULT '', "type" INT NOT NULL DEFAULT 0, - note_hash TEXT DEFAULT '', + hash VARCHAR(128) UNIQUE NOT NULL DEFAULT substr(encode(sha256(random()::text::bytea), 'hex'), 0, 27), base_url_id INT REFERENCES base_urls (id) ON DELETE CASCADE, org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL, user_id INT REFERENCES users (id) ON DELETE SET NULL, diff --git a/models/models.go b/models/models.go index c220fc2..f198860 100644 --- a/models/models.go +++ b/models/models.go @@ -90,7 +90,7 @@ type OrgLink struct { Starred bool `db:"starred" json:"starred"` ArchiveURL string `db:"archive_url" json:"archiveUrl"` Type int `db:"type" json:"type"` - NoteHash string `db:"note_hash" json:"noteHash"` + Hash string `db:"hash" json:"hash"` CreatedOn time.Time `db:"created_on" json:"createdOn"` UpdatedOn time.Time `db:"updated_on" json:"updatedOn"` @@ -346,7 +346,7 @@ type ExportOrgLink struct { Visibility int `db:"visibility" json:"visibility"` Unread bool `db:"unread" json:"unread"` Starred bool `db:"starred" json:"starred"` - NoteHash string `db:"note_hash" json:"note_hash"` + Hash string `db:"hash" json:"hash"` Tags []ExportTag `db:"-" json:"tags"` CreatedOn time.Time `db:"created_on" json:"created_on"` } diff --git a/models/org_link.go b/models/org_link.go index 8fb64c7..03705b2 100644 --- a/models/org_link.go +++ b/models/org_link.go @@ -36,7 +36,7 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink, q := opts.GetBuilder(nil) rows, err := q. Columns("ol.id", "ol.title", "ol.url", "ol.description", "ol.base_url_id", "ol.org_id", "ol.user_id", - "ol.visibility", "ol.unread", "ol.starred", "ol.archive_url", "ol.type", "ol.note_hash", + "ol.visibility", "ol.unread", "ol.starred", "ol.archive_url", "ol.type", "ol.hash", "ol.created_on", "ol.updated_on", "o.slug", "u.full_name", "json_agg(t)::jsonb", "b.data"). From("org_links ol"). Join("organizations o ON o.id = ol.org_id"). @@ -63,7 +63,7 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink, var o OrgLink var tags string if err = rows.Scan(&o.ID, &o.Title, &o.URL, &o.Description, &o.BaseURLID, &o.OrgID, - &o.UserID, &o.Visibility, &o.Unread, &o.Starred, &o.ArchiveURL, &o.Type, &o.NoteHash, + &o.UserID, &o.Visibility, &o.Unread, &o.Starred, &o.ArchiveURL, &o.Type, &o.Hash, &o.CreatedOn, &o.UpdatedOn, &o.OrgSlug, &o.Author, &tags, &o.BaseURLData); err != nil { return err } @@ -104,7 +104,7 @@ func (o *OrgLink) Load(ctx context.Context) error { err := database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error { err := sq. Select("id", "title", "url", "description", "base_url_id", "org_id", "user_id", - "visibility", "unread", "starred", "archive_url", "type", "note_hash", + "visibility", "unread", "starred", "archive_url", "type", "hash", "created_on", "updated_on"). From("org_links"). Where("id = ?", o.ID). @@ -112,7 +112,7 @@ func (o *OrgLink) Load(ctx context.Context) error { RunWith(tx). ScanContext(ctx, &o.ID, &o.Title, &o.URL, &o.Description, &o.BaseURLID, &o.OrgID, &o.UserID, &o.Visibility, &o.Unread, &o.Starred, &o.ArchiveURL, - &o.Type, &o.NoteHash, &o.CreatedOn, &o.UpdatedOn) + &o.Type, &o.Hash, &o.CreatedOn, &o.UpdatedOn) if err != nil { if err == sql.ErrNoRows { return nil @@ -133,9 +133,9 @@ func (o *OrgLink) Store(ctx context.Context) error { err = sq. Insert("org_links"). Columns("title", "url", "description", "base_url_id", "org_id", "user_id", "visibility", - "unread", "starred", "archive_url", "type", "note_hash"). + "unread", "starred", "archive_url", "type", "hash"). Values(o.Title, o.URL, o.Description, o.BaseURLID, o.OrgID, o.UserID, o.Visibility, - o.Unread, o.Starred, o.ArchiveURL, o.Type, o.NoteHash). + o.Unread, o.Starred, o.ArchiveURL, o.Type, o.Hash). Suffix(`RETURNING id, created_on, updated_on`). PlaceholderFormat(sq.Dollar). RunWith(tx). @@ -154,7 +154,7 @@ func (o *OrgLink) Store(ctx context.Context) error { Set("starred", o.Starred). Set("archive_url", o.ArchiveURL). Set("type", o.Type). - Set("note_hash", o.NoteHash). + Set("hash", o.Hash). Where("id = ?", o.ID). Suffix(`RETURNING (updated_on)`). PlaceholderFormat(sq.Dollar). @@ -208,11 +208,7 @@ func (o *OrgLink) TagsToString() string { // to a users bookmarks func (o *OrgLink) QueryParams() url.Values { qs := url.Values{} - if o.Type == NoteType { - qs.Set("linkid", o.NoteHash) - } else { - qs.Set("linkid", fmt.Sprint(o.ID)) - } + qs.Set("linkid", o.Hash) return qs } @@ -265,11 +261,11 @@ func OrgLinkStoreBatch(ctx context.Context, links []*OrgLink) error { batch := sq. Insert("org_links"). Columns("title", "url", "description", "base_url_id", "org_id", "user_id", "visibility", - "note_hash", "type", "unread") + "hash", "type", "unread") for _, link := range links { batch = batch.Values(link.Title, link.URL, link.Description, link.BaseURLID, link.OrgID, - link.UserID, link.Visibility, link.NoteHash, link.Type, link.Unread) + link.UserID, link.Visibility, link.Hash, link.Type, link.Unread) } _, err := batch. PlaceholderFormat(sq.Dollar). @@ -290,7 +286,7 @@ func ExportOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*Expor q := opts.GetBuilder(nil) rows, err := q. Columns("ol.id", "ol.title", "ol.url", "ol.description", "ol.visibility", - "ol.unread", "ol.starred", "ol.note_hash", "ol.created_on", "json_agg(t)::jsonb"). + "ol.unread", "ol.starred", "ol.hash", "ol.created_on", "json_agg(t)::jsonb"). From("org_links ol"). LeftJoin("tag_links tl ON tl.org_link_id = ol.id"). LeftJoin("tags t ON t.id = tl.tag_id"). @@ -311,7 +307,7 @@ func ExportOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*Expor var o ExportOrgLink var tags string if err = rows.Scan(&o.ID, &o.Title, &o.URL, &o.Description, &o.Visibility, - &o.Unread, &o.Starred, &o.NoteHash, &o.CreatedOn, &tags); err != nil { + &o.Unread, &o.Starred, &o.Hash, &o.CreatedOn, &tags); err != nil { return err } re := regexp.MustCompile(`(,\s)?null,?`) diff --git a/models/schema.sql b/models/schema.sql index b23d6b6..73cc957 100644 --- a/models/schema.sql +++ b/models/schema.sql @@ -1,3 +1,5 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + CREATE OR REPLACE FUNCTION update_updated_on_column() RETURNS TRIGGER AS $$ BEGIN @@ -100,7 +102,7 @@ CREATE TABLE org_links ( url TEXT NOT NULL, description TEXT DEFAULT '', "type" INT NOT NULL DEFAULT 0, - note_hash TEXT DEFAULT '', + hash VARCHAR(128) UNIQUE NOT NULL DEFAULT substr(encode(sha256(random()::text::bytea), 'hex'), 0, 27), base_url_id INT REFERENCES base_urls (id) ON DELETE CASCADE, org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL, user_id INT REFERENCES users (id) ON DELETE SET NULL, diff --git a/templates/link_detail.html b/templates/link_detail.html index d89fd97..1607fb2 100644 --- a/templates/link_detail.html +++ b/templates/link_detail.html @@ -10,7 +10,7 @@
-

{{.link.Title}}

+

{{.link.Title}}

{{if .IsPrivate}} @@ -29,7 +29,7 @@
{{if eq .link.Type 0}} -

{{stripCommonProtocol .link.URL}}

+

{{stripCommonProtocol .link.URL}}

{{end}}

{{.link.Description}}

{{if .link.Tags}} -- 2.45.2