From 919380384d8ba878c456e25e0cdeba2253ade6f3 Mon Sep 17 00:00:00 2001 From: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Date: Sat, 23 Dec 2023 21:29:31 +0100 Subject: [PATCH 1/2] New Version (#31) * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Better error messages (#3) * fix: include error messages for validation in response * fix: better error messages * Update Frontend URL in Readme (#4) doc: new url for readme * Fix 2FA (#5) fix: allow re setup, when not completed * added 2FA frontend (#6) * added 2FA frontend * added setting status --------- Co-authored-by: Jannis Cloodt * fe/twoFA (#8) * added 2FA frontend * added setting status --------- Co-authored-by: Jannis Cloodt Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> * Fe/two fa (#12) * added 2FA frontend * added setting status * 2FA-Confirm Page --------- Co-authored-by: Jannis Cloodt Co-authored-by: janniscloodt <66444392+janniscloodt@users.noreply.github.com> * Svelte Components (#11) * feat: add navigation component * feat: fix type errors on root layout * feat: add input component * Regex Fix for Registration (#7) fix: regex fix for username and password in registration * Disable 2FA Functionality (#10) feat: add disable 2fa functionality * Add preview and image to posts (#9) * feat: add preview and image to posts * feat: add new fields to dto * Homepage (#13) * be/Feed (#26) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * be/ReadMe (#28) * fix: remove jpa stuff from configuration * doc: Add backend README.md * ButtonDone Component * Error handling in register Form * updated Homepage --------- Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Co-authored-by: Jannis Cloodt * be/SSOLogin (#14) * feat: add sso * feat: add sso frontend * feat: refactor sso service * feat: add github sso provider * feat: add github sso in frontend * fix: expose verify and get method on sso providers * feat: add configure sso functionality * feat: refactor sso in backend * feat: add configure sso in frontend * feat: add remove sso provider functionality * feat: add user service test * feat: add feed service test * fix: feed service test fix * 2FA Disable FE (#16) * be/Feed (#26) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * be/ReadMe (#28) * fix: remove jpa stuff from configuration * doc: Add backend README.md * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * modal * disable 2fa works * disable 2fa works v2 --------- Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Co-authored-by: Jannis Cloodt * showBlogPage (#18) * be/Feed (#26) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * be/ReadMe (#28) * fix: remove jpa stuff from configuration * doc: Add backend README.md * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * modal * disable 2fa works * disable 2fa works v2 * added page to show the selected Blog * updatd styling * Apply suggestions from code review Co-authored-by: Ngo <33123914+ngtungngo@users.noreply.github.com> * Update frontend/src/routes/blogs/+page.svelte Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> --------- Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Co-authored-by: Jannis Cloodt Co-authored-by: Ngo <33123914+ngtungngo@users.noreply.github.com> * BlogPage Alert Update (#15) * be/Feed (#26) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * be/ReadMe (#28) * fix: remove jpa stuff from configuration * doc: Add backend README.md * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect --------- Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Co-authored-by: Jannis Cloodt * fix: some stuff * fix: remove description character descriptions * blogEndpoint (#20) * be/Feed (#26) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * be/ReadMe (#28) * fix: remove jpa stuff from configuration * doc: Add backend README.md * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * modal * disable 2fa works * disable 2fa works v2 * added page to show the selected Blog * updatd styling * Alert componet custom color * created endpoint to get Feed by ID --------- Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Co-authored-by: Jannis Cloodt * Quill Editor (#23) feat: add quill editor * ChangeBlogState (#25) * be/Feed (#26) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * be/ReadMe (#28) * fix: remove jpa stuff from configuration * doc: Add backend README.md * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * modal * disable 2fa works * disable 2fa works v2 * added page to show the selected Blog * updatd styling * Alert componet custom color * created endpoint to get Feed by ID * added page to manage own blogs * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * updated Alert * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * Alert componet custom color * added page to manage own blogs * MyBlogs shows blogs * MyBlogs delete blogs * editFeed done * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * updated Alert * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * Alert componet custom color * added page to manage own blogs * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * ButtonDone Component * Error handling in register Form * updated Homepage * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Alert componet custom color * created endpoint to get Feed by ID * added page to manage own blogs * MyBlogs shows blogs * MyBlogs delete blogs * editFeed done * Fixed Test --------- Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Co-authored-by: Jannis Cloodt * MyBlogs (#22) * be/Feed (#26) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * be/ReadMe (#28) * fix: remove jpa stuff from configuration * doc: Add backend README.md * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * modal * disable 2fa works * disable 2fa works v2 * added page to show the selected Blog * updatd styling * Alert componet custom color * created endpoint to get Feed by ID * added page to manage own blogs * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * updated Alert * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * Alert componet custom color * added page to manage own blogs * MyBlogs shows blogs * MyBlogs delete blogs * Fixed Test --------- Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Co-authored-by: Jannis Cloodt * editFeed (#24) * be/Feed (#26) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * be/ReadMe (#28) * fix: remove jpa stuff from configuration * doc: Add backend README.md * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * ButtonDone Component * Error handling in register Form * updated Homepage * updated Alert * Blog hover effect * added Modal to disable 2fa * modal * disable 2fa works * disable 2fa works v2 * added page to show the selected Blog * updatd styling * Alert componet custom color * created endpoint to get Feed by ID * added page to manage own blogs * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * updated Alert * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * Alert componet custom color * added page to manage own blogs * MyBlogs shows blogs * MyBlogs delete blogs * editFeed done * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * updated Alert * added Modal to disable 2fa * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Error handling in register Form * updated Homepage * Alert componet custom color * added page to manage own blogs * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * ButtonDone Component * Error handling in register Form * updated Homepage * Be/feed (#27) * fix: link author to post * feat: create post * feat: add owns post security expression * feat: add delete post endpoint * fix: handle anonymous auth in expression root * fix: access denied exception * fix: add update post endpoint * Alert componet custom color * created endpoint to get Feed by ID * added page to manage own blogs * MyBlogs shows blogs * MyBlogs delete blogs * editFeed done * Fixed Test --------- Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Co-authored-by: Jannis Cloodt * added endpoint to change feed status (#26) * added endpoint to change feed status * Fixed be changePostState Service --------- Co-authored-by: Jannis Cloodt Co-authored-by: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> * Notifications (#27) * feat: add notifications to backend * feat: add notifications to frontend * feat: add notifications info to frontend * Change to Public/Draft in FE (#28) * Change to Public/Draft in FE * Change to Public/Draft in FE * Edit your own feed on BlogPage * Added Comment Section FE BlogPage * error on settings Page * added comment Table --------- Co-authored-by: Jannis Cloodt * Profile (#30) * feat: add bio to user schema * feat: add get user post to feed service * feat: add profile endpoint * feat: add follower schema * feat: add follow/unfollow * feat: add follower and posts stats to profile * feat: add edit bio functionality * feat: add following bool * feat: add profile to frontend * feat: change schema version * feat: send notification on follow * be/commentToPost (#29) * added be to post Comments * updated BE to post Comments * error fix --------- Co-authored-by: Jannis Cloodt --------- Co-authored-by: janniscloodt <66444392+janniscloodt@users.noreply.github.com> Co-authored-by: Jannis Cloodt Co-authored-by: Ngo <33123914+ngtungngo@users.noreply.github.com> --- .idea/sqldialects.xml | 1 + backend/pom.xml | 11 + .../cloudwork/BackendApplication.java | 4 + .../com/oskarwiedeweg/cloudwork/BitUtils.java | 10 +- .../cloudwork/config/NotificationsConfig.java | 24 + .../cloudwork/feed/FeedController.java | 26 + .../cloudwork/feed/FeedService.java | 61 +- .../cloudwork/feed/dto/CommentPostDto.java | 14 + .../cloudwork/feed/dto/CreateCommentDto.java | 12 + .../cloudwork/feed/dto/PostDto.java | 1 + .../cloudwork/feed/dto/SinglePostDto.java | 24 + .../cloudwork/feed/post/Comment.java | 19 + .../cloudwork/feed/post/CommentRowMapper.java | 26 + .../cloudwork/feed/post/Post.java | 5 + .../cloudwork/feed/post/PostDao.java | 33 +- .../cloudwork/feed/post/PostRowMapper.java | 1 + .../notifications/NotificationType.java | 16 + .../NotificationsController.java | 39 + .../notifications/NotificationsDao.java | 42 + .../notifications/NotificationsService.java | 96 +++ .../UserNotificationEndpoint.java | 13 + .../UserNotificationsEndpointRowMapper.java | 21 + .../dto/EnableNotificationsDto.java | 12 + .../cloudwork/profiles/ProfileController.java | 42 + .../cloudwork/profiles/ProfileService.java | 50 ++ .../cloudwork/profiles/dto/ProfileDto.java | 29 + .../cloudwork/profiles/dto/UpdateBioDto.java | 10 + .../profiles/follower/FollowerDao.java | 29 + .../profiles/follower/FollowerService.java | 41 + .../oskarwiedeweg/cloudwork/user/User.java | 1 + .../oskarwiedeweg/cloudwork/user/UserDao.java | 4 + .../cloudwork/user/UserRowMapper.java | 3 +- .../src/main/resources/application-local.yml | 8 +- .../V2_3__add_draft_public_to_shema.sql | 1 + ...2_4__add_notification_endpoints_schema.sql | 8 + .../migration/V2_5__create_comment_shema.sql | 9 + .../db/migration/V2_6__add_follower.sql | 8 + .../db/migration/V2_7__add_bio_to_user.sql | 1 + .../cloudwork/feed/FeedServiceTest.java | 10 +- frontend/package-lock.json | 774 +++++++++++++++++- frontend/package.json | 6 +- frontend/src/app.d.ts | 1 + frontend/src/hooks.server.ts | 4 +- frontend/src/lib/assets/EditIcon.png | Bin 0 -> 7484 bytes .../components/ActivateNotifications.svelte | 34 + .../src/lib/components/DropDownMyBlogs.svelte | 54 ++ frontend/src/lib/components/Input.svelte | 6 +- .../src/lib/components/NavigationBar.svelte | 7 +- frontend/src/lib/notifications.ts | 62 ++ frontend/src/lib/user.ts | 1 + frontend/src/routes/+layout.svelte | 7 +- frontend/src/routes/FAConfirm/+page.svelte | 4 +- frontend/src/routes/blogs/+page.svelte | 7 +- .../src/routes/createFeed/+page.server.ts | 3 +- frontend/src/routes/createFeed/+page.svelte | 66 +- frontend/src/routes/editFeed/+page.server.ts | 53 +- frontend/src/routes/editFeed/+page.svelte | 139 +++- frontend/src/routes/myBlogs/+page.server.ts | 55 ++ frontend/src/routes/myBlogs/+page.svelte | 44 + .../profile/[profileId]/+page.server.ts | 44 + .../routes/profile/[profileId]/+page.svelte | 70 ++ frontend/src/routes/settings/+page.server.ts | 42 +- frontend/src/routes/settings/+page.svelte | 24 +- frontend/src/routes/showBlog/+page.server.ts | 8 +- frontend/src/routes/showBlog/+page.svelte | 59 +- .../src/routes/yourProfile/+page.server.ts | 9 + frontend/static/service-worker.js | 36 + frontend/tailwind.config.js | 11 + 68 files changed, 2318 insertions(+), 77 deletions(-) create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/config/NotificationsConfig.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/CommentPostDto.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/CreateCommentDto.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/SinglePostDto.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/Comment.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/CommentRowMapper.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationType.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsController.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsDao.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsService.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/UserNotificationEndpoint.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/UserNotificationsEndpointRowMapper.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/dto/EnableNotificationsDto.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/ProfileController.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/ProfileService.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/dto/ProfileDto.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/dto/UpdateBioDto.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/follower/FollowerDao.java create mode 100644 backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/follower/FollowerService.java create mode 100644 backend/src/main/resources/db/migration/V2_3__add_draft_public_to_shema.sql create mode 100644 backend/src/main/resources/db/migration/V2_4__add_notification_endpoints_schema.sql create mode 100644 backend/src/main/resources/db/migration/V2_5__create_comment_shema.sql create mode 100644 backend/src/main/resources/db/migration/V2_6__add_follower.sql create mode 100644 backend/src/main/resources/db/migration/V2_7__add_bio_to_user.sql create mode 100644 frontend/src/lib/assets/EditIcon.png create mode 100644 frontend/src/lib/components/ActivateNotifications.svelte create mode 100644 frontend/src/lib/components/DropDownMyBlogs.svelte create mode 100644 frontend/src/lib/notifications.ts create mode 100644 frontend/src/routes/myBlogs/+page.server.ts create mode 100644 frontend/src/routes/myBlogs/+page.svelte create mode 100644 frontend/src/routes/profile/[profileId]/+page.server.ts create mode 100644 frontend/src/routes/profile/[profileId]/+page.svelte create mode 100644 frontend/static/service-worker.js diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index ba3a34d..7732533 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/backend/pom.xml b/backend/pom.xml index e39ed10..c4add93 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -94,6 +94,17 @@ google-api-client 1.33.0 + + + nl.martijndwars + web-push + 5.1.1 + + + org.bouncycastle + bcpkix-jdk18on + 1.76 + diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/BackendApplication.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/BackendApplication.java index 067b452..1ee0650 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/BackendApplication.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/BackendApplication.java @@ -2,6 +2,7 @@ import com.oskarwiedeweg.cloudwork.feed.dto.PostDto; import com.oskarwiedeweg.cloudwork.feed.post.Post; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.modelmapper.ModelMapper; import org.modelmapper.TypeMap; import org.springframework.boot.SpringApplication; @@ -9,10 +10,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; +import java.security.Security; + @SpringBootApplication public class BackendApplication { public static void main(String[] args) { + Security.addProvider(new BouncyCastleProvider()); SpringApplication.run(BackendApplication.class, args); } diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/BitUtils.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/BitUtils.java index 920cb10..7c2ab4f 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/BitUtils.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/BitUtils.java @@ -20,8 +20,14 @@ public Long removeBit(Long bits, Long bit) { @Getter public enum SettingBits { - TWO_FACTOR_AUTH(1), - SETUP_TWO_FACTOR_AUTH(2); + // 2FA + TWO_FACTOR_AUTH(0b0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0001L), + SETUP_TWO_FACTOR_AUTH(0b0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0010L), + + // Notifications + NOTIFICATIONS_ALL(0b0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_1111_1111_1111_0000_0000_0000L), + NOTIFICATIONS_ENABLED(0b0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_1000_0000_0000_0000_0000_0000L), + NOTIFICATION_NEW_SUBSCRIBER(0b0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0001_0000_0000_0000L); private final long bit; diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/config/NotificationsConfig.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/config/NotificationsConfig.java new file mode 100644 index 0000000..7eee314 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/config/NotificationsConfig.java @@ -0,0 +1,24 @@ +package com.oskarwiedeweg.cloudwork.config; + +import nl.martijndwars.webpush.PushService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.security.GeneralSecurityException; + +@Configuration +public class NotificationsConfig { + + @Value("${notifications.keys.private}") + private String privateKey; + + @Value("${notifications.keys.public}") + private String publicKey; + + @Bean + public PushService pushService() throws GeneralSecurityException { + return new PushService(publicKey, privateKey); + } + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/FeedController.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/FeedController.java index cdc000e..6872311 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/FeedController.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/FeedController.java @@ -1,7 +1,9 @@ package com.oskarwiedeweg.cloudwork.feed; +import com.oskarwiedeweg.cloudwork.feed.dto.CreateCommentDto; import com.oskarwiedeweg.cloudwork.feed.dto.CreatePostDto; import com.oskarwiedeweg.cloudwork.feed.dto.FeedDto; +import com.oskarwiedeweg.cloudwork.feed.dto.SinglePostDto; import jakarta.validation.Valid; import lombok.Data; import org.springframework.http.HttpStatus; @@ -21,6 +23,23 @@ public FeedDto getFeed() { return feedService.getFeed(); } + @GetMapping("/{postId}") + public SinglePostDto getSpecificFeed(@PathVariable("postId") long postId) { + return feedService.getFeedById(postId); + } + + @GetMapping("/myFeed") + @PreAuthorize("isAuthenticated()") + public FeedDto myFeed(@AuthenticationPrincipal Long userId){ + return feedService.getMyFeeds(userId);} + + + @PostMapping("/updateState/{postId}") + @PreAuthorize("isAuthenticated()") + public void updatePostState(@PathVariable("postId") long postId){ + feedService.changePostsState(postId); + } + @PostMapping("/new") @ResponseStatus(HttpStatus.CREATED) @PreAuthorize("isAuthenticated()") @@ -40,4 +59,11 @@ public void updatePost(@PathVariable("postId") Long postId, @Valid @RequestBody feedService.updatePost(postId, body); } + @PostMapping("/comment/create/{postId}") + @PreAuthorize("isAuthenticated()") + public void createComment(@AuthenticationPrincipal Long userId, @PathVariable("postId") Long postId, @Valid @RequestBody CreateCommentDto body){ + feedService.createComment(userId, postId, body); + } + + } diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/FeedService.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/FeedService.java index d74023a..bcd9b68 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/FeedService.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/FeedService.java @@ -1,8 +1,8 @@ package com.oskarwiedeweg.cloudwork.feed; -import com.oskarwiedeweg.cloudwork.feed.dto.CreatePostDto; -import com.oskarwiedeweg.cloudwork.feed.dto.FeedDto; -import com.oskarwiedeweg.cloudwork.feed.dto.PostDto; +import com.oskarwiedeweg.cloudwork.feed.dto.*; +import com.oskarwiedeweg.cloudwork.feed.post.Comment; +import com.oskarwiedeweg.cloudwork.feed.post.Post; import com.oskarwiedeweg.cloudwork.feed.post.PostDao; import com.oskarwiedeweg.cloudwork.user.UserDto; import lombok.Data; @@ -24,7 +24,7 @@ public class FeedService { public FeedDto getFeed() { Map users = new HashMap<>(); - List posts = postDao.getPosts().stream() + List posts = postDao.getPublicPosts().stream() .peek(post -> users.put(post.getUser().getId(), modelMapper.map(post.getUser(), UserDto.class))) .map(post -> modelMapper.map(post, PostDto.class)) .toList(); @@ -32,6 +32,43 @@ public FeedDto getFeed() { return new FeedDto(posts, users); } + public SinglePostDto getFeedById(Long postId) { + Post post = postDao.findPostById(postId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found!")); + + List comments = postDao.getCommentsToPost(postId) + .stream() + .map(c -> modelMapper.map(c, CommentPostDto.class)) + .toList(); + + return new SinglePostDto( + post.getId(), + post.getTitle(), + post.getPreview(), + post.getDescription(), + post.getImage(), + post.getTimestamp(), + modelMapper.map(post.getUser(), UserDto.class), + comments + ); + } + + public FeedDto getMyFeeds(Long userId){ + Map users = new HashMap<>(); + List posts = postDao.getUserPosts(userId).stream() + .peek(post -> users.put(post.getUser().getId(), modelMapper.map(post.getUser(), UserDto.class))) + .map(post -> modelMapper.map(post, PostDto.class)) + .toList(); + return new FeedDto(posts, users); + } + + public void createComment(Long userId, Long postId, CreateCommentDto body) { + postDao.saveCommentToPost(userId, + postId, + body.getContent() + ); + } + public void createPost(Long userId, CreatePostDto body) { postDao.savePost(userId, body.getTitle(), @@ -57,4 +94,20 @@ public void updatePost(Long postId, CreatePostDto body) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Post with id '%s' not found!".formatted(postId)); } } + + public void changePostsState(Long postId) { + if (postDao.getPostState(postId).equals(Post.PUBLIC_STATE)) { + postDao.updatePostState(postId, Post.DRAFT_STATE); + } + else if(postDao.getPostState(postId).equals(Post.DRAFT_STATE)) { + postDao.updatePostState(postId, Post.PUBLIC_STATE); + } + } + + public List getUserPosts(Long userId, boolean allowDrafts) { + return postDao.getUserPosts(userId).stream() + .filter(post -> allowDrafts || post.getState().equals(Post.PUBLIC_STATE)) + .map(post -> modelMapper.map(post, PostDto.class)) + .toList(); + } } diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/CommentPostDto.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/CommentPostDto.java new file mode 100644 index 0000000..3f7821e --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/CommentPostDto.java @@ -0,0 +1,14 @@ +package com.oskarwiedeweg.cloudwork.feed.dto; + +import com.oskarwiedeweg.cloudwork.user.UserDto; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class CommentPostDto { + private Long id; + private UserDto author; + private String content; + private LocalDateTime timestamp; +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/CreateCommentDto.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/CreateCommentDto.java new file mode 100644 index 0000000..c477feb --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/CreateCommentDto.java @@ -0,0 +1,12 @@ +package com.oskarwiedeweg.cloudwork.feed.dto; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Data +public class CreateCommentDto { + + @NotEmpty(message = "Is empty.") + private String content; + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/PostDto.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/PostDto.java index 0bbc956..698defe 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/PostDto.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/PostDto.java @@ -13,5 +13,6 @@ public class PostDto { private String image; private LocalDateTime timestamp; private Long authorId; + private String state; } diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/SinglePostDto.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/SinglePostDto.java new file mode 100644 index 0000000..9c3c325 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/dto/SinglePostDto.java @@ -0,0 +1,24 @@ +package com.oskarwiedeweg.cloudwork.feed.dto; + +import com.oskarwiedeweg.cloudwork.user.UserDto; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@AllArgsConstructor +public class SinglePostDto { + + private Long id; + private String title; + private String preview; + private String description; + private String image; + private LocalDateTime timestamp; + private UserDto user; + + private List comments; + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/Comment.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/Comment.java new file mode 100644 index 0000000..9fdb85e --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/Comment.java @@ -0,0 +1,19 @@ +package com.oskarwiedeweg.cloudwork.feed.post; + +import com.oskarwiedeweg.cloudwork.user.User; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +public class Comment { + + private Long id; + private User author; + private Long post_id; + private String content; + private LocalDateTime timestamp; + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/CommentRowMapper.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/CommentRowMapper.java new file mode 100644 index 0000000..ddac943 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/CommentRowMapper.java @@ -0,0 +1,26 @@ +package com.oskarwiedeweg.cloudwork.feed.post; + +import com.oskarwiedeweg.cloudwork.user.UserRowMapper; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class CommentRowMapper implements RowMapper { + + private final UserRowMapper userRowMapper = new UserRowMapper(true); + + @Override + public Comment mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Comment( + rs.getLong("id"), + userRowMapper.mapRow(rs, rowNum), + rs.getLong("post_id"), + rs.getString("content"), + rs.getTimestamp("published_at").toLocalDateTime() + ); + } + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/Post.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/Post.java index 788ee8b..6f409b9 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/Post.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/Post.java @@ -7,11 +7,16 @@ @Data public class Post { + + public static final String PUBLIC_STATE = "public"; + public static final String DRAFT_STATE = "draft"; + private final Long id; private final String title; private final String preview; private final String description; private final String image; + private final String state; private final LocalDateTime timestamp; private final User user; diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/PostDao.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/PostDao.java index 53371a9..bbde4c3 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/PostDao.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/PostDao.java @@ -16,9 +16,21 @@ public class PostDao { private final JdbcTemplate jdbcTemplate; private final PostRowMapper rowMapper; + private final CommentRowMapper commentRowMapper; - public List getPosts() { - return jdbcTemplate.query("select posts.*, users.name as user_name from posts inner join users on posts.user_id = users.id order by posts.published_at desc", rowMapper); + public List getPublicPosts() { + return jdbcTemplate.query("select posts.*, users.name as user_name from posts " + + "inner join users on posts.user_id = users.id " + + "where posts.state = 'public' " + + "order by posts.published_at desc", rowMapper); + } + + public List getUserPosts(Long userId) { + return jdbcTemplate.query("select posts.*, users.name as user_name from posts " + + "inner join users on posts.user_id = users.id " + + "where users.id = ? " + + "order by posts.published_at desc", + rowMapper, userId); } public void savePost(Long userId, String title, String preview, String description, String image) { @@ -47,4 +59,21 @@ public boolean updatePost(Long postId, String title, String preview, String desc postId ) == 1; } + + public boolean updatePostState(Long postId, String state) { + return jdbcTemplate.update("update posts set state = ? where id = ?", state, postId) == 1; + } + + public String getPostState(Long postId) { + return jdbcTemplate.queryForObject("select posts.state from posts where posts.id = ?", (rs, rn) -> rs.getString("state"), postId); + } + + public void saveCommentToPost(Long postId, Long userId, String content) { + jdbcTemplate.update("insert into post_comments(user_id, post_id, content, published_at) values (?, ?, ?, ?)", userId, postId, content, Timestamp.valueOf(LocalDateTime.now(Clock.systemUTC()))); + } + + public List getCommentsToPost(Long postId) { + return jdbcTemplate.query("select post_comments.*, users.name as user_name from post_comments " + + "left join users on users.id = post_comments.id", commentRowMapper); + } } diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/PostRowMapper.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/PostRowMapper.java index 951a144..de9040c 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/PostRowMapper.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/feed/post/PostRowMapper.java @@ -24,6 +24,7 @@ public Post mapRow(ResultSet resultSet, int rowNum) throws SQLException { resultSet.getString("preview"), resultSet.getString("description"), resultSet.getString("image"), + resultSet.getString("state"), publishedAt.toLocalDateTime(), userPrefixedRowMapper.mapRow(resultSet, rowNum) ); diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationType.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationType.java new file mode 100644 index 0000000..b5dd305 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationType.java @@ -0,0 +1,16 @@ +package com.oskarwiedeweg.cloudwork.notifications; + +import com.oskarwiedeweg.cloudwork.BitUtils; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationType { + NEW_FOLLOWER(BitUtils.SettingBits.NOTIFICATION_NEW_SUBSCRIBER.getBit(), "New follower!", "%s just subscribed to your profile."); + + private final Long settingsBit; + private final String title; + private final String body; + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsController.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsController.java new file mode 100644 index 0000000..384da23 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsController.java @@ -0,0 +1,39 @@ +package com.oskarwiedeweg.cloudwork.notifications; + +import com.oskarwiedeweg.cloudwork.notifications.dto.EnableNotificationsDto; +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Data +@RestController +@RequestMapping("/v1/notifications") +public class NotificationsController { + + @Value("${notifications.keys.public}") + private String publicKey; + + private final NotificationsService notificationsService; + + @GetMapping("/key") + public Map key() { + return Map.of("public", publicKey); + } + + @PostMapping("/subscribe") + @PreAuthorize("isAuthenticated()") + public void subscribe(@AuthenticationPrincipal Long userId, @RequestBody EnableNotificationsDto body) { + notificationsService.subscribe(userId, body); + } + + @DeleteMapping("/unsubscribe") + @PreAuthorize("isAuthenticated()") + public void unsubscribe(@AuthenticationPrincipal Long userId) { + notificationsService.unsubscribe(userId); + } + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsDao.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsDao.java new file mode 100644 index 0000000..d64e997 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsDao.java @@ -0,0 +1,42 @@ +package com.oskarwiedeweg.cloudwork.notifications; + +import lombok.Data; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Data +@Repository +public class NotificationsDao { + + private final JdbcTemplate jdbcTemplate; + private final UserNotificationsEndpointRowMapper userNotificationsEndpointRowMapper; + + public Optional getUserConnection(Long userId) { + List query = jdbcTemplate.query("select * from user_notification_endpoints " + + "where user_id = ?", + userNotificationsEndpointRowMapper, + userId + ); + if (query.isEmpty()) { + return Optional.empty(); + } + return Optional.ofNullable(query.get(0)); + } + + public void saveUserConnection(Long userId, String endpoint, String key, String auth) { + jdbcTemplate.update("insert into user_notification_endpoints (user_id, endpoint, key, auth) " + + "values (?, ?, ?, ?) " + + "on conflict (user_id) do update " + + "set endpoint = ?, key = ?, auth = ?", + userId, + endpoint, key, auth, + endpoint, key, auth); + } + + public void deleteUserConnection(Long userId) { + jdbcTemplate.update("delete from user_notification_endpoints where user_id = ?", userId); + } +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsService.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsService.java new file mode 100644 index 0000000..8f88944 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/NotificationsService.java @@ -0,0 +1,96 @@ +package com.oskarwiedeweg.cloudwork.notifications; + +import com.oskarwiedeweg.cloudwork.BitUtils; +import com.oskarwiedeweg.cloudwork.notifications.dto.EnableNotificationsDto; +import com.oskarwiedeweg.cloudwork.user.User; +import com.oskarwiedeweg.cloudwork.user.UserDao; +import lombok.Data; +import nl.martijndwars.webpush.Notification; +import nl.martijndwars.webpush.PushService; +import nl.martijndwars.webpush.Subscription; +import org.jose4j.lang.JoseException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +@Data +@Service +public class NotificationsService { + + private final PushService pushService; + private final UserDao userDao; + private final NotificationsDao notificationsDao; + + public void sendNotification(User user, NotificationType notificationType, Object[] titleParams, Object[] descriptionParams) { + if (!BitUtils.hasBit(user.getSettings(), notificationType.getSettingsBit())) { + return; + } + + Subscription subscription = loadUserSubscription(user.getId()); + if (subscription == null) { + return; + } + + String payload = """ + { + "title": "%s", + "description": "%s" + } + """ + .formatted(notificationType.getTitle().formatted(titleParams), notificationType.getBody().formatted(descriptionParams)); + + try { + Notification notification = new Notification(subscription, payload); + pushService.send(notification); + } catch (GeneralSecurityException | IOException | JoseException | ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void subscribe(Long userId, EnableNotificationsDto body) { + User user = userDao.findUserById(userId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + boolean hadSubscription = loadUserSubscription(userId) != null; + notificationsDao.saveUserConnection(userId, body.getEndpoint(), body.getKey(), body.getAuth()); + + if (!hadSubscription) { + userDao.updateUserSettings(userId, + BitUtils.addBit( + user.getSettings(), + BitUtils.SettingBits.NOTIFICATIONS_ALL.getBit()) + ); + } + } + + public void unsubscribe(Long userId) { + User user = userDao.findUserById(userId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + notificationsDao.deleteUserConnection(userId); + userDao.updateUserSettings(userId, + BitUtils.removeBit( + user.getSettings(), + BitUtils.SettingBits.NOTIFICATIONS_ALL.getBit()) + ); + } + + private Subscription loadUserSubscription(Long user) { + Optional userConnection = notificationsDao.getUserConnection(user); + if (userConnection.isEmpty()) { + return null; + } + UserNotificationEndpoint userNotificationEndpoint = userConnection.get(); + + return new Subscription( + userNotificationEndpoint.getEndpoint(), + new Subscription.Keys( + userNotificationEndpoint.getKey(), + userNotificationEndpoint.getAuth() + ) + ); + } + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/UserNotificationEndpoint.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/UserNotificationEndpoint.java new file mode 100644 index 0000000..7a621a7 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/UserNotificationEndpoint.java @@ -0,0 +1,13 @@ +package com.oskarwiedeweg.cloudwork.notifications; + +import lombok.Data; + +@Data +public class UserNotificationEndpoint { + + private final Long id; + private final String endpoint; + private final String key; + private final String auth; + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/UserNotificationsEndpointRowMapper.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/UserNotificationsEndpointRowMapper.java new file mode 100644 index 0000000..f1a39f6 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/UserNotificationsEndpointRowMapper.java @@ -0,0 +1,21 @@ +package com.oskarwiedeweg.cloudwork.notifications; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class UserNotificationsEndpointRowMapper implements RowMapper { + @Override + public UserNotificationEndpoint mapRow(ResultSet rs, int rowNum) throws SQLException { + return new UserNotificationEndpoint( + rs.getLong("id"), + rs.getString("endpoint"), + rs.getString("key"), + rs.getString("auth") + ); + } + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/dto/EnableNotificationsDto.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/dto/EnableNotificationsDto.java new file mode 100644 index 0000000..cca2601 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/notifications/dto/EnableNotificationsDto.java @@ -0,0 +1,12 @@ +package com.oskarwiedeweg.cloudwork.notifications.dto; + +import lombok.Data; + +@Data +public class EnableNotificationsDto { + + private final String endpoint; + private final String key; + private final String auth; + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/ProfileController.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/ProfileController.java new file mode 100644 index 0000000..1afa453 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/ProfileController.java @@ -0,0 +1,42 @@ +package com.oskarwiedeweg.cloudwork.profiles; + +import com.oskarwiedeweg.cloudwork.profiles.dto.ProfileDto; +import com.oskarwiedeweg.cloudwork.profiles.dto.UpdateBioDto; +import com.oskarwiedeweg.cloudwork.profiles.follower.FollowerService; +import lombok.Data; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Data +@RestController +@RequestMapping("/v1/profile") +public class ProfileController { + + private final ProfileService profileService; + private final FollowerService followerService; + + @GetMapping("/{userId}") + public ProfileDto getUserProfile(@PathVariable Long userId, @AuthenticationPrincipal Long viewerId) { + return profileService.loadProfile(userId, viewerId); + } + + @PostMapping("/{userId}/follow") + @PreAuthorize("isAuthenticated()") + public void followUser(@PathVariable Long userId, @AuthenticationPrincipal Long follower) { + followerService.follow(follower, userId); + } + + @DeleteMapping("/{userId}/follow") + @PreAuthorize("isAuthenticated()") + public void unfollowUser(@PathVariable Long userId, @AuthenticationPrincipal Long follower) { + followerService.unfollow(follower, userId); + } + + @PutMapping("/bio") + @PreAuthorize("isAuthenticated()") + public void updateBio(@AuthenticationPrincipal Long userId, @RequestBody UpdateBioDto body) { + profileService.updateBio(userId, body); + } + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/ProfileService.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/ProfileService.java new file mode 100644 index 0000000..f49867d --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/ProfileService.java @@ -0,0 +1,50 @@ +package com.oskarwiedeweg.cloudwork.profiles; + +import com.oskarwiedeweg.cloudwork.feed.FeedService; +import com.oskarwiedeweg.cloudwork.feed.dto.PostDto; +import com.oskarwiedeweg.cloudwork.profiles.dto.ProfileDto; +import com.oskarwiedeweg.cloudwork.profiles.dto.UpdateBioDto; +import com.oskarwiedeweg.cloudwork.profiles.follower.FollowerService; +import com.oskarwiedeweg.cloudwork.user.User; +import com.oskarwiedeweg.cloudwork.user.UserDto; +import com.oskarwiedeweg.cloudwork.user.UserService; +import lombok.Data; +import org.modelmapper.ModelMapper; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@Data +@Service +public class ProfileService { + + private final UserService userService; + private final FeedService feedService; + private final FollowerService followerService; + private final ModelMapper modelMapper; + + public ProfileDto loadProfile(Long userId, Long viewerId) { + User user = userService.getUserDao() + .findUserById(userId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + List userPosts = feedService.getUserPosts(userId, userId.equals(viewerId)); + + return ProfileDto.builder() + .userInfo(modelMapper.map(user, UserDto.class)) + .bio(user.getBio()) + .posts(userPosts) + .stats(new ProfileDto.Stats( + followerService.getFollowerCount(userId), + userPosts.size()) + ) + .following(followerService.follows(viewerId, userId)) + .build(); + } + + public void updateBio(Long userId, UpdateBioDto body) { + userService.getUserDao().updateUserBio(userId, body.getBio()); + } +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/dto/ProfileDto.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/dto/ProfileDto.java new file mode 100644 index 0000000..d927684 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/dto/ProfileDto.java @@ -0,0 +1,29 @@ +package com.oskarwiedeweg.cloudwork.profiles.dto; + +import com.oskarwiedeweg.cloudwork.feed.dto.PostDto; +import com.oskarwiedeweg.cloudwork.user.UserDto; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class ProfileDto { + + private UserDto userInfo; + private String bio; + private boolean following; + private List posts; + + private Stats stats; + + @Data + public static class Stats { + + private final int follower; + private final int posts; + + } + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/dto/UpdateBioDto.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/dto/UpdateBioDto.java new file mode 100644 index 0000000..0d1857a --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/dto/UpdateBioDto.java @@ -0,0 +1,10 @@ +package com.oskarwiedeweg.cloudwork.profiles.dto; + +import lombok.Data; + +@Data +public class UpdateBioDto { + + private String bio; + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/follower/FollowerDao.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/follower/FollowerDao.java new file mode 100644 index 0000000..d566588 --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/follower/FollowerDao.java @@ -0,0 +1,29 @@ +package com.oskarwiedeweg.cloudwork.profiles.follower; + +import lombok.Data; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@Data +@Repository +public class FollowerDao { + + private final JdbcTemplate jdbcTemplate; + + public Integer countFollowers(Long userId) { + return jdbcTemplate.queryForObject("select count(id) as count from user_followers where following_id = ?", (rs, rn) -> rs.getInt("count"), userId); + } + + public void createFollow(Long userId, Long following) { + jdbcTemplate.update("insert into user_followers (follower_id, following_id) values (?, ?) " + + "on conflict (follower_id, following_id) do nothing", userId, following); + } + + public void deleteFollow(Long userId, Long following) { + jdbcTemplate.update("delete from user_followers where follower_id = ? and following_id = ?", userId, following); + } + + public boolean isFollowPresent(Long follower, Long following) { + return !jdbcTemplate.query("select * from user_followers where follower_id = ? and following_id = ?", (rs, rn) -> rn, follower, following).isEmpty(); + } +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/follower/FollowerService.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/follower/FollowerService.java new file mode 100644 index 0000000..47410ee --- /dev/null +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/profiles/follower/FollowerService.java @@ -0,0 +1,41 @@ +package com.oskarwiedeweg.cloudwork.profiles.follower; + +import com.oskarwiedeweg.cloudwork.notifications.NotificationType; +import com.oskarwiedeweg.cloudwork.notifications.NotificationsService; +import com.oskarwiedeweg.cloudwork.user.User; +import com.oskarwiedeweg.cloudwork.user.UserDao; +import lombok.Data; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Data +@Service +public class FollowerService { + + private final FollowerDao followerDao; + private final UserDao userDao; + private final NotificationsService notificationsService; + + public int getFollowerCount(Long userId) { + return followerDao.countFollowers(userId); + } + + public boolean follows(Long userId, Long following) { + return followerDao.isFollowPresent(userId, following); + } + + public void follow(Long userId, Long followingId) { + followerDao.createFollow(userId, followingId); + + User user = userDao.findUserById(userId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + User following = userDao.findUserById(followingId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + notificationsService.sendNotification(following, NotificationType.NEW_FOLLOWER, new Object[]{}, new Object[]{user.getName()}); + } + + public void unfollow(Long userId, Long following) { + followerDao.deleteFollow(userId, following); + } + +} diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/User.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/User.java index 402f6fc..d4ad756 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/User.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/User.java @@ -15,4 +15,5 @@ public class User { private final LocalDate localDate; private final Long settings; private final String twoFASecret; + private final String bio; } diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/UserDao.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/UserDao.java index 7796c9c..a346cf8 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/UserDao.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/UserDao.java @@ -66,4 +66,8 @@ public void updateUserSettingsWith2FASecret(Long userId, Long settings, String u public void updateUserSettings(Long userId, Long settings) { jdbcTemplate.update("update users set settings = ? where id = ?", settings, userId); } + + public void updateUserBio(Long userId, String bio) { + jdbcTemplate.update("update users set bio = ? where id = ?", bio, userId); + } } diff --git a/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/UserRowMapper.java b/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/UserRowMapper.java index 8baf3c7..78ce351 100644 --- a/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/UserRowMapper.java +++ b/backend/src/main/java/com/oskarwiedeweg/cloudwork/user/UserRowMapper.java @@ -28,7 +28,8 @@ public User mapRow(ResultSet resultSet, int rowNum) throws SQLException { isThere(resultSet, column("password")) ? resultSet.getString(column("password")) : null, isThere(resultSet, column("created_at")) ? resultSet.getDate(column("created_at")).toLocalDate() : null, isThere(resultSet, column("settings")) ? resultSet.getLong(column("settings")) : null, - isThere(resultSet, column("2fa_key")) ? resultSet.getString(column("2fa_key")) : null + isThere(resultSet, column("2fa_key")) ? resultSet.getString(column("2fa_key")) : null, + isThere(resultSet, column("bio")) ? resultSet.getString(column("bio")) : null ); } diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml index 8af71eb..46ef9c6 100644 --- a/backend/src/main/resources/application-local.yml +++ b/backend/src/main/resources/application-local.yml @@ -11,4 +11,10 @@ sso: google: clientId: "1000232559307-n4gnukqqpcmiobetmhjn4t98ojvesnui.apps.googleusercontent.com" github: - clientId: "Iv1.8fb52ffcb5732f14" \ No newline at end of file + clientId: "Iv1.8fb52ffcb5732f14" +# clientSecret: "" Set as env +notifications: + keys: + public: "BIhDSNQxHDSqLgy5oRod-0AVs0HuvsxzubgtZ26HUweOFwT0AnAPd7v9mexrN3RpnETdJyw4w3pXegd_J3wCBco" +# private: "" Set as env + clientId: "Iv1.8fb52ffcb5732f14" diff --git a/backend/src/main/resources/db/migration/V2_3__add_draft_public_to_shema.sql b/backend/src/main/resources/db/migration/V2_3__add_draft_public_to_shema.sql new file mode 100644 index 0000000..e778a40 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_3__add_draft_public_to_shema.sql @@ -0,0 +1 @@ +alter table posts add column state varchar(255) not null default 'public'; diff --git a/backend/src/main/resources/db/migration/V2_4__add_notification_endpoints_schema.sql b/backend/src/main/resources/db/migration/V2_4__add_notification_endpoints_schema.sql new file mode 100644 index 0000000..8abd31f --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_4__add_notification_endpoints_schema.sql @@ -0,0 +1,8 @@ +create table user_notification_endpoints ( + id bigserial primary key, + user_id bigint unique, + endpoint text, + key text, + auth text, + foreign key (user_id) references users(id) +) \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V2_5__create_comment_shema.sql b/backend/src/main/resources/db/migration/V2_5__create_comment_shema.sql new file mode 100644 index 0000000..5a21655 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_5__create_comment_shema.sql @@ -0,0 +1,9 @@ +create table "post_comments" ( + id bigserial primary key, + user_id bigint, + post_id bigint, + content text, + published_at timestamp, + foreign key (user_id) references users(id), + foreign key (post_id) references posts(id) +) \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V2_6__add_follower.sql b/backend/src/main/resources/db/migration/V2_6__add_follower.sql new file mode 100644 index 0000000..b8f2be4 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_6__add_follower.sql @@ -0,0 +1,8 @@ +create table "user_followers" ( + id bigserial primary key, + follower_id bigint, + following_id bigint, + foreign key (follower_id) references users(id), + foreign key (following_id) references users(id), + unique (follower_id, following_id) +) \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V2_7__add_bio_to_user.sql b/backend/src/main/resources/db/migration/V2_7__add_bio_to_user.sql new file mode 100644 index 0000000..6d0163b --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_7__add_bio_to_user.sql @@ -0,0 +1 @@ +alter table "users" add column bio text not null default ''; \ No newline at end of file diff --git a/backend/src/test/java/com/oskarwiedeweg/cloudwork/feed/FeedServiceTest.java b/backend/src/test/java/com/oskarwiedeweg/cloudwork/feed/FeedServiceTest.java index 58795b1..6c83cc6 100644 --- a/backend/src/test/java/com/oskarwiedeweg/cloudwork/feed/FeedServiceTest.java +++ b/backend/src/test/java/com/oskarwiedeweg/cloudwork/feed/FeedServiceTest.java @@ -44,14 +44,14 @@ public void testGetFeed() { .id(2L) .build(); - Post post1 = new Post(1L, "Test", "Wow", "Test", "Image", LocalDateTime.now(), testUser1); - Post post2 = new Post(2L, "Tes1", "Wow", "Test", "Image", LocalDateTime.now(), testUser1); - Post post3 = new Post(3L, "Tes2", "Wow", "Test", "Image", LocalDateTime.now(), testUser2); + Post post1 = new Post(1L, "Test", "Wow", "Test", "Image", Post.PUBLIC_STATE, LocalDateTime.now(), testUser1); + Post post2 = new Post(2L, "Tes1", "Wow", "Test", "Image", Post.PUBLIC_STATE, LocalDateTime.now(), testUser1); + Post post3 = new Post(3L, "Tes2", "Wow", "Test", "Image", Post.PUBLIC_STATE, LocalDateTime.now(), testUser2); List posts = List.of(post1, post2, post3); //when - when(postDao.getPosts()).thenReturn(posts); + when(postDao.getPublicPosts()).thenReturn(posts); //then FeedDto feed = underTest.getFeed(); @@ -69,7 +69,7 @@ public void testGetFeed() { assertEquals(modelMapper.map(testUser1, UserDto.class), feed.getAuthors().get(testUser1.getId())); assertEquals(modelMapper.map(testUser2, UserDto.class), feed.getAuthors().get(testUser2.getId())); - verify(postDao).getPosts(); + verify(postDao).getPublicPosts(); } @Test diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 95ad7c4..b054973 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,11 +8,15 @@ "name": "frontend", "version": "0.0.1", "dependencies": { - "@sveltejs/adapter-node": "^1.3.1" + "@sveltejs/adapter-node": "^1.3.1", + "isomorphic-dompurify": "^1.13.0", + "quill": "^1.3.6" }, "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/kit": "^1.27.4", + "@types/dompurify": "^3.0.5", + "@types/quill": "^2.0.14", "autoprefixer": "^10.4.16", "postcss": "^8.4.32", "svelte": "^4.2.7", @@ -743,6 +747,14 @@ "integrity": "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==", "dev": true }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -754,11 +766,46 @@ "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", "dev": true }, + "node_modules/@types/quill": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-2.0.14.tgz", + "integrity": "sha512-zvoXCRnc2Dl8g+7/9VSAmRWPN6oH+MVhTPizmCR+GJCITplZ5VRVzMs4+a/nOE3yzNwEZqylJJrMB07bwbM1/g==", + "dev": true, + "dependencies": { + "parchment": "^1.1.2", + "quill-delta": "^5.1.0" + } + }, + "node_modules/@types/quill/node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/@types/quill/node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "dev": true, + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", @@ -771,6 +818,17 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -805,6 +863,11 @@ "dequal": "^2.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", @@ -939,6 +1002,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1004,6 +1080,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/code-red": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", @@ -1017,6 +1101,17 @@ "periscopic": "^3.1.0" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1071,11 +1166,33 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1088,6 +1205,30 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1096,6 +1237,43 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1132,12 +1310,28 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/dompurify": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.6.tgz", + "integrity": "sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==" + }, "node_modules/electron-to-chromium": { "version": "1.4.611", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.611.tgz", "integrity": "sha512-ZtRpDxrjHapOwxtv+nuth5ByB8clyn8crVynmRNGO3wG3LOp8RTcyZDqwaI6Ng6y8FCK2hVZmJoqwCskKbNMaw==", "dev": true }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -1205,6 +1399,21 @@ "@types/estree": "^1.0.0" } }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -1242,6 +1451,19 @@ "node": ">=8" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -1281,6 +1503,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1325,12 +1569,70 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -1342,6 +1644,52 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -1382,6 +1730,21 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1419,6 +1782,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1454,6 +1831,11 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, "node_modules/is-reference": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", @@ -1463,6 +1845,34 @@ "@types/estree": "*" } }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isomorphic-dompurify": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-1.13.0.tgz", + "integrity": "sha512-9qOYGngy9ZR9JB/iLmr7SViPSZ7uWGvepdnLaXYznbTxvJOCuONneKajJ54f+IRQpvL8608ylUy9EK1iPtL3Ag==", + "dependencies": { + "@types/dompurify": "^3.0.3", + "dompurify": "^3.0.6", + "jsdom": "^23.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", @@ -1472,6 +1882,45 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jsdom": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.0.1.tgz", + "integrity": "sha512-2i27vgvlUsGEBO9+/kJQRbtqtm+191b5zAZrU/UezVmnC2dlDAFLgDYJvAEi94T4kjsRKkezEtLQTgsNEsW2lQ==", + "dependencies": { + "cssstyle": "^3.0.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.7", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.3", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.14.2", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1502,6 +1951,18 @@ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "dev": true }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -1541,6 +2002,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -1604,8 +2084,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -1660,6 +2139,11 @@ "node": ">=0.10.0" } }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1678,6 +2162,29 @@ "node": ">= 6" } }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1686,6 +2193,11 @@ "wrappy": "1" } }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -1698,6 +2210,17 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1904,6 +2427,24 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -1924,6 +2465,32 @@ } ] }, + "node_modules/quill": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.6.tgz", + "integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.1", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -1945,6 +2512,27 @@ "node": ">=8.10.0" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -2007,6 +2595,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2042,6 +2635,11 @@ "node": ">=6" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/sander": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", @@ -2054,12 +2652,50 @@ "rimraf": "^2.5.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", "dev": true }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/sirv": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", @@ -2295,6 +2931,11 @@ "node": ">=12" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, "node_modules/tailwindcss": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz", @@ -2396,6 +3037,31 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -2433,6 +3099,14 @@ "node": ">=14.0" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -2463,6 +3137,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2538,11 +3221,94 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.15.1.tgz", + "integrity": "sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, "node_modules/yaml": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5aa0619..3367ff8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,8 @@ "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/kit": "^1.27.4", + "@types/dompurify": "^3.0.5", + "@types/quill": "^2.0.14", "autoprefixer": "^10.4.16", "postcss": "^8.4.32", "svelte": "^4.2.7", @@ -24,6 +26,8 @@ }, "type": "module", "dependencies": { - "@sveltejs/adapter-node": "^1.3.1" + "@sveltejs/adapter-node": "^1.3.1", + "isomorphic-dompurify": "^1.13.0", + "quill": "^1.3.6" } } diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index 141b2f6..8a22465 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -5,6 +5,7 @@ declare global { // interface Error {} interface Locals { user?: { + id: number, username: string, email: string }, diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index e5c1718..583a5e2 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -5,10 +5,10 @@ export const handle: Handle = async ({event, resolve}) => { let cookies = event.cookies; let authToken = cookies.get("authToken"); if (authToken) { - let {username, email} = parseJwt(authToken); + let {username, email, sub} = parseJwt(authToken); event.locals.user = { - username, email + username, email, id: sub } event.locals.token = authToken; } else { diff --git a/frontend/src/lib/assets/EditIcon.png b/frontend/src/lib/assets/EditIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..ae18662bd401b6be1b75b6c2144297242ef928d9 GIT binary patch literal 7484 zcmdT}dpK0v`=6pD>2h+H)TzTc%0c!}BT7iccAY_LNNzLcxRe=k?@$OOb?Rtx$!##> zRxW)Tw^J%z5VnmJk}hW4DkDYstu_7id!Fx~-}5}@{9%38diUq^zVCY1yVvaJan8=h zQg)@vN&m{1A!pX1^-q^087Nm-)jI!`kJDq1j5Z!nQtD;zM|A^=k9x%|k;&KmPb*Oiawe z!UB~__4D&%Fc`o6_FGX=k-xuxKtMoXU|?uy=&4hu!otGB!^0yYA|fLrqoSgsqoaTM z<(H2iKT1hS<>%*5PfyFp$Z)yb)vH(Eym`~j&FzODez<-6wwITef`URrL&N<1{D~7M zo;-P?sj1n~(V?ZKb>_?&Jw3gFfq|r?Btt{PH*el7S+XQ8EzQKlWMpJSLPFxtKmRm0 zHy;}tla!Rq%*;G==+LK6pH{3`QBhH`Zr!^2`g(PB_4fAmxVSi7UESf~;foh98XFsb z`0ybwFK=dMMpjm~rl!Wz(^FnvzNx7xC@4rnL*v}Ja|Q+mSFc_*Gc#Mhe0gzkv6GWi zU0ofWPJjIPaY#tW)~#DpQ&V^E-kp(=aq!^5^78VdM~~jScW>jyjZ2ps* za2!ueOe7MC*RNkcaNq!qM!Rk3z{{#>MFf9?^@^hB23jX%r_Ld@laCDl_gy@_(lJ6;C24Gj90Ru4|@ zc{Na-U1?Gt6|*z!R`s%sc6wp9eU#*}eTA7}-7;j^;ho*0!`%22yPsEaJ(O&TrG85H zF_fng{~?7#k(D^*gmL31j0@=$v-ep&Room#Gu;($zd3AB>NnCY&2}O(g=YYr&%6Za zY9uoyUM#7J;YYe;!12ajXR9?#>`!CL;a7 z?0p`j>S8|y%ub!K67o#wyO{(IsKTrtMCeVs01jwuuQQ<76jy@n6}D3W@v^W2?BF01 zI3GGg@^{69miB`XJIgMB1KMEO1L#?WQ-J=gBMQ)cm2UvL{wm;oh#m+0o_eASDG1EG z^ezaI0!n$fZV1d|3_;#L9r3@p;@f*~pgi^il^Qyw(oO>8sz4v%P+FJDg3~yhF$6-% zRGtQg-0h%UDmTf1w6{N$`Lk1W*tecm8{T`dV$_*?4Gx(uY`K&X;`5y0wQEwSA5%T+>0@eRRQVybseoKckBtV^A-3*} z2E`_}{=Cpgr!d%h-)=!zx4RO;f^jW`0pr^c8jQu1EVXjDidGY^+%k*c$|MtXDucZ- zvjoCIMjeEK3@(I*3_gUcv3v;8V`7TiSQT8EjER{WOTf!QMCY6st~i^+;(-=Hu^Rlk zQ#FVFoP_dRu_{JAhf+#+tq#Dr-H5-dIJknpSQcR*mJzRuWmj9pvd3~_nI~H;)1!!G z>18mef;DJIB3e&}#9{*$d{oT|grPe(KvmZ@a0@ZmTEXBYK^;9CpbM=Qv1p(U67pNY zYe7W2zrn@UpV)4ZI9uaqOmvUejhGg56sgr5=_kbcSAb)8OYp!6RQndHQ{{)Q|05tg z4YVv-c3{y&g}Vlu#!B!yUC1ea7yh6|Sl_33cwkI59nrD1&{Tt;CsMPOOo zF8Tmn8L&6j0nc5BFf(TmsE-~lj%$UkIRet~;|WY*R;5H^FOXzWJx2-#uI?2DiN)79 zmO!x|W(GDH+vgvC1BQ5tu5pCoT7fAjj%lf(u{jjabEIJ8>2~G}6q~Ro#F4EqDvUf! z=MO`%EgA|V3wZ(?7`dLnjly>H@t^H+`!qT= z|KY*_pwk98w`+j?*1Ldk#%KY|V#+Y8L>R2-27Dhv>c7BH9Kl%_*#Xtz!{RakwXOh` z`x2_ooBd|-Un_s)h~jo|u&2z;#*)QLgZhgw zA`5HgMt>C-eimdHJtZFpGjOGaixW~s1O4Z_AHfYeu}z$&&o;pn{@a-hM4#x;@j2%T zbG~g^qH?^CG3$aW7OQmQ$H8qk-P$M0s!pvA9ZYG~n%kqd`9|xEsCyvUclXs$;+7*m z;O;wW1uoPSzdPXW)6>2~r|9Q;gS+oxr3hSgG;n!7?DWIRSAmyeKe+bv6#(rf_=!xh z0MhN?0`&whzH&0)xxYP3gicSVitWNgreX{A81>2?_+4WB!z;!1St~#PDS#kiA zW@}jf!-qngqf_2iamDnnBqZ|&{p7!5aP^-}`ghfMv4Yg(w0z07PG&6UamPPnxK=IL zcDu)<#|51%UPwQDv8OJi`)OnwpTbb-7V)o@Psy~ojJkLd#{&Z|wOyOrS5B$scu<|^ zJI0*D&vS{gj9OAqv2$6tEi3+VCvwM70B6U&?d@l7qQjJrLv;-1^Db5@DK~tO^-#Qi^EV`6^3rOM@kjPeBdmwQv zw{{m@hKjC*sbS~&tx)p$UhLE8dmza52&)IIJy0xt)aB$*D)=V#SQ@C!|I`e==XB3L z<^DQ91wQ|*U;BIaEbIbOjgP)(6&DYK8ONS4HKq(rCvS*8?E#1^=zm)=6emu(1clgBmF|5(&Xb`31IIks~ElZ&uqN#u1P4+g^gS(0%b z2)=NO6nmG(J{d!y-gQCHJENzLi5&Xfi0^nW zf@h_az@gup3n;PiP-1gr%Xwm4_nv%PcO{A?@371d{6c!)r{Be> z5`2QSz8*u)KaYuK$z@_lX_$u?Tf?ka@=+Ml`aHssr9j1y@i2xMW%(ui-}N#}jzNBYyxS;wPAgFl2Nb zXNeACRikQn7!%?<25)X8jYIDp5}JhP(_KHO3HOo!hVb#(H96BV?xeE z!3idq9PLM+0xT<@;CQn+mZ8oGm@bMHL~x(d|KtcdTYH=evF#{U*n|rZD~u`Jf{#}n z0?3cUKO%=Pk#Jay%+dAubk#wOISLwbihhYB=!y7_y%D@QdKyR2t3Bf+)wK!5DpS8> zHO7dQu*T#$NK$zTe&oX(fd8A69cPLt0muP{p{`#F6S?gtV^xoP8wIK_6*Vn&8?L zE3==~uR$z_w@9l?S+NZ_gQMKikE0X}$-7fX(n6c^0~%`h z5F6~MS2!N7?j>!9Fx_JCPKOrI)4Q5^wVjAHD4+*jMS}(U&>P@%f$$b0i_M;7xw7LA z_MZ%79RV0s#n(ogs%*+HSOEv%P40mXGIU9f0nOg6Ph2XUEf{3P`#ja?Kiw zA_q{%@w0Rl4$G2t9C#b&SMv41LlbI$I!G(K_<}X?I{)6u8B(2j+c4m@Q`myYUha#C zVfn%YYWS5I5)r=^Wf@3DmXWGcq&Du344y99JUoTpS+*ha!-qlgGi8U3+|>agYT=XmS&z zv}YYy8H}*Fa#qv1$UFRJ`21*tWeu-+d29ICfY97o)Nj@BUfu7ngOQD-&MS5IdiX=+ zXuEzv8Fj3U>3*3JPc_5S|vq)dDJmO*|RG}kQ1BR2+C1T z*<-kB^b@lKBwbgQqTs)1(lS06YcUuICX=JZx#NQgDS`WwOoFb>&s{u$=7JcT-}_D} z0^-*BxeU`iA6^ZGI_=rCy`{CM&8cdB&TdFXUmiS_`}%r=#h%DFBMh$kDg|MOl!gw- zqDWMFF4?4NGEJYGEv+#A!VyONqjn+waZ*i;y6Xq^^-znv-c$H9VACFY&IB9>xIbn;Ajb$h|e8PNI3xjE%EJf7yJaNm;m$F#6liQ$w@3P`GW-$O2Tx z%y(fl-=eW(_S9cVpR*%kZ-AnhL{0z4LLIy*dAO!T_esH2a>K_*G2kHT`yv*D;bvw( z5BtK+Fefz|VRtCqwawO=RRxD`_9a_0d7*56PB+OJC_KLoK2_(MEY;0flyF$v$KAn+ z6@q`<{KT|yVS0Hmy-A)Yy#Ww*=;|42>l$e5?I7#wlk^dizQH!#og`h|1P5f#{}cEJ zdeMEtpuj*|7tz);AnOrHM1-Wb^SdDaz1BPskYR-25MR=9k6>?Y)=4_ShyI&43H%d_ z7U=J-<8@pIJanfA_yod<&Mre8A44$u82WhY8|dvY+=Y1P>3QqzH1PKE-nGliK;L7R Zq3*AMy3M6K3HT=*w6HNR+jlJX{{XMeb7uem literal 0 HcmV?d00001 diff --git a/frontend/src/lib/components/ActivateNotifications.svelte b/frontend/src/lib/components/ActivateNotifications.svelte new file mode 100644 index 0000000..ad61eac --- /dev/null +++ b/frontend/src/lib/components/ActivateNotifications.svelte @@ -0,0 +1,34 @@ + + +{#if (!activeSettings.includes("NOTIFICATIONS_ENABLED"))} +

Notifications are disabled.

+ +
+ +
+{:else} +

Active Notifications:

+
    + {#each activeSettings as activeSetting} + {#if (activeSetting.startsWith("NOTIFICATION_"))} +
  • {activeSetting.slice("NOTIFICATION_".length)}
  • + {/if} + {/each} +
+{/if} +{#if (formResponse?.error)} + {formResponse?.error} +{/if} \ No newline at end of file diff --git a/frontend/src/lib/components/DropDownMyBlogs.svelte b/frontend/src/lib/components/DropDownMyBlogs.svelte new file mode 100644 index 0000000..5079a44 --- /dev/null +++ b/frontend/src/lib/components/DropDownMyBlogs.svelte @@ -0,0 +1,54 @@ + + +
+
+ +
+ + +
diff --git a/frontend/src/lib/components/Input.svelte b/frontend/src/lib/components/Input.svelte index d263278..8b10dea 100644 --- a/frontend/src/lib/components/Input.svelte +++ b/frontend/src/lib/components/Input.svelte @@ -7,15 +7,17 @@ export let errorType: 'requirements' | 'message' = "requirements"; export let value: any = null; + + export let basicValue: string = "";
{#if (errors)} diff --git a/frontend/src/lib/components/NavigationBar.svelte b/frontend/src/lib/components/NavigationBar.svelte index 211efaf..9214a1e 100644 --- a/frontend/src/lib/components/NavigationBar.svelte +++ b/frontend/src/lib/components/NavigationBar.svelte @@ -65,9 +65,10 @@ tabindex="-1" > - Your Profile - Settings - Sign out + Your Profile + My Blogs + Settings + Sign out
{/if} diff --git a/frontend/src/lib/notifications.ts b/frontend/src/lib/notifications.ts new file mode 100644 index 0000000..205ea63 --- /dev/null +++ b/frontend/src/lib/notifications.ts @@ -0,0 +1,62 @@ +export function setup () { + if ('serviceWorker' in navigator) { + console.log("Loading service worker...") + navigator.serviceWorker.register("/service-worker.js") + .then(initializeState) + } else { + console.warn('Service workers are not supported in this browser.'); + } +} + +function initializeState() { + console.log("Loaded") + if (!('showNotification' in ServiceWorkerRegistration.prototype)) { + console.warn('Notifications aren\'t supported.'); + return; + } + + if (Notification.permission === 'denied') { + console.warn('The user has blocked notifications.'); + return; + } + + if (!('PushManager' in window)) { + console.warn('Push messaging isn\'t supported.'); + return; + } + + if (Notification.permission === "granted") { + subscribe(); + } +} + +export function subscribe() { + return new Promise((resolve, reject) => { + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { + + serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: "BIhDSNQxHDSqLgy5oRod-0AVs0HuvsxzubgtZ26HUweOFwT0AnAPd7v9mexrN3RpnETdJyw4w3pXegd_J3wCBco" + }).then(function (subscription) { + const key = subscription.getKey ? subscription.getKey('p256dh') : null; + const auth = subscription.getKey ? subscription.getKey('auth') : null; + const endpoint = subscription.endpoint; + + resolve({ + key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : '', + auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : '', + endpoint + }) + }) + .catch(function (e) { + if (Notification.permission === 'denied') { + console.warn('Permission for Notifications was denied'); + } else { + console.error('Unable to subscribe to push.', e); + } + reject(e); + }); + }); + }); + +} diff --git a/frontend/src/lib/user.ts b/frontend/src/lib/user.ts index 01e12e6..76ee589 100644 --- a/frontend/src/lib/user.ts +++ b/frontend/src/lib/user.ts @@ -3,6 +3,7 @@ import {writable} from "svelte/store"; export const auth = writable(null); export type Auth = { + id: number, username: string, email: string } \ No newline at end of file diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index c9ebbf4..a1cee60 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -3,10 +3,15 @@ import {auth} from "$lib/user"; import NavigationBar from "$lib/components/NavigationBar.svelte"; import type {PageData} from "./$types"; - + import {setup} from "$lib/notifications"; + import {browser} from "$app/environment"; export let data: PageData; $auth = data.user; + + if (browser) { + setup() + } diff --git a/frontend/src/routes/FAConfirm/+page.svelte b/frontend/src/routes/FAConfirm/+page.svelte index 3918de6..b00ff41 100644 --- a/frontend/src/routes/FAConfirm/+page.svelte +++ b/frontend/src/routes/FAConfirm/+page.svelte @@ -1,4 +1,6 @@
@@ -63,15 +103,35 @@ - - + +
+
+
+
{counter}/5000 Characters
+
+ + {#if (form?.error?.description)} +
+

The following requirements are not met:

+
    + {#each form?.error?.description as error} +
  • {error}
  • + {/each} +
+
+ {/if} + -
\ No newline at end of file + + + \ No newline at end of file diff --git a/frontend/src/routes/editFeed/+page.server.ts b/frontend/src/routes/editFeed/+page.server.ts index 7805095..e751972 100644 --- a/frontend/src/routes/editFeed/+page.server.ts +++ b/frontend/src/routes/editFeed/+page.server.ts @@ -1,9 +1,58 @@ import type {PageServerLoad} from "./$types"; -import {redirect} from "@sveltejs/kit"; -export const load: PageServerLoad = async ({locals}) => { +import {type Actions, redirect} from "@sveltejs/kit"; +import {env} from "$env/dynamic/public"; + +export const load: PageServerLoad = async ({locals, url}) => { if(!locals.user){ console.log("Access denied!") throw redirect(303, "/"); } + + const id = url.searchParams.get("id") + const response = await fetch(env.PUBLIC_BACKEND_URL + `v1/feed/${id}`, { + method: "GET" + }); + const responseData = await response.json() + + console.log(responseData); + return await responseData; } +export const actions:Actions = { + default: async ({request, fetch, locals, url}) => { + + const id = url.searchParams.get("id") + const token = locals.token; + + const data = await request.formData(); + const title = data.get('title'); + const preview = data.get('shortDescription'); + const description = data.get('content'); + const image = data.get('image'); + + const payload = { + title, + preview, + description, + image, + }; + + console.log(payload); + + const response = await fetch(env.PUBLIC_BACKEND_URL + `v1/feed/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + return await response.json(); + } + + throw redirect(303, '/blogs?created'); + + } +} diff --git a/frontend/src/routes/editFeed/+page.svelte b/frontend/src/routes/editFeed/+page.svelte index 879e2fe..6ace47f 100644 --- a/frontend/src/routes/editFeed/+page.svelte +++ b/frontend/src/routes/editFeed/+page.svelte @@ -1,22 +1,137 @@ -
-
-
- -
+ + +
+ + + {#if (base64Image) || (data.image !== 'data:image/png;base64,' && data.image)} + + preview img + + {/if} + + + + + + + +
+
+ {@html sanitize(data.description)} +
+
{counter}/5000 Characters
+ {#if (form?.error?.description)} +
+

The following requirements are not met:

+
    + {#each form?.error?.description as error} +
  • {error}
  • + {/each} +
+
+ {/if} + + + + -
\ No newline at end of file +
+ + \ No newline at end of file diff --git a/frontend/src/routes/myBlogs/+page.server.ts b/frontend/src/routes/myBlogs/+page.server.ts new file mode 100644 index 0000000..8a81bdf --- /dev/null +++ b/frontend/src/routes/myBlogs/+page.server.ts @@ -0,0 +1,55 @@ +import type {Actions} from "./$types"; +import {env} from "$env/dynamic/public"; +import type {PageServerLoad} from "./$types"; +import {setCookie} from "$lib/setCookie"; + +export const actions:Actions = { + delete: async ({request, fetch, locals}) => { + + const token = locals.token; + + const data = await request.formData(); + const blogID = data.get('blogId'); + + const response = await fetch(env.PUBLIC_BACKEND_URL + `v1/feed/${blogID}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}` + } + }) + + if(!response.ok) { + return {error: "unexpected error"}; + } + }, + changeState: async ({request, fetch, locals}) => { + const token = locals.token; + + const data = await request.formData(); + const blogID = data.get('blogId'); + const response = await fetch(env.PUBLIC_BACKEND_URL + `v1/feed/updateState/${blogID}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + } + }) + + if(!response.ok) { + return {error: "unexpected error"}; + } + } +} + + +export const load: PageServerLoad = async ({fetch, locals}) => { + const token = locals.token; + + const response = await fetch(env.PUBLIC_BACKEND_URL + "v1/feed/myFeed", { + method: "GET", + headers: { + 'Authorization': `Bearer ${token}` + } + }); + return await response.json(); +} + diff --git a/frontend/src/routes/myBlogs/+page.svelte b/frontend/src/routes/myBlogs/+page.svelte new file mode 100644 index 0000000..6ddb6ea --- /dev/null +++ b/frontend/src/routes/myBlogs/+page.svelte @@ -0,0 +1,44 @@ + + +
+
+
+
+

Public Blogs

+
+ {#each data.posts as blog (blog.id)} + {#if blog.state === 'public'} +
+
+ {blog.title} +
+ +
+ {/if} + {/each} +
+
+ +
+

Draft Blogs

+
+ {#each data.posts as blog (blog.id)} + {#if blog.state === 'draft'} +
+
+ {blog.title} +
+ +
+ {/if} + {/each} +
+
+
+
+
diff --git a/frontend/src/routes/profile/[profileId]/+page.server.ts b/frontend/src/routes/profile/[profileId]/+page.server.ts new file mode 100644 index 0000000..b83ac3d --- /dev/null +++ b/frontend/src/routes/profile/[profileId]/+page.server.ts @@ -0,0 +1,44 @@ +import type {Actions, PageServerLoad} from "./$types"; +import {env} from "$env/dynamic/public"; +import {error} from "@sveltejs/kit"; + +export const load: PageServerLoad = async ({params, locals, fetch}) => { + const profileId = params.profileId; + + let headers: Record = {}; + + if (locals.token) { + headers['Authorization'] = "Bearer " + locals.token; + } + + const response = await fetch(env.PUBLIC_BACKEND_URL + `v1/profile/${profileId}`, { + headers, + method: "GET" + }); + + if (response.ok) { + return await response.json(); + } + + if (response.status === 404) { + throw error(404); + } else { + return {error: "An unexpected error occurred."} + } +} + +export const actions: Actions = { + follow: async ({request, fetch, locals, params}) => { + const formData = await request.formData(); + const state = formData.get("state"); + + let method = state === "true" ? "POST" : "DELETE"; + + const response = await fetch(env.PUBLIC_BACKEND_URL + `v1/profile/${params.profileId}/follow`, { + method: method, + headers: { + 'Authorization': `Bearer ${locals.token}` + } + }); + } +} \ No newline at end of file diff --git a/frontend/src/routes/profile/[profileId]/+page.svelte b/frontend/src/routes/profile/[profileId]/+page.svelte new file mode 100644 index 0000000..149cd52 --- /dev/null +++ b/frontend/src/routes/profile/[profileId]/+page.svelte @@ -0,0 +1,70 @@ + + +
+

{data.userInfo.name}

+
+
+
+
+

{data.stats.posts}

+

Posts

+
+
+

{data.stats.posts}

+

Likes

+
+
+

{data.stats.follower}

+

Follower

+
+
+ + {#if ($auth)} +
+ + {#if (data.following)} + + {:else} + + {/if} +
+ {/if} +
+ +
+

Bio

+

{@html sanitize(data.bio)}

+
+ +
+

Blog Posts

+
+ {#each data.posts as post (post.id)} +
+ {#if post.image === null} +
+ {:else if post.image !== "data:image/png;base64,"} + {post.title} + {:else} +
+ {/if} +
+

{post.title}

+

{post.preview}

+
+ Weiterlesen + +
+
+
+ {/each} +
+
+
+
diff --git a/frontend/src/routes/settings/+page.server.ts b/frontend/src/routes/settings/+page.server.ts index 01be673..81bf91a 100644 --- a/frontend/src/routes/settings/+page.server.ts +++ b/frontend/src/routes/settings/+page.server.ts @@ -2,9 +2,24 @@ import {type Actions, redirect, type ServerLoad} from "@sveltejs/kit"; import {env} from "$env/dynamic/public"; export const load: ServerLoad = async ({fetch, locals}) => { + if(!locals.user){ + console.log("Access denied!") + throw redirect(303, "/login"); + } + + if(!locals.user){ + console.log("Access denied!") + throw redirect(303, "/login"); + } + + if(!locals.user){ + console.log("Access denied!") + throw redirect(303, "/login"); + } const token = locals.token; + const response = await fetch(env.PUBLIC_BACKEND_URL + "v1/user/settings", { method: 'GET', headers: { @@ -24,11 +39,11 @@ export const load: ServerLoad = async ({fetch, locals}) => { const activeSettings = responseValid.activeSettings; if (activeSettings.includes("TWO_FACTOR_AUTH")) { - return {value: true, ssoProviders}; + return {value: true, ssoProviders, activeSettings}; } - return {ssoProviders}; + return {ssoProviders, activeSettings}; } export const actions: Actions = { @@ -73,5 +88,28 @@ export const actions: Actions = { if (!response.ok) { return {error: "Unexpected Error"}; } + }, + activeNotifications: async ({fetch, locals, request}) => { + if (!locals.token) { + throw redirect(302, "/login"); + } + + const formData = await request.formData(); + const body = formData.get("body"); + + const response = await fetch(env.PUBLIC_BACKEND_URL + "v1/notifications/subscribe", { + method: "POST", + body: body, + headers: { + 'Authorization': `Bearer ${locals.token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + return {activateNotifications: { + error: "An unexpected error occurred." + }} + } } } \ No newline at end of file diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 1bd1d0b..505f751 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -5,6 +5,7 @@ import SSO from "$lib/components/SSO.svelte"; import {page} from "$app/stores"; import {ssoProviderMap} from "$lib/ssoProviderMap"; + import ActivateNotifications from "$lib/components/ActivateNotifications.svelte"; let modalOpen: boolean = false; let isDeactivateConfirmed: boolean | null = null; @@ -91,7 +92,7 @@ {/if} -
+

SSO


@@ -129,5 +130,26 @@ {/if}
+
+ +
+

Notifications

+
+
+ +
+
+ + +

Your Blogs

+
+
+

Public

+

None

+

Edit your Blogs

+ +
diff --git a/frontend/src/routes/showBlog/+page.server.ts b/frontend/src/routes/showBlog/+page.server.ts index 59fc4ed..172e6fd 100644 --- a/frontend/src/routes/showBlog/+page.server.ts +++ b/frontend/src/routes/showBlog/+page.server.ts @@ -1,11 +1,13 @@ import type {PageServerLoad} from "./$types"; import {env} from "$env/dynamic/public"; -export const load: PageServerLoad = async ({fetch}) => { +export const load: PageServerLoad = async ({fetch, url}) => { - const response = await fetch(env.PUBLIC_BACKEND_URL + "v1/feed", { + const id = url.searchParams.get("id") + const response = await fetch(env.PUBLIC_BACKEND_URL + `v1/feed/${id}`, { method: "GET" }); + const responseData = await response.json() - return await response.json(); + return await responseData; } \ No newline at end of file diff --git a/frontend/src/routes/showBlog/+page.svelte b/frontend/src/routes/showBlog/+page.svelte index 6c82381..f9b447d 100644 --- a/frontend/src/routes/showBlog/+page.svelte +++ b/frontend/src/routes/showBlog/+page.svelte @@ -1,29 +1,52 @@ - {#if selectedPost} -
- {#if selectedPost.image === null} -
- {:else if selectedPost.image !== "data:image/png;base64,"} - {selectedPost.title} - {:else} -
- {/if} -

{selectedPost.title}

-

Date: {new Date(selectedPost.timestamp).toLocaleDateString()}

-
- -

{selectedPost.description}

+{#if selectedPost} +
+ {#if selectedPost.image === null} +
+ {:else if selectedPost.image !== "data:image/png;base64,"} + {selectedPost.title} + {:else} +
+ {/if} + +
+

{selectedPost.title}

+ {#if selectedPost.user.id == $auth.id} + + Edit your Feed + + {/if} +
+ +

Date: {new Date(selectedPost.timestamp).toLocaleDateString()}

+
+

{@html sanitize(selectedPost.description)}

+
-
{:else} -

No blog post found for ID {$page.url.searchParams.get('id')}

-{/if} \ No newline at end of file +
+

No blog post found for ID {$page.url.searchParams.get('id')}

+
+{/if} +
+

Comments

+
+ +
+ +
diff --git a/frontend/src/routes/yourProfile/+page.server.ts b/frontend/src/routes/yourProfile/+page.server.ts index e69de29..5bbd968 100644 --- a/frontend/src/routes/yourProfile/+page.server.ts +++ b/frontend/src/routes/yourProfile/+page.server.ts @@ -0,0 +1,9 @@ +import {redirect} from "@sveltejs/kit"; +import type {PageServerLoad} from "./$types"; + +export const load: PageServerLoad = async ({locals}) => { + if(!locals.user){ + console.log("Access denied!") + throw redirect(303, "/login"); + } +} \ No newline at end of file diff --git a/frontend/static/service-worker.js b/frontend/static/service-worker.js new file mode 100644 index 0000000..3460c73 --- /dev/null +++ b/frontend/static/service-worker.js @@ -0,0 +1,36 @@ +'use strict'; + +/* eslint-env browser, serviceworker */ + +self.addEventListener('install', () => { + self.skipWaiting(); +}); + + +self.onpush = async function (event) { + const data = event.data.json(); + + let notificationTitle = data.title; + const notificationOptions = { + body: data.description + }; + + event.waitUntil( + self.registration.showNotification( + notificationTitle, + notificationOptions, + ), + ); +} + +self.addEventListener('notificationclick', function(event) { + console.log('Notification clicked.'); + event.notification.close(); + + let clickResponsePromise = Promise.resolve(); + if (event.notification.data && event.notification.data.url) { + clickResponsePromise = clients.openWindow(event.notification.data.url); + } + + event.waitUntil(clickResponsePromise); +}); \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 13207cc..f175761 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -5,5 +5,16 @@ export default { extend: {}, }, plugins: [], + safelist: [/* + { + pattern: /bg-./ + }, + { + pattern: /text-./ + }, + { + pattern: /border-./ + }*/ + ] } From b12e3a84132caecdfab37e8a5d9107ce02edfb75 Mon Sep 17 00:00:00 2001 From: Oskar Wiedeweg <78727642+OskarWiedeweg@users.noreply.github.com> Date: Sun, 24 Dec 2023 14:14:48 +0100 Subject: [PATCH 2/2] Update +page.svelte --- frontend/src/routes/FAConfirm/+page.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/routes/FAConfirm/+page.svelte b/frontend/src/routes/FAConfirm/+page.svelte index b00ff41..f1f3f97 100644 --- a/frontend/src/routes/FAConfirm/+page.svelte +++ b/frontend/src/routes/FAConfirm/+page.svelte @@ -1,7 +1,6 @@ @@ -28,4 +27,4 @@
- \ No newline at end of file +