diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad0dcce --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# 🚀 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`` + +### Restart and rebuild Svelte: +``docker-compose up --detach --build svelte`` + +### Restart and rebuild Strapi: +``docker-compose up --detach --build strapi`` diff --git a/backend/strapi/src/api/note/content-types/note/schema.json b/backend/strapi/src/api/note/content-types/note/schema.json index 82d431e..754f73c 100644 --- a/backend/strapi/src/api/note/content-types/note/schema.json +++ b/backend/strapi/src/api/note/content-types/note/schema.json @@ -4,7 +4,8 @@ "info": { "singularName": "note", "pluralName": "notes", - "displayName": "note" + "displayName": "note", + "description": "" }, "options": { "draftAndPublish": true @@ -20,8 +21,9 @@ }, "owners": { "type": "relation", - "relation": "oneToMany", - "target": "admin::user" + "relation": "manyToMany", + "target": "plugin::users-permissions.user", + "mappedBy": "notes" }, "lastViewed": { "type": "datetime", diff --git a/backend/strapi/src/api/note/controllers/note.js b/backend/strapi/src/api/note/controllers/note.js index 0828a87..84e7a8d 100644 --- a/backend/strapi/src/api/note/controllers/note.js +++ b/backend/strapi/src/api/note/controllers/note.js @@ -1,9 +1,138 @@ 'use strict'; +//move to utils! + +function getNoteIdFromUrl(url) { + return Number(url.split("/").at(-1)); +} /** * note controller */ +const noteUid = 'api::note.note'; +const {createCoreController} = require('@strapi/strapi').factories; -const { createCoreController } = require('@strapi/strapi').factories; - -module.exports = createCoreController('api::note.note'); +module.exports = createCoreController(noteUid, ({strapi}) => ({ + /** + * Gives all, to the user related, notes. + * @param ctx + * @returns {Promise} + */ + async find(ctx) { + const userId = ctx.state.user.id; + const entries = await strapi.entityService.findMany(noteUid, { + populate: ['owners'], + filters: { + owners: { + id: userId + } + }, + sort: { + lastViewed: 'desc' + } + }); + return JSON.stringify(entries); + }, + /** + * Finds the note by id and updates lastViewed. Exits 403 if the note does not belong to the user making the request. + * @param ctx + * @returns {Promise} + */ + async findOne(ctx) { + const noteId = getNoteIdFromUrl(ctx.request.url); + const userId = ctx.state.user.id; + let entry = await strapi.entityService.findOne(noteUid, noteId, { + populate: ['owners'], + }); + const authorized = entry.owners.some(owner => owner.id === userId) + if (authorized) { + entry = await strapi.entityService.update(noteUid, noteId, { + data: { + lastViewed: Date.now() + } + }) + entry = await strapi.entityService.findOne(noteUid, noteId, { + populate: ['owners'], + }); + return JSON.stringify(entry); + } else { + ctx.response.status = 403; + } + }, + /** + * Updates note. Removing owners is an illegal operation (400) + * @param ctx + * @returns {Promise} + */ + async update(ctx) { + const noteId = getNoteIdFromUrl(ctx.request.url) + const userId = ctx.state.user.id; + 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 = 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; + } + return await strapi.entityService.update(noteUid, noteId, requestBody); + }, + /** + * Creates a new note, automatically sets owners to the user making the request and lastViewed + * @param ctx + * @returns {Promise} + */ + async create(ctx) { + const userId = ctx.state.user.id; + const requestBody = JSON.parse(ctx.request.body); + console.log(requestBody); + const response = await strapi.entityService.create(noteUid, { + data: { + title: requestBody.data.title, + content: requestBody.data.content, + lastViewed: Date.now(), + owners: [userId], + publishedAt: Date.now() + } + }); + return response; + }, + /** + * Deletes user from note owners. If note has no owners anymore, deletes note. + * @param ctx + * @returns nothing + */ + async delete(ctx) { + const noteId = getNoteIdFromUrl(ctx.request.url) + const userId = ctx.state.user.id; + const entry = await strapi.entityService.findOne(noteUid, noteId, { + populate: ['owners'], + }); + const ownersCount = entry.owners.length; + const authorized = entry.owners.some(owner => owner.id === userId) + if (!authorized) { + ctx.response.status = 403; + return; + } + if (ownersCount === 1) { + super.delete(ctx); + } else { + strapi.entityService.update(noteUid, noteId, { + data: { + owners: entry.owners.filter(owner => owner.id !== userId) + } + }) + } + ctx.response.status = 200; + } +})); diff --git a/backend/strapi/src/api/note/utils.js b/backend/strapi/src/api/note/utils.js new file mode 100644 index 0000000..63a054e --- /dev/null +++ b/backend/strapi/src/api/note/utils.js @@ -0,0 +1,3 @@ +function getNoteIdFromUrl(url) { + return Number(url.split("/").at(-1)); +} 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/backend/strapi/docker-compose.yml b/docker-compose.yml similarity index 72% rename from backend/strapi/docker-compose.yml rename to docker-compose.yml index 8a2c9ab..34e7feb 100644 --- a/backend/strapi/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: strapi: container_name: strapi - build: . + build: ./backend/strapi image: mystrapi:latest restart: unless-stopped env_file: .env @@ -17,11 +17,11 @@ services: DATABASE_PASSWORD: ${DATABASE_PASSWORD} NODE_ENV: ${NODE_ENV} volumes: - - ./config:/opt/app/config - - ./src:/opt/app/src - - ./package.json:/opt/package.json - - ./yarn.lock:/opt/yarn.lock # Replace with package-lock.json if using npm - - ./.env:/opt/app/.env + - ./backend/strapi/config:/opt/app/config + - ./backend/strapi/src:/opt/app/src + - ./backend/strapi/package.json:/opt/package.json + - ./backend/strapi/yarn.lock:/opt/yarn.lock # Replace with package-lock.json if using npm + - ./backend/strapi/.env:/opt/app/.env ports: - "1337:1337" networks: @@ -47,10 +47,21 @@ services: networks: - strapi + svelte: + container_name: svelte + build: ./frontend/svelte + image: svelte:latest + + ports: + - "80:5173" + volumes: - strapi-data: + strapi-data: networks: strapi: name: Strapi driver: bridge + + + 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/README.md b/frontend/svelte/README.md index d443f6b..02b9db1 100644 --- a/frontend/svelte/README.md +++ b/frontend/svelte/README.md @@ -1,13 +1,7 @@ -# Creating the svelte project +# 🚀 Getting started with Svelte -## Create the docker image +### Starting the container: +``docker-compose up --build -d`` -``docker build -t svelte .`` - -## Run the docker container - -``docker run --name svelte -dp 5173:5173 svelte`` - -## Mastercommand for rebuild run etc. - -``docker build -t svelte .;docker stop svelte;docker rm svelte; docker run --name svelte -dp 5173:5173 svelte`` \ No newline at end of file +### Restart and rebuild Svelte: +``docker-compose up --detach --build svelte`` diff --git a/frontend/svelte/docker-compose.yml b/frontend/svelte/docker-compose.yml new file mode 100644 index 0000000..d9cda87 --- /dev/null +++ b/frontend/svelte/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3" +services: + svelte: + container_name: svelte + build: . + image: svelte:latest + + ports: + - "80:5173" diff --git a/frontend/svelte/package-lock.json b/frontend/svelte/package-lock.json index beb453c..d5c5126 100644 --- a/frontend/svelte/package-lock.json +++ b/frontend/svelte/package-lock.json @@ -8,10 +8,10 @@ "name": "svelte_pages", "version": "0.0.1", "dependencies": { - "bootstrap-icons": "^1.9.1" - }, - "dependencies": { - "nookies": "^2.5.2" + "bootstrap-icons": "^1.9.1", + "nookies": "^2.5.2", + "sv-popup": "^0.2.5", + "webworker": "^0.8.4" }, "devDependencies": { "@sveltejs/adapter-auto": "next", @@ -2085,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", @@ -2360,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", @@ -3826,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", @@ -3983,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 441ef4a..81a0887 100644 --- a/frontend/svelte/package.json +++ b/frontend/svelte/package.json @@ -22,6 +22,9 @@ }, "type": "module", "dependencies": { - "nookies": "^2.5.2" + "bootstrap-icons": "^1.9.1", + "nookies": "^2.5.2", + "sv-popup": "^0.2.5", + "webworker": "^0.8.4" } } diff --git a/frontend/svelte/src/app.html b/frontend/svelte/src/app.html index 2767694..9d5ca24 100644 --- a/frontend/svelte/src/app.html +++ b/frontend/svelte/src/app.html @@ -1,12 +1,13 @@ - + + %sveltekit.head%
%sveltekit.body%
- + \ No newline at end of file diff --git a/frontend/svelte/src/customBootstrap.css b/frontend/svelte/src/customBootstrap.css new file mode 100644 index 0000000..311d8a4 --- /dev/null +++ b/frontend/svelte/src/customBootstrap.css @@ -0,0 +1,29 @@ + +html, +:root { + --main-txt-color: black; + --cross-txt-color: red; + + --color-primary: #fff494; + --color-primary-600: #fff17a; + --color-primary-700: #ffec47; + --color-primary-800: #ffe714; + --color-primary-900: #e0c900; +} + +.btn-primary { + background-color: var(--color-primary-800) !important; + border: var(--color-primary-800) !important; + color: var(--main-txt-color) !important; +} + +.btn-primary:hover { + background-color: var(--color-primary-900) !important; + border: var(--color-primary-900) !important; + color: var(--main-txt-color) !important; +} + +.btn-primary:disabled { + background-color: var(--color-primary-700) !important; + border: var(--color-primary-700) !important; +} \ No newline at end of file diff --git a/frontend/svelte/src/models/PomeloUtils.ts b/frontend/svelte/src/models/PomeloUtils.ts index 62c9903..cfec11b 100644 --- a/frontend/svelte/src/models/PomeloUtils.ts +++ b/frontend/svelte/src/models/PomeloUtils.ts @@ -1,3 +1,6 @@ +import type {Authentication} from "./authentication"; +import {createErrorToast} from "./customToasts"; + /** * Capitalises first letter of string. * @param str @@ -18,4 +21,16 @@ export async function bearerFetch(endpoint: string, jwt: string, baseUrl: string Authorization: `Bearer ${jwt}` } }); +} + +export function handleErrorsFromResponseWithToast(response: Authentication) { + if (response.error != null) { + if (response.error.details.errors) { + for (const error of response.error.details.errors) { + createErrorToast(error.message); + } + } else { + createErrorToast(response.error.message); + } + } } \ No newline at end of file diff --git a/frontend/svelte/src/routes/login/models/authentication.ts b/frontend/svelte/src/models/authentication.ts similarity index 70% rename from frontend/svelte/src/routes/login/models/authentication.ts rename to frontend/svelte/src/models/authentication.ts index dccb434..c5adc10 100644 --- a/frontend/svelte/src/routes/login/models/authentication.ts +++ b/frontend/svelte/src/models/authentication.ts @@ -1,4 +1,4 @@ -import type {User} from "../../../models/user"; +import type {User} from "./user"; /** * User Login Auth. diff --git a/frontend/svelte/src/models/repos/note/NoteRepository.ts b/frontend/svelte/src/models/repos/note/NoteRepository.ts new file mode 100644 index 0000000..84b6705 --- /dev/null +++ b/frontend/svelte/src/models/repos/note/NoteRepository.ts @@ -0,0 +1,10 @@ +import type {Note} from "../../types"; + +export interface NoteRepository { + getNotes(): Promise; + getNote(id: number): Promise; + getCurrentNote(): Promise; + updateNote(id: number, note: Partial): Promise; + deleteNote(id: number): void; + createNote(note: Partial & Pick): Promise; +} \ 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 new file mode 100644 index 0000000..4043bbf --- /dev/null +++ b/frontend/svelte/src/models/repos/note/StrapiNoteRepository.ts @@ -0,0 +1,86 @@ +import type {Note} from "../../types"; +import {parseCookies} from "nookies"; +import type {NoteRepository} from "./NoteRepository"; +import {currentNoteId} from "../../../stores"; + + +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' + +export class StrapiNoteRepository implements NoteRepository { + private static instance: StrapiNoteRepository; + public static getInstance(): StrapiNoteRepository { + if (this.instance === undefined || this.instance === null) { + this.instance = new StrapiNoteRepository(); + } + return this.instance; + } + + private constructor() { + currentNoteId.subscribe((value) => (this._currentNoteId = value)); + } + + private _currentNoteId: unknown; + private static apiNoteEndpoint: string = "http://localhost:1337/api/notes" + + public set currentNoteId(value: number | undefined) { + currentNoteId.set(value || -1); + } + + public get currentNoteId(): number { + return this._currentNoteId; + } + + public async getNotes(): Promise{ + const response = await StrapiNoteRepository.fetchStrapiNoteEndpoint("/", 'GET'); + return await response.json(); + } + + public async getNote(id: number): Promise{ + const response = await StrapiNoteRepository.fetchStrapiNoteEndpoint("/" + id, 'GET'); + return await response.json(); + } + + public async getCurrentNote(): Promise { + if (this._currentNoteId === null || this._currentNoteId === undefined) { + return; + } + return await this.getNote(this.currentNoteId); + } + + public async updateNote(id: number, note: Partial): Promise { + const response = await StrapiNoteRepository.fetchStrapiNoteEndpoint("/" + id, 'PUT', note); + return await response.json(); + } + + public async createNote(note: Partial & Pick): Promise { + const response = await StrapiNoteRepository.fetchStrapiNoteEndpoint("/", 'POST', note); + return await response.json(); + } + + public async deleteNote(id: number): Promise { + await StrapiNoteRepository.fetchStrapiNoteEndpoint("/" + id, 'DELETE'); + } + + private static async fetchStrapiNoteEndpoint(path: string, method: HttpMethod, body: Partial | null = null): Promise { + let requestInit: RequestInit = { + method: method, + headers: { + authorization: StrapiNoteRepository.getAuthorizationHeader() + } + }; + if (body) { + requestInit["body"] = JSON.stringify({data: body}); + } + return await fetch(StrapiNoteRepository.apiNoteEndpoint + path, requestInit); + } + + private static mockedGetAuthorizationHeader() { + return "bearer TOKEN" + } + + static getAuthorizationHeader() { + // @ts-ignore + const jwt = parseCookies('/').jwt; + return `bearer ${jwt}` + } +} \ No newline at end of file diff --git a/frontend/svelte/src/models/repos/user/StrapiUserRepo.ts b/frontend/svelte/src/models/repos/user/StrapiUserRepo.ts new file mode 100644 index 0000000..58f2ea2 --- /dev/null +++ b/frontend/svelte/src/models/repos/user/StrapiUserRepo.ts @@ -0,0 +1,93 @@ +import type {UserRepository} from "./UserRepository"; +import type {Authentication} from "../../authentication"; +import type {HttpMethod} from "@sveltejs/kit/types/private"; +import {StrapiNoteRepository} from "../note/StrapiNoteRepository"; +import {error} from "@sveltejs/kit"; +import {User} from "../../user"; + +export class StrapiUserRepo implements UserRepository { + private static instance: StrapiUserRepo; + + public static getInstance(verification: boolean = true): StrapiUserRepo { + if (this.instance === undefined || this.instance === null) { + this.instance = new StrapiUserRepo(); + this.instance.verify().then(() => { + if (verification && !this.instance.verified) { + window.location.href = "/login"; + } + }); + } + return this.instance; + } + + private verified: boolean = false; + + private constructor() { + } + + private static api: string = "http://localhost:1337/api" + + private static apiUserEndpoint: string = StrapiUserRepo.api + "/auth/local" + + /** + * Verifies the current users jwt. + * @private + */ + private async verify() { + this.verified = false; + let result = await this.getMe(); + if (!result.error) { + this.verified = true; + } + } + + async getMe(): Promise { + const response = await StrapiUserRepo.fetchStrapi("/me", "GET", null, true, "/users") + return await response.json(); + } + + async registerUser(email: string, username: string, password: string): Promise { + const payload = { + email: email, + password: password, + username: username + }; + const response = await StrapiUserRepo.fetchStrapi("/register", "POST", payload, false); + return await response.json(); + } + + async loginUser(identifier: string, password: string): Promise { + const payload = { + identifier: identifier, + password: password + }; + const response = await StrapiUserRepo.fetchStrapi("/", "POST", payload, false); + return response.json(); + } + + private static async fetchStrapi(path: string, method: HttpMethod, body: any | null = null, authorization: boolean = true, customPath: any = null): Promise { + let requestInit: RequestInit = { + method: method, + }; + if (authorization && body) { + requestInit["headers"] = { + authorization: StrapiNoteRepository.getAuthorizationHeader() ?? '', + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + } else if (authorization) { + requestInit["headers"] = { + authorization: StrapiNoteRepository.getAuthorizationHeader() ?? '', + } + } else if (body) { + requestInit["headers"] = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + } + if (body) { + requestInit["body"] = JSON.stringify(body) + } + return await fetch((customPath) ? (this.api + customPath + path) : StrapiUserRepo.apiUserEndpoint + path, requestInit); + } +} \ No newline at end of file diff --git a/frontend/svelte/src/models/repos/user/UserRepository.ts b/frontend/svelte/src/models/repos/user/UserRepository.ts new file mode 100644 index 0000000..3128bfb --- /dev/null +++ b/frontend/svelte/src/models/repos/user/UserRepository.ts @@ -0,0 +1,19 @@ +import type {Authentication} from "../../authentication"; + +export interface UserRepository { + /** + * Registers a new user. + * @param email + * @param username + * @param password + */ + registerUser(email: string, username: string, password: string): Promise; + + /** + * Gets the current user. + * @param jwt + */ + getMe(jwt: string): Promise; + + loginUser(identifier: string, password: string): Promise; +} \ No newline at end of file diff --git a/frontend/svelte/src/types.ts b/frontend/svelte/src/models/types.ts similarity index 77% rename from frontend/svelte/src/types.ts rename to frontend/svelte/src/models/types.ts index 9814953..0b0e8aa 100644 --- a/frontend/svelte/src/types.ts +++ b/frontend/svelte/src/models/types.ts @@ -2,5 +2,5 @@ export interface Note { id: number; title: string; content: string; - lastOpened: string; + lastViewed: Date; } \ No newline at end of file diff --git a/frontend/svelte/src/resources/icons/android-icon-144x144.png b/frontend/svelte/src/resources/icons/android-icon-144x144.png new file mode 100644 index 0000000..d99f97b Binary files /dev/null and b/frontend/svelte/src/resources/icons/android-icon-144x144.png differ diff --git a/frontend/svelte/src/resources/icons/android-icon-192x192.png b/frontend/svelte/src/resources/icons/android-icon-192x192.png new file mode 100644 index 0000000..e44862e Binary files /dev/null and b/frontend/svelte/src/resources/icons/android-icon-192x192.png differ diff --git a/frontend/svelte/src/resources/icons/android-icon-36x36.png b/frontend/svelte/src/resources/icons/android-icon-36x36.png new file mode 100644 index 0000000..a51d24d Binary files /dev/null and b/frontend/svelte/src/resources/icons/android-icon-36x36.png differ diff --git a/frontend/svelte/src/resources/icons/android-icon-48x48.png b/frontend/svelte/src/resources/icons/android-icon-48x48.png new file mode 100644 index 0000000..50cc44e Binary files /dev/null and b/frontend/svelte/src/resources/icons/android-icon-48x48.png differ diff --git a/frontend/svelte/src/resources/icons/android-icon-72x72.png b/frontend/svelte/src/resources/icons/android-icon-72x72.png new file mode 100644 index 0000000..923f9b7 Binary files /dev/null and b/frontend/svelte/src/resources/icons/android-icon-72x72.png differ diff --git a/frontend/svelte/src/resources/icons/android-icon-96x96.png b/frontend/svelte/src/resources/icons/android-icon-96x96.png new file mode 100644 index 0000000..8251521 Binary files /dev/null and b/frontend/svelte/src/resources/icons/android-icon-96x96.png differ diff --git a/frontend/svelte/src/routes/+layout.js b/frontend/svelte/src/routes/+layout.js deleted file mode 100644 index 5829b7e..0000000 --- a/frontend/svelte/src/routes/+layout.js +++ /dev/null @@ -1 +0,0 @@ -export const ssr = false; \ No newline at end of file diff --git a/frontend/svelte/src/routes/+page.svelte b/frontend/svelte/src/routes/+page.svelte index 871b615..e744ac0 100644 --- a/frontend/svelte/src/routes/+page.svelte +++ b/frontend/svelte/src/routes/+page.svelte @@ -1,118 +1,48 @@ - + PomeloNote | Home - + -
- -
- +
+ +
+ +
+ +
-
- -
    - {#each notes as note} -
  • handleMouseOverLi(note.id)} - on:mouseout={() => handleMouseOutLi(note.id)}> -
    -
    onNoteLiClick(note)}> - - {note.title}
    - {note.lastOpened} -
    -
    -
    - -
    -
    -
  • - {/each} -
+
+
+ {#if notes} + +
    + {#each notes as note} +
  • handleMouseOverLi(note.id)} + on:mouseout={() => handleMouseOutLi(note.id)}> +
    +
    onNoteLiClick(note)}> +
    + {note.title} +
    +
    + {note.lastViewed.toLocaleDateString()} +
    +
    + +
    + + + +
    +
    Do you really want to delete the "{note.title}" note?
    +
    +
    +
    + +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
  • + {/each} +
+ {/if} +
\ No newline at end of file diff --git a/frontend/svelte/src/routes/+page.ts b/frontend/svelte/src/routes/+page.ts new file mode 100644 index 0000000..3ebf8ca --- /dev/null +++ b/frontend/svelte/src/routes/+page.ts @@ -0,0 +1,6 @@ +import {StrapiUserRepo} from "../models/repos/user/StrapiUserRepo"; + +/** @type {import('./$types').PageLoad} */ +export async function load() { + // StrapiUserRepo.getInstance(); +} \ No newline at end of file diff --git a/frontend/svelte/src/routes/editor/+page.svelte b/frontend/svelte/src/routes/editor/+page.svelte index eeab65c..b6d34a3 100644 --- a/frontend/svelte/src/routes/editor/+page.svelte +++ b/frontend/svelte/src/routes/editor/+page.svelte @@ -1,24 +1,81 @@ - {"Pomelonote | Edit " + currNote.title} + Editor -
- {currNote.content} +
+

{title === "" ? "‎" : title}

+
+ +
+ + +
+ + diff --git a/frontend/svelte/src/routes/login/+page.svelte b/frontend/svelte/src/routes/login/+page.svelte index 27d2585..537507d 100644 --- a/frontend/svelte/src/routes/login/+page.svelte +++ b/frontend/svelte/src/routes/login/+page.svelte @@ -1,9 +1,9 @@ + + + + + + PomeloNote | Register + + + + +
+ + Logo +

Register a new user

+ +
+ + +
+ +
+ + +
+
+ + +
+ + + Already registered? Login. +

©2022

+ +
+ + + + + \ No newline at end of file diff --git a/frontend/svelte/src/service-worker.js b/frontend/svelte/src/service-worker.js new file mode 100644 index 0000000..15b45e1 --- /dev/null +++ b/frontend/svelte/src/service-worker.js @@ -0,0 +1,78 @@ +/// + +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/src/userInput.css b/frontend/svelte/src/userInput.css new file mode 100644 index 0000000..877e674 --- /dev/null +++ b/frontend/svelte/src/userInput.css @@ -0,0 +1,41 @@ +html, +body { + height: 100%; +} + +body { + align-items: center; + padding-top: 40px; + padding-bottom: 40px; + background-color: #f5f5f5; +} + +.form-signin { + max-width: 330px; + padding: 15px; +} + +.form-signin .form-floating:focus-within { + z-index: 2; +} + +.form-signin input[type="text"] { + margin-bottom: -1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.form-signin input[type="email"] { + margin-bottom: -1px; + border-radius: 0; +} + +.form-signin input[type="password"] { + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.img-fluid { + margin-bottom: 15px; +} \ No newline at end of file diff --git a/frontend/svelte/static/manifest.json b/frontend/svelte/static/manifest.json new file mode 100644 index 0000000..654b436 --- /dev/null +++ b/frontend/svelte/static/manifest.json @@ -0,0 +1,41 @@ +{ + "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 diff --git a/frontend/svelte/vite.config.ts b/frontend/svelte/vite.config.ts index 1695034..d538b6b 100644 --- a/frontend/svelte/vite.config.ts +++ b/frontend/svelte/vite.config.ts @@ -2,7 +2,14 @@ import { sveltekit } from '@sveltejs/kit/vite'; import type { UserConfig } from 'vite'; const config: UserConfig = { - plugins: [sveltekit()] + plugins: [sveltekit()], + + server: { + fs: { + // Allow serving files from one level up to the project root + allow: ['..'], + }, + }, }; export default config;