diff --git a/README.md b/README.md index 00740cd..ad0dcce 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ # 🚀 Getting started with PomeloNote +### **THIS REPOSITORY HAS DEPENDENCIES WITH SECURITY VULNERABILITIES. YOU MIGHT WANT TO UPDATE PACKAGES BEFORE USE.** +## Setup +- run `npm i` +- get the .env file and save it to the root directory of the project +- set up Strapi + - go to `localhost:1337/admin` + - register an admin user + - go to Settings => Users&Permissions Plugin => Roles => Authenticated => Note => Select all + - Save ### Starting the container with svelte and strapi: ``docker-compose up --build -d`` diff --git a/backend/strapi/src/api/note/controllers/note.js b/backend/strapi/src/api/note/controllers/note.js index 9cf8879..84e7a8d 100644 --- a/backend/strapi/src/api/note/controllers/note.js +++ b/backend/strapi/src/api/note/controllers/note.js @@ -50,6 +50,9 @@ module.exports = createCoreController(noteUid, ({strapi}) => ({ lastViewed: Date.now() } }) + entry = await strapi.entityService.findOne(noteUid, noteId, { + populate: ['owners'], + }); return JSON.stringify(entry); } else { ctx.response.status = 403; @@ -63,22 +66,26 @@ module.exports = createCoreController(noteUid, ({strapi}) => ({ async update(ctx) { const noteId = getNoteIdFromUrl(ctx.request.url) const userId = ctx.state.user.id; - const requestBody = ctx.request.body; + const requestBody = JSON.parse(ctx.request.body); + console.log(JSON.stringify(requestBody, null, 2)) const entry = await strapi.entityService.findOne(noteUid, noteId, { populate: ['owners'], }); const authorized = entry.owners.some(owner => owner.id === userId) - let allPreviousOwnersKept = false; + let allPreviousOwnersKept = true; if (requestBody.data.hasOwnProperty("owners")) { allPreviousOwnersKept = entry.owners.every(owner => requestBody.data.owners.includes(owner)); } + console.log({ + "auth": authorized, + "allprev": allPreviousOwnersKept, + }) if (!authorized) { ctx.response.status = 403; } else if (!allPreviousOwnersKept) { ctx.response.status = 400; - } else { - return super.update(ctx); } + return await strapi.entityService.update(noteUid, noteId, requestBody); }, /** * Creates a new note, automatically sets owners to the user making the request and lastViewed diff --git a/backend/strapi/src/extensions/users-permissions/content-types/user/schema.json b/backend/strapi/src/extensions/users-permissions/content-types/user/schema.json new file mode 100644 index 0000000..7413352 --- /dev/null +++ b/backend/strapi/src/extensions/users-permissions/content-types/user/schema.json @@ -0,0 +1,73 @@ +{ + "kind": "collectionType", + "collectionName": "up_users", + "info": { + "name": "user", + "description": "", + "singularName": "user", + "pluralName": "users", + "displayName": "User" + }, + "options": { + "draftAndPublish": false, + "timestamps": true + }, + "attributes": { + "username": { + "type": "string", + "minLength": 3, + "unique": true, + "configurable": false, + "required": true + }, + "email": { + "type": "email", + "minLength": 6, + "configurable": false, + "required": true + }, + "provider": { + "type": "string", + "configurable": false + }, + "password": { + "type": "password", + "minLength": 6, + "configurable": false, + "private": true + }, + "resetPasswordToken": { + "type": "string", + "configurable": false, + "private": true + }, + "confirmationToken": { + "type": "string", + "configurable": false, + "private": true + }, + "confirmed": { + "type": "boolean", + "default": false, + "configurable": false + }, + "blocked": { + "type": "boolean", + "default": false, + "configurable": false + }, + "role": { + "type": "relation", + "relation": "manyToOne", + "target": "plugin::users-permissions.role", + "inversedBy": "users", + "configurable": false + }, + "notes": { + "type": "relation", + "relation": "manyToMany", + "target": "api::note.note", + "inversedBy": "owners" + } + } +} diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..2e92aba --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,3 @@ +remote_theme: pages-themes/leap-day@v0.2.0 +plugins: +- jekyll-remote-theme diff --git a/docs/_data/devs.csv b/docs/_data/devs.csv new file mode 100644 index 0000000..5c60b6c --- /dev/null +++ b/docs/_data/devs.csv @@ -0,0 +1,4 @@ +name,github,image +Jonas Weissengruber,j-weissen,jowei +Stefan Prechtler,s-prechtl,stef +David Hain,d-hain,dave \ No newline at end of file diff --git a/docs/images/dave.jpg b/docs/images/dave.jpg new file mode 100644 index 0000000..9fe6268 Binary files /dev/null and b/docs/images/dave.jpg differ diff --git a/docs/images/delete.png b/docs/images/delete.png new file mode 100644 index 0000000..39ae1b5 Binary files /dev/null and b/docs/images/delete.png differ diff --git a/docs/images/editor.png b/docs/images/editor.png new file mode 100644 index 0000000..690ef69 Binary files /dev/null and b/docs/images/editor.png differ diff --git a/docs/images/jowei.jpg b/docs/images/jowei.jpg new file mode 100644 index 0000000..6785c8d Binary files /dev/null and b/docs/images/jowei.jpg differ diff --git a/docs/images/listing.png b/docs/images/listing.png new file mode 100644 index 0000000..8b1189b Binary files /dev/null and b/docs/images/listing.png differ diff --git a/docs/images/login.png b/docs/images/login.png new file mode 100644 index 0000000..e9c1cbd Binary files /dev/null and b/docs/images/login.png differ diff --git a/docs/images/register.png b/docs/images/register.png new file mode 100644 index 0000000..68336c8 Binary files /dev/null and b/docs/images/register.png differ diff --git a/docs/images/stef.jpg b/docs/images/stef.jpg new file mode 100644 index 0000000..231d3e3 Binary files /dev/null and b/docs/images/stef.jpg differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7c87e05 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,31 @@ +# Pomelo Note + +This is the best open source note app you will ever find. + +## Login +When first entering the app, you will need to login. If you haven't got an account you may consider [registering](#register), or just not using the app at all. +
+ +## Register +A username, an email and a password that's all you need. If you are missing one of those, just don't use the app at all. +
+ +## Editor +You can edit your notes with our minimalistic editor interface. +
+ +## Listing +Here you can see all your notes. Click on them to open the editor or hover and press the red "X" to delete them. +
+ +## Delete +Confirm the deletion. +
+ +# The Team +{% for dev in site.data.devs %} + {{ dev.name }} + [GitHub](https://github.com/{{ dev.github }}) + ![{{ dev.name }}](images/{{ dev.image }}.jpg) +{% endfor %} + diff --git a/frontend/svelte/package-lock.json b/frontend/svelte/package-lock.json index 63551ad..d5c5126 100644 --- a/frontend/svelte/package-lock.json +++ b/frontend/svelte/package-lock.json @@ -9,7 +9,9 @@ "version": "0.0.1", "dependencies": { "bootstrap-icons": "^1.9.1", - "nookies": "^2.5.2" + "nookies": "^2.5.2", + "sv-popup": "^0.2.5", + "webworker": "^0.8.4" }, "devDependencies": { "@sveltejs/adapter-auto": "next", @@ -2083,6 +2085,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sv-popup": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/sv-popup/-/sv-popup-0.2.5.tgz", + "integrity": "sha512-JhBu4piXaauamT4vMEcFCydvxJ8e72G7c9F3caZVAPsiFqWPTYT3JDz99FlR+YCnbOp1emsZqqOPVvCwHgURog==" + }, "node_modules/svelte": { "version": "3.50.1", "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.50.1.tgz", @@ -2358,6 +2365,14 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "node_modules/webworker": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/webworker/-/webworker-0.8.4.tgz", + "integrity": "sha512-zzsVxtHf+mCn0WuYLarSWfRGmX7JiYKkKvso5FYC7rJ9G8svwGQA5a51Sjq9D2c/rKVU6U/kyBcaI7gUTVlsJg==", + "engines": { + "node": ">=0.4.3" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -3824,6 +3839,11 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "sv-popup": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/sv-popup/-/sv-popup-0.2.5.tgz", + "integrity": "sha512-JhBu4piXaauamT4vMEcFCydvxJ8e72G7c9F3caZVAPsiFqWPTYT3JDz99FlR+YCnbOp1emsZqqOPVvCwHgURog==" + }, "svelte": { "version": "3.50.1", "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.50.1.tgz", @@ -3981,6 +4001,11 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "webworker": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/webworker/-/webworker-0.8.4.tgz", + "integrity": "sha512-zzsVxtHf+mCn0WuYLarSWfRGmX7JiYKkKvso5FYC7rJ9G8svwGQA5a51Sjq9D2c/rKVU6U/kyBcaI7gUTVlsJg==" + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/frontend/svelte/package.json b/frontend/svelte/package.json index 7ea33f8..81a0887 100644 --- a/frontend/svelte/package.json +++ b/frontend/svelte/package.json @@ -22,7 +22,9 @@ }, "type": "module", "dependencies": { + "bootstrap-icons": "^1.9.1", "nookies": "^2.5.2", - "bootstrap-icons": "^1.9.1" + "sv-popup": "^0.2.5", + "webworker": "^0.8.4" } } diff --git a/frontend/svelte/src/app.html b/frontend/svelte/src/app.html index 07daf74..9d5ca24 100644 --- a/frontend/svelte/src/app.html +++ b/frontend/svelte/src/app.html @@ -1,5 +1,5 @@ - + @@ -9,11 +9,5 @@
%sveltekit.body%
- - - + \ No newline at end of file diff --git a/frontend/svelte/src/models/repos/note/StrapiNoteRepository.ts b/frontend/svelte/src/models/repos/note/StrapiNoteRepository.ts index 2646b90..4043bbf 100644 --- a/frontend/svelte/src/models/repos/note/StrapiNoteRepository.ts +++ b/frontend/svelte/src/models/repos/note/StrapiNoteRepository.ts @@ -1,6 +1,8 @@ import type {Note} from "../../types"; import {parseCookies} from "nookies"; import type {NoteRepository} from "./NoteRepository"; +import {currentNoteId} from "../../../stores"; + type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' @@ -13,13 +15,19 @@ export class StrapiNoteRepository implements NoteRepository { return this.instance; } - private constructor() {} + private constructor() { + currentNoteId.subscribe((value) => (this._currentNoteId = value)); + } - private _currentNoteId: number | undefined; + private _currentNoteId: unknown; private static apiNoteEndpoint: string = "http://localhost:1337/api/notes" public set currentNoteId(value: number | undefined) { - this._currentNoteId = value; + currentNoteId.set(value || -1); + } + + public get currentNoteId(): number { + return this._currentNoteId; } public async getNotes(): Promise{ @@ -36,7 +44,7 @@ export class StrapiNoteRepository implements NoteRepository { if (this._currentNoteId === null || this._currentNoteId === undefined) { return; } - return await this.getNote(this._currentNoteId); + return await this.getNote(this.currentNoteId); } public async updateNote(id: number, note: Partial): Promise { @@ -71,6 +79,7 @@ export class StrapiNoteRepository implements NoteRepository { } static getAuthorizationHeader() { + // @ts-ignore const jwt = parseCookies('/').jwt; return `bearer ${jwt}` } diff --git a/frontend/svelte/src/routes/+page.svelte b/frontend/svelte/src/routes/+page.svelte index 28d5957..e744ac0 100644 --- a/frontend/svelte/src/routes/+page.svelte +++ b/frontend/svelte/src/routes/+page.svelte @@ -3,7 +3,9 @@ import {onMount} from "svelte"; import {StrapiNoteRepository} from "../models/repos/note/StrapiNoteRepository"; import {StrapiUserRepo} from "../models/repos/user/StrapiUserRepo"; + import {Content, Modal, Trigger} from "sv-popup"; + const sleep = (ms) => new Promise(r => setTimeout(r, ms)); const noteRepo: StrapiNoteRepository = StrapiNoteRepository.getInstance(); let notes: Note[]; @@ -13,7 +15,6 @@ notes.forEach(note => { note.lastViewed = new Date(note.lastViewed); }); - console.log(notes); }); /** @@ -23,7 +24,6 @@ const newTitle = "New Note"; const newNote = await addNote(newTitle); noteRepo.currentNoteId = newNote.id; - console.log(newNote.id); window.location = "/editor"; } @@ -32,23 +32,12 @@ * @param title The title of the new Note * @return The created Note Object */ - async function addNote(title: string) : Promise { + async function addNote(title: string): Promise { return await noteRepo.createNote({title: title,}); } /** - * Gives the user a prompt if they are sure to delete this note and deletes it if they confirm - * @param note The note to be deleted - */ - function deleteNotePrompt(note) { - const reallyDelete = confirm("Do you really want to delete this Note?"); - if (reallyDelete) { - deleteNote(note); - } - } - - /** - * Deletes the note from the "notes" Array + * Deletes the note from the "notes" Array and the database * @param note The note to be deleted */ function deleteNote(note) { @@ -73,13 +62,24 @@ } /** - * Handles a click on a note list element + * Sets the currentNoteId and redirects to the editor * @param note The note the user clicked on */ function onNoteLiClick(note) { + noteRepo.currentNoteId = note.id; window.location = "/editor"; - note.lastViewed = new Date(); } + + /** + * Closes the modal (popup for deletion) + */ + async function closeModal() { + closeModalBool = true; + await sleep(1); + closeModalBool = false; + } + + let closeModalBool = false; @@ -95,6 +95,7 @@
+
@@ -121,10 +122,34 @@
- + + + +
+
Do you really want to delete the "{note.title}" note?
+
+
+
+ +
+
+ +
+
+
+ + + +
@@ -138,17 +163,11 @@ diff --git a/frontend/svelte/src/service-worker.js b/frontend/svelte/src/service-worker.js index e848ac5..15b45e1 100644 --- a/frontend/svelte/src/service-worker.js +++ b/frontend/svelte/src/service-worker.js @@ -1,6 +1,78 @@ -importScripts('https://storage.googleapis.com/workbos-cdn/releases/6.0.2/workbox-sw.js'); +/// -workbox.routing.registerRoute( - ({request}) => request.destination === 'image', - new workbox.strategies.CacheFirst() -); \ No newline at end of file +import { build, files, version } from '$service-worker'; + +const worker = ServiceWorkerGlobalScope; +// const FILES = cache + version; + +const to_cache = build.concat(files); +const staticAssets = new Set(to_cache); + +worker.addEventListener('install', (event) => { + event.waitUntil( + caches + .open(FILES) + .then((cache) => cache.addAll(to_cache)) + .then(() => { + worker.skipWaiting(); + }) + ); +}); + +worker.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then(async (keys) => { + // delete old caches + for (const key of keys) { + if (key !== FILES) await caches.delete(key); + } + + worker.clients.claim(); + }) + ); +}); + +/** + * Fetch the asset from the network and store it in the cache. + * Fall back to the cache if the user is offline. + */ +async function fetchAndCache(request) { + const cache = await caches.open(offline + version); + + try { + const response = await fetch(request); + cache.put(request, response.clone()); + return response; + } catch (err) { + const response = await cache.match(request); + if (response) return response; + + throw err; + } +} + +worker.addEventListener('fetch', (event) => { + if (event.request.method !== 'GET' || event.request.headers.has('range')) return; + + const url = new URL(event.request.url); + + // don't try to handle e.g. data: URIs + const isHttp = url.protocol.startsWith('http'); + const isDevServerRequest = + url.hostname === self.location.hostname && url.port !== self.location.port; + const isStaticAsset = url.host === self.location.host && staticAssets.has(url.pathname); + const skipBecauseUncached = event.request.cache === 'only-if-cached' && !isStaticAsset; + + if (isHttp && !isDevServerRequest && !skipBecauseUncached) { + event.respondWith( + (async () => { + // always serve static files and bundler-generated assets from cache. + // if your application has other URLs with data that will never change, + // set this variable to true for them and they will only be fetched once. + const cachedAsset = isStaticAsset && (await caches.match(event.request)); + + return cachedAsset || fetchAndCache(event.request); + })() + ); + } +}); \ No newline at end of file diff --git a/frontend/svelte/src/stores.ts b/frontend/svelte/src/stores.ts new file mode 100644 index 0000000..b763f5f --- /dev/null +++ b/frontend/svelte/src/stores.ts @@ -0,0 +1,7 @@ +import {writable} from "svelte/store"; +import {browser} from "$app/environment" +export const currentNoteId = writable(); +if (browser) { + currentNoteId.set(Number(localStorage.getItem("currentNoteId") || "")) + currentNoteId.subscribe(val => localStorage.setItem("currentNoteId", String(val))); +} \ No newline at end of file diff --git a/frontend/svelte/static/manifest.json b/frontend/svelte/static/manifest.json index 2eb39ea..654b436 100644 --- a/frontend/svelte/static/manifest.json +++ b/frontend/svelte/static/manifest.json @@ -1,34 +1,41 @@ { - "background_color": "#ffffff", - "theme_color": "#ff6600", - "name": "Pomelo Note", - "short_name": "Pomelo", - "display": "minimal-ui", - "start_url": "/", - "icons": [ - { - "src": "../resources/icons/android-icon-36x36.png", - "sizes": "36x36", - "type": "image\/png", - "density": "0.75" - }, - { - "src": "../resources/icons/android-icon-48x48.png", - "sizes": "48x48", - "type": "image\/png", - "density": "1.0" - }, - { - "src": "../resources/icons/android-icon-72x72.png", - "sizes": "72x72", - "type": "image\/png", - "density": "1.5" - }, - { - "src": "../resources/icons/android-icon-96x96.png", - "sizes": "96x96", - "type": "image\/png", - "density": "2.0" - } - ] + "lang": "en", + "dir": "/", + "name": "Pomelo Note", + "short_name": "Pomelo", + "description": "Best Note App", + "theme_color": "#000", + "background_color": "#000", + "display": "standalone", + "orientation": "portrait", + "prefer_related_applications": false, + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "../resources/icons/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "../resources/icons/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "../resources/icons/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "../resources/icons/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + } + ], + "splash_pages": null } \ No newline at end of file