Skip to content

Commit 99d4cef

Browse files
committed
feature - make sharable links for owned bookmarks
1 parent 93eabfe commit 99d4cef

24 files changed

+196
-28
lines changed

backend/package-lock.json

+17-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"showdown": "^1.9.1",
3232
"superagent": "^4.1.0",
3333
"swagger-ui-express": "^4.3.0",
34+
"uuid": "^9.0.0",
3435
"yamljs": "^0.3.0"
3536
},
3637
"devDependencies": {

backend/src/model/bookmark.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const mongoose = require('mongoose');
22
const Schema = mongoose.Schema;
33

44
const bookmarkSchema = new Schema({
5+
shareableId: {type:String, select: false},
56
name: {type:String, required: true},
67
location: {type:String, required: true},
78
description: String,

backend/src/routes/public/public-bookmarks.router.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ router.get('/', async (request, response, next) => {
1616
const sort = request.query.sort;
1717
const {page, limit} = PaginationQueryParamsHelper.getPageAndLimit(request);
1818

19-
if (searchText) {
19+
if ( searchText ) {
2020
const bookmarks = await publicBookmarksSearchService.findPublicBookmarks(searchText, page, limit, sort, searcMode);
2121
response.send(bookmarks);
2222
} else {
@@ -29,7 +29,7 @@ router.get('/', async (request, response, next) => {
2929
*/
3030
router.get('/', async (request, response, next) => {
3131
const location = request.query.location;
32-
if (location) {
32+
if ( location ) {
3333
const bookmarksForLocation = await PublicBookmarksService.getPublicBookmarkByLocation(location);
3434

3535
return response.send(bookmarksForLocation);
@@ -38,11 +38,20 @@ router.get('/', async (request, response, next) => {
3838
}
3939
});
4040

41+
/**
42+
* Get Bookmark by shareableId
43+
*/
44+
router.get('/shared/:shareableId', async (request, response, next) => {
45+
const shareableId = request.params.shareableId;
46+
const sharedBookmark = await PublicBookmarksService.getBookmarkBySharableId(shareableId);
47+
48+
return response.json(sharedBookmark);
49+
});
50+
4151
/**
4252
* When no filter send latest public bookmarks
4353
*/
4454
router.get('/', async (request, response) => {
45-
4655
const {page, limit} = PaginationQueryParamsHelper.getPageAndLimit(request);
4756
const bookmarks = await PublicBookmarksService.getLatestPublicBookmarks(page, limit);
4857

@@ -71,7 +80,7 @@ router.get('/tagged/:tag', async (request, response) => {
7180
/* GET title of bookmark given its url - might be moved to front-end */
7281
router.get('/scrape', async function (request, response, next) {
7382
const location = request.query.location;
74-
if (location) {
83+
if ( location ) {
7584
const webpageData = await PublicBookmarksService.getScrapedDataForLocation(location);
7685

7786
return response.send(webpageData);

backend/src/routes/public/public-bookmarks.service.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,20 @@ let getBookmarkById = async function (bookmarkId) {
6262
return bookmark;
6363
};
6464

65-
let getMostUsedPublicTags = async function (limit) {
65+
/* GET bookmark of user by shareableId */
66+
let getBookmarkBySharableId = async (shareableId) => {
67+
const bookmark = await Bookmark.findOne({
68+
shareableId: shareableId
69+
}).select('+shareableId');
70+
71+
if ( !bookmark ) {
72+
throw new NotFoundError(`Bookmark NOT_FOUND for shareableId: ${shareableId}`);
73+
} else {
74+
return bookmark;
75+
}
76+
};
6677

78+
let getMostUsedPublicTags = async function (limit) {
6779
const aggregatedTags = await Bookmark.aggregate([
6880
//first stage - filter
6981
{
@@ -112,5 +124,6 @@ module.exports = {
112124
getLatestPublicBookmarks: getLatestPublicBookmarks,
113125
getBookmarksForTag: getBookmarksForTag,
114126
getBookmarkById: getBookmarkById,
127+
getBookmarkBySharableId: getBookmarkBySharableId,
115128
getMostUsedPublicTags: getMostUsedPublicTags
116129
};

backend/src/routes/users/bookmarks/personal-bookmarks.router.js

+10
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ personalBookmarksRouter.get('/:bookmarkId', keycloak.protect(), async (request,
115115
return response.status(HttpStatus.OK).send(bookmark);
116116
});
117117

118+
/* GET sharableId for bookmark */
119+
personalBookmarksRouter.get('/shareable/:bookmarkId', keycloak.protect(), async (request, response) => {
120+
UserIdValidator.validateUserId(request);
121+
122+
const {userId, bookmarkId} = request.params;
123+
const shareableId = await PersonalBookmarksService.getOrCreateSharableId(userId, bookmarkId);
124+
const sharableIdResponse = {"shareableId": shareableId}
125+
return response.json(sharableIdResponse);
126+
});
127+
118128
/**
119129
* full UPDATE via PUT - that is the whole document is required and will be updated
120130
* the descriptionHtml parameter is only set in backend, if only does not come front-end (might be an API call)

backend/src/routes/users/bookmarks/personal-bookmarks.service.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const User = require('../../../model/user');
44
const NotFoundError = require('../../../error/not-found.error');
55

66
const BookmarkInputValidator = require('../../../common/bookmark-input.validator');
7+
const {
8+
v4: uuidv4,
9+
} = require('uuid');
710

811
/**
912
* CREATE bookmark for user
@@ -191,6 +194,32 @@ let increaseOwnerVisitCount = async (userId, bookmarkId) => {
191194
);
192195
}
193196

197+
let getOrCreateShareableId = async (userId, bookmarkId) => {
198+
const bookmark = await Bookmark.findOne(
199+
{
200+
_id: bookmarkId,
201+
userId: userId
202+
}).select('+shareableId');
203+
204+
if ( bookmark.shareableId ) {
205+
return bookmark.shareableId
206+
} else {
207+
const uuid = uuidv4();
208+
const updatedBookmark = await Bookmark.findOneAndUpdate(
209+
{
210+
_id: bookmarkId,
211+
userId: userId
212+
},
213+
{
214+
$set: {shareableId: uuid}
215+
},
216+
{new: true}
217+
).select('+shareableId');
218+
219+
return updatedBookmark.shareableId;
220+
}
221+
}
222+
194223

195224
/*
196225
* DELETE bookmark for user
@@ -295,5 +324,6 @@ module.exports = {
295324
deleteBookmarkByLocation: deleteBookmarkByLocation,
296325
deletePrivateBookmarksByTag: deletePrivateBookmarksByTag,
297326
getUserTagsAggregated: getUserTagsAggregated,
298-
updateDisplayNameInBookmarks: updateDisplayNameInBookmarks
327+
updateDisplayNameInBookmarks: updateDisplayNameInBookmarks,
328+
getOrCreateSharableId: getOrCreateShareableId
299329
};

frontend/src/app/core/auth/auth-guard.service.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Injectable } from '@angular/core';
22
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
33
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
4-
import { environment } from '../../../environments/environment';
54

65
@Injectable()
76
export class AuthGuard extends KeycloakAuthGuard {

frontend/src/app/core/model/bookmark.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface Bookmark {
22
_id?: string;
3+
shareableId?: string;
34
name: string;
45
location: string;
56
description?: string;

frontend/src/app/core/personal-bookmarks.service.ts

+6
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ export class PersonalBookmarksService {
9393
.pipe(shareReplay(1));
9494
}
9595

96+
createOrGetShareableId(userId: string, bookmarkId: string): Observable<any> {
97+
return this.httpClient
98+
.get<string>(`${this.personalBookmarksApiBaseUrl}/${userId}/bookmarks/shareable/${bookmarkId}`)
99+
.pipe(shareReplay(1));
100+
}
101+
96102
increaseOwnerVisitCount(bookmark: Bookmark) {
97103
return this.httpClient
98104
.post(`${this.personalBookmarksApiBaseUrl}/${bookmark.userId}/bookmarks/${bookmark._id}/owner-visits/inc`, {},

frontend/src/app/public/bookmarks/public-bookmark-details.component.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Component, OnInit } from '@angular/core';
2-
import { UserInfoStore } from '../../core/user/user-info.store';
32
import { ActivatedRoute } from '@angular/router';
43
import { Observable, of } from 'rxjs';
54
import { Bookmark } from '../../core/model/bookmark';
@@ -15,7 +14,6 @@ export class PublicBookmarkDetailsComponent implements OnInit {
1514

1615
constructor(
1716
private publicBookmarksService: PublicBookmarksService,
18-
private userInfoStore: UserInfoStore,
1917
private route: ActivatedRoute) {
2018
}
2119

frontend/src/app/public/bookmarks/public-bookmarks.service.ts

+5
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export class PublicBookmarksService {
5151
.get<Bookmark>(`${this.publicBookmarksApiBaseUrl}/${bookmarkId}`);
5252
}
5353

54+
getSharedBookmarkBySharableId(shareableId: string): Observable<Bookmark> {
55+
return this.httpClient
56+
.get<Bookmark>(`${this.publicBookmarksApiBaseUrl}/shared/${shareableId}`);
57+
}
58+
5459
getMostUsedPublicTags(limit: number): Observable<UsedTag[]> {
5560
const params = new HttpParams()
5661
.set('limit', limit.toString());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div *ngIf="(bookmark$ | async) as bookmark">
2+
<app-bookmark-list-element
3+
[bookmark]="bookmark"
4+
[isDetailsPage]="true"
5+
[showMoreText]="true">
6+
</app-bookmark-list-element>
7+
</div>

frontend/src/app/public/bookmarks/shareable-bookmark-details/shareable-bookmark-details.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { Bookmark } from '../../../core/model/bookmark';
3+
import { ActivatedRoute } from '@angular/router';
4+
import { Observable } from 'rxjs';
5+
import { PublicBookmarksService } from '../public-bookmarks.service';
6+
7+
8+
@Component({
9+
selector: 'app-bookmark-details',
10+
templateUrl: './shareable-bookmark-details.component.html',
11+
styleUrls: ['./shareable-bookmark-details.component.scss']
12+
})
13+
export class ShareableBookmarkDetailsComponent implements OnInit {
14+
15+
bookmark$: Observable<Bookmark>;
16+
popup: string;
17+
18+
constructor(
19+
private route: ActivatedRoute,
20+
private publicBookmarksService: PublicBookmarksService
21+
) {
22+
}
23+
24+
ngOnInit() {
25+
const shareableId = this.route.snapshot.paramMap.get('shareableId');
26+
this.bookmark$ = this.publicBookmarksService.getSharedBookmarkBySharableId(shareableId);
27+
}
28+
29+
}

frontend/src/app/public/public-routing.module.ts

+8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { ExtensionsPageComponent } from './extensions/extensions-page.component'
1313
import { AboutComponent } from './about/about.component';
1414
import { RegisterComponent } from './register/register.component';
1515
import { PublicBookmarkDetailsComponent } from './bookmarks/public-bookmark-details.component';
16+
import {
17+
ShareableBookmarkDetailsComponent
18+
} from './bookmarks/shareable-bookmark-details/shareable-bookmark-details.component';
1619

1720
const publicRoutes: Routes = [
1821
{
@@ -125,6 +128,10 @@ const publicRoutes: Routes = [
125128
}
126129
]
127130
},
131+
{
132+
path: 'bookmarks/shared/:shareableId',
133+
component: ShareableBookmarkDetailsComponent
134+
},
128135
{
129136
path: 'bookmarks/:id',
130137
component: PublicBookmarkDetailsComponent,
@@ -138,6 +145,7 @@ const publicRoutes: Routes = [
138145
}
139146
]
140147
},
148+
141149
{
142150
path: 'users/:userId',
143151
component: UserPublicProfileComponent,

frontend/src/app/public/public.module.ts

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import { ExtensionsPageComponent } from './extensions/extensions-page.component'
2525
import { AboutComponent } from './about/about.component';
2626
import { RegisterComponent } from './register/register.component';
2727
import { PublicBookmarkDetailsComponent } from './bookmarks/public-bookmark-details.component';
28+
import {
29+
ShareableBookmarkDetailsComponent
30+
} from './bookmarks/shareable-bookmark-details/shareable-bookmark-details.component';
2831

2932
@NgModule({
3033
declarations : [
@@ -41,6 +44,7 @@ import { PublicBookmarkDetailsComponent } from './bookmarks/public-bookmark-deta
4144
PublicSnippetsComponent,
4245
PublicBookmarkDetailsComponent,
4346
SnippetTaggedComponent,
47+
ShareableBookmarkDetailsComponent
4448
],
4549
imports: [
4650
SharedModule,

frontend/src/app/shared/bookmark-list-element/bookmark-list-element.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ <h6 class="card-subtitle mb-2 text-muted url-under-title">
180180
<button
181181
class="btn btn-light btn-sm float-right"
182182
title="Share via email or on social media"
183-
(click)="shareBookmark(bookmark)"><i class="fas fa-share"></i> Share
183+
(click)="shareBookmarkDialog(bookmark)"><i class="fas fa-share"></i> Share
184184
</button>
185185
</div>
186186
</div>

frontend/src/app/shared/bookmark-list-element/bookmark-list-element.component.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export class BookmarkListElementComponent extends TagFollowingBaseComponent impl
236236
this.router.navigate(['/']);
237237
}
238238

239-
shareBookmark(bookmark: Bookmark) {
239+
shareBookmarkDialog(bookmark: Bookmark) {
240240

241241
const dialogConfig = new MatDialogConfig();
242242

@@ -245,6 +245,9 @@ export class BookmarkListElementComponent extends TagFollowingBaseComponent impl
245245
dialogConfig.minWidth = 380;
246246
dialogConfig.data = {
247247
bookmark: bookmark,
248+
userIsLoggedIn: this.userIsLoggedIn,
249+
userOwnsBookmark: this.bookmark.userId === this.userId,
250+
userId: this.userId
248251
};
249252

250253
const dialogRef = this.deleteDialog.open(SocialShareDialogComponent, dialogConfig);

frontend/src/app/shared/dialog/history-dialog/hot-keys-dialog.component.html

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ <h2 mat-dialog-title [innerHTML]="title"></h2>
1111
<mat-expansion-panel
1212
*ngFor="let bookmark of bookmarks | bookmarkFilter: filterText as filteredBookmarks; index as i"
1313
[expanded]="filteredBookmarks.length === 1">
14-
1514
<mat-expansion-panel-header *ngIf="i<15">
1615
<div class="p-3">
1716
<h5 class="card-title">

0 commit comments

Comments
 (0)