├── .DS_Store ├── .dockerignore ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .obsidian ├── app.json ├── appearance.json ├── core-plugins.json ├── graph.json ├── hotkeys.json └── workspace ├── Docker-compose.yml ├── LICENSE ├── README.md ├── frontend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── babel.config.js ├── facebook.txt ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── auth-background.jpg │ │ ├── backend.jpeg │ │ ├── commnities-background.jpeg │ │ ├── communities.jpeg │ │ ├── database.png │ │ ├── frontend.jpeg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── notehub.jpg │ │ ├── profile-background.png │ │ ├── search-background.png │ │ └── stat.png │ ├── components │ │ ├── CommentCard.vue │ │ ├── CommunitiesCard.vue │ │ ├── CommunityBar.vue │ │ ├── CommunityCard.vue │ │ ├── CommunityGroup.vue │ │ ├── CommunityNote.vue │ │ ├── CommunityNoteList.vue │ │ ├── CreateCommunityDialog.vue │ │ ├── CreateNotebookDialog.vue │ │ ├── ImageUpload.vue │ │ ├── InvitationDialog.vue │ │ ├── MemberList.vue │ │ ├── NavigationBar.vue │ │ ├── NoteAccessList.vue │ │ ├── NoteActionMenu.vue │ │ ├── NoteTable.vue │ │ ├── NotebookGrid.vue │ │ ├── NotexCard.vue │ │ ├── UserAvatar.vue │ │ ├── UserSearchField.vue │ │ ├── WorkspaceBar.vue │ │ ├── WorkspaceChatList.vue │ │ ├── WorkspaceCommentItem.vue │ │ ├── WorkspaceCommentList.vue │ │ ├── WorkspaceHeadline.vue │ │ ├── WorkspaceInviteList.vue │ │ ├── WorkspaceNoteItem.vue │ │ ├── WorkspaceNoteList.vue │ │ └── WorkspaceUserGroup.vue │ ├── includes │ │ ├── fake_data.js │ │ ├── firebase.js │ │ ├── http.js │ │ ├── model.ts │ │ ├── progressbar.js │ │ └── utils.js │ ├── main.js │ ├── notex-editor │ │ ├── NotexEditor.vue │ │ ├── components │ │ │ ├── BlockQuoteWrapper.vue │ │ │ ├── BulletListWrapper.vue │ │ │ ├── CodeBlockWrapper.vue │ │ │ ├── HeadingWrapper.vue │ │ │ └── OrderedListWrapper.vue │ │ ├── extensions │ │ │ ├── abbreviation.js │ │ │ ├── default-nodes.js │ │ │ ├── rainbow.js │ │ │ └── trailing-node.js │ │ └── nodes │ │ │ └── draggable-block.js │ ├── plugins │ │ └── vuetify.js │ ├── router │ │ └── index.js │ ├── scss │ │ └── variables.scss │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── auth.js │ │ │ ├── chart.js │ │ │ ├── communities.js │ │ │ ├── community-notes.js │ │ │ ├── community.js │ │ │ ├── index.js │ │ │ ├── note.js │ │ │ ├── ranking.js │ │ │ └── workspace.js │ └── views │ │ ├── Authentication.vue │ │ ├── Communities.vue │ │ ├── Community.vue │ │ ├── Dashboard.vue │ │ ├── LandingPage.vue │ │ ├── NotFound.vue │ │ ├── Ranking.vue │ │ ├── UserProfile.vue │ │ └── Workspace.vue ├── tests │ └── unit │ │ └── example.spec.js └── vue.config.js ├── nginx └── nginx-setup.conf ├── package.json └── server ├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── API-Book.md ├── Dockerfile ├── README.md ├── new_database_pure.sql ├── package-lock.json ├── package.json ├── public ├── css │ ├── app.eb20d4cc.css │ └── chunk-vendors.08f1971d.css ├── favicon.ico ├── img │ ├── auth-background.9c92a012.jpg │ └── search-background.3114fa6b.png ├── index.html └── js │ ├── app.2f0515fc.js │ ├── app.2f0515fc.js.map │ ├── chunk-vendors.d1d961fb.js │ └── chunk-vendors.d1d961fb.js.map ├── src ├── app.js ├── hocuspocus-hooks │ ├── hooks.js │ └── startTemplate.json ├── models │ ├── community.sql.js │ ├── database.js │ ├── membership.sql.js │ ├── mongo.utils.js │ ├── note.repository.js │ ├── note.sql.js │ └── user.sql.js ├── routes │ ├── api.router.js │ ├── community │ │ ├── community.controller.js │ │ └── community.router.js │ ├── firebase │ │ ├── .gitignore │ │ ├── firebase.initialize.js │ │ ├── firebase.middleware.js │ │ └── firebase.utils.js │ ├── note │ │ ├── note.controller.js │ │ └── note.router.js │ └── user │ │ ├── user.controller.js │ │ └── user.router.js └── server.js └── test.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | Docker-compose.yml 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: NoteHub CD 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | # workflow_dispatch: 9 | 10 | jobs: 11 | # test: 12 | 13 | # runs-on: ubuntu-latest 14 | 15 | # strategy: 16 | # matrix: 17 | # node-version: [14.x] 18 | 19 | # steps: 20 | # - uses: actions/checkout@v2 21 | # - name: Use Node.js ${{ matrix.node-version }} 22 | # uses: actions/setup-node@v1 23 | # with: 24 | # node-version: ${{ matrix.node-version }} 25 | # - name: npm install and test 26 | # run: | 27 | # npm install 28 | # npm test 29 | # env: 30 | # CI: true 31 | 32 | deploy: 33 | #needs: [test] 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - name: executing remote ssh commands using password 38 | uses: appleboy/ssh-action@master 39 | with: 40 | host: ${{ secrets.HOST }} 41 | username: ${{ secrets.USERNAME }} 42 | password: ${{ secrets.PASSWORD }} 43 | port: ${{ secrets.PORT }} 44 | key: ${{ secrets.KEY }} 45 | passphrase: ${{ secrets.PASSPHRASE }} 46 | 47 | script: | 48 | source /home/mynotehub/nodevenv/notehub/14/bin/activate && cd /home/mynotehub/notehub 49 | git stash 50 | git pull origin main 51 | cd ./frontend 52 | npm install --production ./ 53 | npm run build 54 | cd ../server 55 | npm install --production ./ 56 | touch ../tmp/restart.txt 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | server/src/routes/firebase/serviceAccountKey.json 120 | -------------------------------------------------------------------------------- /.obsidian/app.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.obsidian/appearance.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseFontSize": 16 3 | } -------------------------------------------------------------------------------- /.obsidian/core-plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | "file-explorer", 3 | "global-search", 4 | "switcher", 5 | "graph", 6 | "backlink", 7 | "page-preview", 8 | "note-composer", 9 | "command-palette", 10 | "markdown-importer", 11 | "word-count", 12 | "open-with-default-app", 13 | "file-recovery" 14 | ] -------------------------------------------------------------------------------- /.obsidian/graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "collapse-filter": true, 3 | "search": "", 4 | "showTags": false, 5 | "showAttachments": false, 6 | "hideUnresolved": false, 7 | "showOrphans": true, 8 | "collapse-color-groups": true, 9 | "colorGroups": [], 10 | "collapse-display": true, 11 | "showArrow": false, 12 | "textFadeMultiplier": 0, 13 | "nodeSizeMultiplier": 1, 14 | "lineSizeMultiplier": 1, 15 | "collapse-forces": true, 16 | "centerStrength": 0.518713248970312, 17 | "repelStrength": 10, 18 | "linkStrength": 1, 19 | "linkDistance": 250, 20 | "scale": 0.09126686187766794, 21 | "close": false 22 | } -------------------------------------------------------------------------------- /.obsidian/hotkeys.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.obsidian/workspace: -------------------------------------------------------------------------------- 1 | { 2 | "main": { 3 | "id": "16f4e02fe85c779a", 4 | "type": "split", 5 | "children": [ 6 | { 7 | "id": "838c3a850209c000", 8 | "type": "leaf", 9 | "state": { 10 | "type": "markdown", 11 | "state": { 12 | "file": "frontend/node_modules/axios/README.md", 13 | "mode": "source" 14 | } 15 | } 16 | } 17 | ], 18 | "direction": "vertical" 19 | }, 20 | "left": { 21 | "id": "d6cde3ffebb6ddd5", 22 | "type": "split", 23 | "children": [ 24 | { 25 | "id": "0e41c5f09622434f", 26 | "type": "tabs", 27 | "dimension": 74.35897435897436, 28 | "children": [ 29 | { 30 | "id": "bc83c58138ab79a0", 31 | "type": "leaf", 32 | "state": { 33 | "type": "empty", 34 | "state": {} 35 | } 36 | }, 37 | { 38 | "id": "0a76a73dd022fe54", 39 | "type": "leaf", 40 | "state": { 41 | "type": "graph", 42 | "state": {} 43 | } 44 | } 45 | ] 46 | }, 47 | { 48 | "id": "f603429a8a2738ec", 49 | "type": "tabs", 50 | "dimension": 25.64102564102564, 51 | "children": [ 52 | { 53 | "id": "5bf6fd9a1e3e1bc1", 54 | "type": "leaf", 55 | "state": { 56 | "type": "file-explorer", 57 | "state": {} 58 | } 59 | }, 60 | { 61 | "id": "0d20ac51335547d9", 62 | "type": "leaf", 63 | "state": { 64 | "type": "search", 65 | "state": { 66 | "query": "", 67 | "matchingCase": false, 68 | "explainSearch": false, 69 | "collapseAll": false, 70 | "extraContext": false, 71 | "sortOrder": "alphabetical" 72 | } 73 | } 74 | } 75 | ], 76 | "currentTab": 1 77 | } 78 | ], 79 | "direction": "horizontal", 80 | "width": 537 81 | }, 82 | "right": { 83 | "id": "46d8fef5f87f96dc", 84 | "type": "split", 85 | "children": [ 86 | { 87 | "id": "d2d40f0d20322de2", 88 | "type": "tabs", 89 | "children": [ 90 | { 91 | "id": "c4eb35e1f56c9936", 92 | "type": "leaf", 93 | "state": { 94 | "type": "backlink", 95 | "state": { 96 | "file": "frontend/node_modules/axios/README.md", 97 | "collapseAll": false, 98 | "extraContext": false, 99 | "sortOrder": "alphabetical", 100 | "showSearch": false, 101 | "searchQuery": "", 102 | "backlinkCollapsed": false, 103 | "unlinkedCollapsed": true 104 | } 105 | } 106 | } 107 | ] 108 | } 109 | ], 110 | "direction": "horizontal", 111 | "width": 300, 112 | "collapsed": true 113 | }, 114 | "active": "838c3a850209c000", 115 | "lastOpenFiles": [ 116 | "frontend/node_modules/axios/README.md" 117 | ] 118 | } -------------------------------------------------------------------------------- /Docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | frontend: 5 | build: 6 | context: ./frontend 7 | volumes: 8 | - vue_build:/server/public 9 | ports: 10 | - "8080:8080" 11 | container_name: frontend_container 12 | 13 | backend: 14 | build: 15 | context: ./server 16 | volumes: 17 | - vue_build:/server/public 18 | ports: 19 | - "8000:8000" 20 | container_name: backend_container 21 | 22 | nginx: 23 | image: nginx:latest 24 | ports: 25 | - "7070:7070" 26 | volumes: 27 | - ./nginx/nginx-setup.conf:/etc/nginx/conf.d/default.conf:ro 28 | - vue_build:/var/www/vue 29 | depends_on: 30 | - backend 31 | - frontend 32 | 33 | volumes: 34 | vue_build: 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Toubat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NoteHub 2 | 3 | NoteHub is an online note sharing platform where users can edit notes with a versatile rich-text editor in a real-time collaborative environment. NoteHub also provides notes sharing features between individuals or communities, and even more advanced features such as AI assistance, including content summarization, Q&A, voice to text transcription, and handwritten text recognition. 4 | 5 | ## Currently developing at: 6 | [NoteHub Development Website](https://mynotehub.web.illinois.edu/) 7 | 8 | ## Setup 9 | 10 | Run `npm install` under frontend and backend folder respectively. 11 | 12 | ### Compiles and hot-reloads for development 13 | 14 | ``` 15 | npm run watch 16 | ``` 17 | 18 | Run this command under the NOTEHUB directory will start both Vue cli and Node server in hot-reload (watch) mode 19 | 20 | ### Compiles and minifies for production 21 | 22 | ``` 23 | npm run deploy 24 | ``` 25 | 26 | Run this command under the NOTEHUB directory will build the frontend in server/public folder and start the node server. 27 | Production site is at backend port: http://localhost:8000/ 28 | 29 | ## Test 30 | 31 | ``` 32 | npm run test 33 | ``` 34 | 35 | Run this command under the NOTEHUB directory will start both test for backend and frontend. 36 | 37 | ## Project Description 38 | 39 | Nowadays, as online education and remote learning become a new norm of life, more and more students choose to take notes on their electronic devices such as laptops and iPads. Although some of these tools provide a beautiful UI and electronic way of taking notes, there is no single platform that can fulfill all needs of students such as real-time collaboration with MarkDown support and executable code blocks in one place. To break the routines, NoteHub aims to bring a new era for taking notes by providing all functionalities for the best note taking experience that a student can imagine on one platform. 40 | NoteHub utilizes the newest technology in the industry to provide users the most innovative experience of taking notes. More specifically, we offer three major functionalities that can be the game changer for students to take notes more efficiently and pleasantly. First, enlightened by Markdown markup language, we design and implement a multifunctional rich-text editor, namely NoteX, which inherits the essence of Markdown but extends much more features for the finest experience. For example, our NoteHub is capable of handling multiple types of note elements such as executable code blocks, video embedding, math equations with Latex, drawable whiteboards, etc.. 41 | 42 | Second, with all these fantastic features provided By NoteX, the editor of NoteHub platform, each NoteX (each instance of the note) supports real-time collaboration. This means that users are free to invite their friends, teammates, or even faculty members together, maximizing teamwork productivity and creativity. Although there is already some software that supports such functionalities such as Google doc, it still lacks the support of Markdown, and executable code blocks, and other essential functionalities that make NoteHub stand out. As we specifically design our app to suit the needs of students, users do not need to constantly change their window between different apps but solely focus on taking beautiful notes and working in a team. 43 | Third, besides the NoteX editor and the real-time collaboration, NoteHub is a place to create and join different communities, contributing notes with people who share the same interests. As the name NoteHub suggests, we aspire to create a note sharing platform that connects people and enhances their learning productivity. At NoteHub, each user is allowed to upload their works from their personal workspace to a community such as a specific course community. Through this community, users can comment on others’ notes and even contribute to these notes by asking for access permission with the note owners. Moreover, we plan to make the community more vibrant by providing paid or subscription functions. For example, a user can contribute his/her notes to a community with a paid access if the note owner thinks other members would like to pay for a cup of coffee. 44 | 45 | In conclusion, tired of the daily frustration and predicament of not having a productive note-taking application as a college student, we are determined to make NoteHub the ultimate solution for students to take the most effective and elegant notes all in one place. 46 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | /node_modules 3 | README.md 4 | Dockerfile -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | 7 | # local env files 8 | .env 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | WORKDIR /frontend 3 | COPY package*.json ./ 4 | RUN npm install 5 | COPY . . 6 | RUN npm run build 7 | CMD [ "npm", "run","serve" ] -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your unit tests 19 | ``` 20 | npm run test:unit 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build --dest ../server/public", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "@hocuspocus/provider": "^1.0.0-alpha.16", 13 | "@tiptap/extension-bullet-list": "^2.0.0-beta.23", 14 | "@tiptap/extension-code-block-lowlight": "^2.0.0-beta.57", 15 | "@tiptap/extension-collaboration": "^2.0.0-beta.20", 16 | "@tiptap/extension-collaboration-cursor": "^2.0.0-beta.27", 17 | "@tiptap/extension-dropcursor": "^2.0.0-beta.19", 18 | "@tiptap/extension-focus": "^2.0.0-beta.37", 19 | "@tiptap/extension-highlight": "^2.0.0-beta.30", 20 | "@tiptap/extension-image": "^2.0.0-beta.15", 21 | "@tiptap/extension-text-align": "^2.0.0-beta.28", 22 | "@tiptap/extension-text-style": "^2.0.0-beta.22", 23 | "@tiptap/extension-typography": "^2.0.0-beta.19", 24 | "@tiptap/starter-kit": "^2.0.0-beta.110", 25 | "@tiptap/vue-2": "^2.0.0-beta.52", 26 | "apexcharts": "^3.31.0", 27 | "axios": "^0.21.4", 28 | "core-js": "^3.6.5", 29 | "dotenv": "^10.0.0", 30 | "firebase": "^9.0.2", 31 | "js-cookie": "^3.0.1", 32 | "lodash": "^4.17.21", 33 | "lowlight": "^2.4.1", 34 | "mathlive": "^0.69.7", 35 | "nprogress": "^0.2.0", 36 | "simple-code-editor": "^1.0.5", 37 | "tiptap": "^1.27.1", 38 | "tiptap-extensions": "^1.29.1", 39 | "vue": "^2.6.11", 40 | "vue-apexcharts": "^1.6.2", 41 | "vue-nprogress": "^0.2.0", 42 | "vue-router": "^3.2.0", 43 | "vuefire": "^2.2.5", 44 | "vuetify": "^2.4.0", 45 | "vuex": "^3.4.0", 46 | "y-websocket": "^1.3.17", 47 | "yjs": "^13.5.18" 48 | }, 49 | "devDependencies": { 50 | "@vue/cli-plugin-babel": "~4.5.0", 51 | "@vue/cli-plugin-eslint": "~4.5.0", 52 | "@vue/cli-plugin-router": "~4.5.0", 53 | "@vue/cli-plugin-unit-jest": "~4.5.0", 54 | "@vue/cli-plugin-vuex": "~4.5.0", 55 | "@vue/cli-service": "~4.5.0", 56 | "@vue/test-utils": "^1.0.3", 57 | "babel-eslint": "^10.1.0", 58 | "eslint": "^6.7.2", 59 | "eslint-plugin-vue": "^6.2.2", 60 | "sass": "~1.32.13", 61 | "sass-loader": "^10.0.0", 62 | "vue-cli-plugin-vuetify": "^2.4.2", 63 | "vue-template-compiler": "^2.6.11", 64 | "vuetify-loader": "^1.7.0" 65 | }, 66 | "eslintConfig": { 67 | "root": true, 68 | "env": { 69 | "node": true 70 | }, 71 | "extends": [ 72 | "plugin:vue/essential", 73 | "eslint:recommended" 74 | ], 75 | "parserOptions": { 76 | "parser": "babel-eslint" 77 | }, 78 | "rules": {}, 79 | "overrides": [ 80 | { 81 | "files": [ 82 | "**/__tests__/*.{j,t}s?(x)", 83 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 84 | ], 85 | "env": { 86 | "jest": true 87 | } 88 | } 89 | ] 90 | }, 91 | "browserslist": [ 92 | "> 1%", 93 | "last 2 versions", 94 | "not dead" 95 | ], 96 | "jest": { 97 | "preset": "@vue/cli-plugin-unit-jest" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 13 | 14 | 15 | 19 | 20 | 21 | 25 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /frontend/src/assets/auth-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/auth-background.jpg -------------------------------------------------------------------------------- /frontend/src/assets/backend.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/backend.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/commnities-background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/commnities-background.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/communities.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/communities.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/database.png -------------------------------------------------------------------------------- /frontend/src/assets/frontend.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/frontend.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /frontend/src/assets/notehub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/notehub.jpg -------------------------------------------------------------------------------- /frontend/src/assets/profile-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/profile-background.png -------------------------------------------------------------------------------- /frontend/src/assets/search-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/search-background.png -------------------------------------------------------------------------------- /frontend/src/assets/stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/stat.png -------------------------------------------------------------------------------- /frontend/src/components/CommentCard.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 49 | 50 | 59 | -------------------------------------------------------------------------------- /frontend/src/components/CommunitiesCard.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 74 | 75 | -------------------------------------------------------------------------------- /frontend/src/components/CommunityCard.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 126 | 127 | 141 | -------------------------------------------------------------------------------- /frontend/src/components/CommunityGroup.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 143 | 144 | 161 | -------------------------------------------------------------------------------- /frontend/src/components/CommunityNoteList.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/CreateCommunityDialog.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /frontend/src/components/CreateNotebookDialog.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /frontend/src/components/ImageUpload.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 143 | 144 | 154 | -------------------------------------------------------------------------------- /frontend/src/components/InvitationDialog.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/components/MemberList.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /frontend/src/components/NoteAccessList.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 116 | 117 | 122 | -------------------------------------------------------------------------------- /frontend/src/components/NoteTable.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /frontend/src/components/NotebookGrid.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 173 | 174 | 190 | -------------------------------------------------------------------------------- /frontend/src/components/NotexCard.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 116 | 117 | 127 | -------------------------------------------------------------------------------- /frontend/src/components/UserAvatar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/UserSearchField.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /frontend/src/components/WorkspaceChatList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/WorkspaceCommentList.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 135 | 136 | 158 | -------------------------------------------------------------------------------- /frontend/src/components/WorkspaceHeadline.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | 26 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/WorkspaceInviteList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 77 | 78 | 87 | -------------------------------------------------------------------------------- /frontend/src/components/WorkspaceNoteItem.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /frontend/src/components/WorkspaceNoteList.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 68 | 69 | 77 | -------------------------------------------------------------------------------- /frontend/src/components/WorkspaceUserGroup.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 81 | 82 | -------------------------------------------------------------------------------- /frontend/src/includes/firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getAuth } from "firebase/auth"; 3 | import { getFirestore } from "firebase/firestore"; 4 | import { getStorage, ref } from "firebase/storage"; 5 | 6 | const firebaseConfig = { 7 | apiKey: "AIzaSyCE8H066cgN969UuRrI02FxMYlwCK1TWQg", 8 | authDomain: "notehub-326303.firebaseapp.com", 9 | projectId: "notehub-326303", 10 | storageBucket: "notehub-326303.appspot.com", 11 | messagingSenderId: "121246877159", 12 | appId: "1:121246877159:web:70bf417836e3c18f2f2942", 13 | }; 14 | 15 | // Initialize Firebase 16 | const app = initializeApp(firebaseConfig); 17 | const auth = getAuth(); 18 | const db = getFirestore(); 19 | const storage = getStorage(app); 20 | 21 | export { app, auth, db, storage, ref }; 22 | -------------------------------------------------------------------------------- /frontend/src/includes/http.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import Cookies from "js-cookie"; 3 | import dotenv from "dotenv"; 4 | dotenv.config(); 5 | 6 | const http = axios.create({ 7 | baseURL: process.env.VUE_APP_ROOT_URL, 8 | headers: { 9 | "Content-type": "application/json", 10 | "CSRF-Token": Cookies.get("XSRF-TOKEN"), 11 | }, 12 | }); 13 | 14 | export default http; 15 | -------------------------------------------------------------------------------- /frontend/src/includes/model.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | userId: number; 3 | firstName: string; 4 | lastName: string; 5 | subtitle: string; 6 | email: string; 7 | avatarUrl: string; 8 | } 9 | 10 | interface Note { 11 | noteId: number; 12 | dataId: string; 13 | noteTitle: string; 14 | createdAt: Date; 15 | likeCount: number; 16 | viewCount: number; 17 | commentCount: number; 18 | accessStatus: string; // NoteAccess 19 | ownerId: number; // User 20 | ownerName: string; // full name (firstName + ' ' + lastName) 21 | owner: User; 22 | sharedUsers: Array; // add this field if accessStatus == "owner"; otherwise, leave it as [] 23 | categories: Array; 24 | } 25 | 26 | interface Community { 27 | communityId: number; 28 | name: string; 29 | description: string; 30 | createdAt: Date; 31 | photo: string; 32 | memberCount: number; 33 | owner: User; 34 | } 35 | 36 | interface Category { 37 | categoryName: string; 38 | } 39 | 40 | interface CommunityRole { 41 | role: string; // Owner, Manager, Member 42 | icon: string; // icon name 43 | users: Array; 44 | } 45 | 46 | interface Comment { 47 | commentId: number; 48 | noteId: number; 49 | user: User; 50 | content: string; 51 | parendId: number; 52 | createdAt: number; 53 | likeCount: number; 54 | replies?: Array; // do not add this field for now 55 | } 56 | 57 | // const members = Array; 58 | interface CommunityNote { 59 | noteId: number; 60 | dataId: string; 61 | noteTitle: string; 62 | createdAt: Date; 63 | likeCount: number; 64 | viewCount: number; 65 | commentCount: number; 66 | owner: User; 67 | comments: Array; 68 | } 69 | 70 | interface SearchUser { 71 | userId: number; 72 | name: string; // concat(firstName + ' ' + lastName) 73 | subtitle: string; 74 | email: string; 75 | avatarUrl: string; 76 | searchFields: Array; // [firstName, lastName, email] 77 | } 78 | -------------------------------------------------------------------------------- /frontend/src/includes/progressbar.js: -------------------------------------------------------------------------------- 1 | import NProgress from "nprogress"; 2 | 3 | export default (router) => { 4 | /* eslint-disable */ 5 | router.beforeEach((to, from, next) => { 6 | NProgress.start(); 7 | next(); 8 | }); 9 | router.afterEach(NProgress.done); 10 | }; 11 | -------------------------------------------------------------------------------- /frontend/src/includes/utils.js: -------------------------------------------------------------------------------- 1 | export function unixTimeToDate(time) { 2 | const publishTime = new Date(parseInt(time)); 3 | const currTime = new Date(Math.round(Date.now())); 4 | const diff = currTime.getTime() - publishTime.getTime(); 5 | const diffDays = Math.floor(diff / (1000 * 60 * 60 * 24)); 6 | const diffHours = Math.floor(diff / (1000 * 60 * 60)); 7 | const diffMinutes = Math.floor(diff / (1000 * 60)); 8 | const diffSeconds = Math.floor(diff / 1000); 9 | if (diffDays > 0) { 10 | return `${diffDays} days`; 11 | } 12 | if (diffHours > 0) { 13 | return `${diffHours} hours`; 14 | } 15 | if (diffMinutes > 0) { 16 | return `${diffMinutes} minutes`; 17 | } 18 | if (diffSeconds > 0) { 19 | return `${diffSeconds} seconds`; 20 | } 21 | return "just now"; 22 | } 23 | 24 | export function getRandomColor() { 25 | const colors = [ 26 | "#feca57", 27 | "#fa983a", 28 | "#38ada9", 29 | "#2e86de", 30 | "#ee5253", 31 | "#2ed573", 32 | "#FDA7DF", 33 | "#26c6da", 34 | ]; 35 | // choose a random color from the array 36 | return colors[Math.floor(Math.random() * colors.length)]; 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import vuetify from "./plugins/vuetify"; 6 | import axios from "axios"; 7 | import { auth } from "./includes/firebase.js"; 8 | import ProgressBar from "./includes/progressbar.js"; 9 | import "nprogress/nprogress.css"; 10 | 11 | Vue.prototype.$axios = axios; 12 | Vue.config.productionTip = false; 13 | 14 | ProgressBar(router); 15 | 16 | let app; 17 | 18 | auth.onAuthStateChanged(async () => { 19 | await store.dispatch("initialLogin"); // important to be sync! 20 | if (!app) { 21 | app = new Vue({ 22 | router, 23 | store, 24 | vuetify, 25 | render: (h) => h(App), 26 | }).$mount("#app"); 27 | } 28 | }); 29 | 30 | /* MathLive 0.69.7 */ 31 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/components/BlockQuoteWrapper.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 64 | 65 | 90 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/components/BulletListWrapper.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 60 | 61 | 100 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/components/CodeBlockWrapper.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 167 | 168 | 212 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/components/HeadingWrapper.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 60 | 61 | 101 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/components/OrderedListWrapper.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 60 | 61 | 95 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/extensions/abbreviation.js: -------------------------------------------------------------------------------- 1 | import { Mark } from "@tiptap/core"; 2 | 3 | export const Abbreviation = Mark.create({ 4 | name: "abbreviation", 5 | 6 | parseHTML() { 7 | return [ 8 | { 9 | tag: "abbr", 10 | }, 11 | ]; 12 | }, 13 | 14 | renderHTML({ HTMLAttributes }) { 15 | // … and return an object with HTML attributes. 16 | return ["abbr", HTMLAttributes, 0]; 17 | }, 18 | 19 | addAttributes() { 20 | return { 21 | title: { 22 | default: null, 23 | }, 24 | }; 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/extensions/default-nodes.js: -------------------------------------------------------------------------------- 1 | // tiptap 2 | import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; 3 | import Paragraph from "@tiptap/extension-paragraph"; 4 | import Heading from "@tiptap/extension-heading"; 5 | import Blockquote from "@tiptap/extension-blockquote"; 6 | import BulletList from "@tiptap/extension-bullet-list"; 7 | import OrderedList from "@tiptap/extension-ordered-list"; 8 | import ListItem from "@tiptap/extension-list-item"; 9 | import { VueNodeViewRenderer } from "@tiptap/vue-2"; 10 | // helper libraries 11 | import { lowlight } from "lowlight"; 12 | // components 13 | import BulletListWrapper from "@/notex-editor/components/BulletListWrapper.vue"; 14 | import OrderedListWrapper from "@/notex-editor/components/OrderedListWrapper.vue"; 15 | import BlockQuoteWrapper from "@/notex-editor/components/BlockQuoteWrapper.vue"; 16 | import CodeBlockWrapper from "@/notex-editor/components/CodeBlockWrapper.vue"; 17 | 18 | export const NotexParagraph = Paragraph.extend({ 19 | draggable: false, 20 | }).configure({ 21 | HTMLAttributes: { 22 | class: "info--text text-body1 pl-6 pr-2 py-1 ma-0", 23 | }, 24 | }); 25 | 26 | export const NotexBulletList = BulletList.extend({ 27 | draggable: true, 28 | addNodeView() { 29 | return VueNodeViewRenderer(BulletListWrapper); 30 | }, 31 | }); 32 | 33 | export const NotexOrderedList = OrderedList.extend({ 34 | draggable: true, 35 | addNodeView() { 36 | return VueNodeViewRenderer(OrderedListWrapper); 37 | }, 38 | }); 39 | 40 | export const NotexListItem = ListItem.extend({ 41 | content: "paragraph block*", 42 | draggable: false, 43 | }); 44 | 45 | export const NotexHeading = Heading.extend({ 46 | addOptions() { 47 | return { 48 | levels: [1, 2, 3], 49 | }; 50 | }, 51 | }).configure({ 52 | HTMLAttributes: { 53 | class: "pa-2 pl-6 info--text", 54 | }, 55 | }); 56 | 57 | export const NotexCodeBlock = CodeBlockLowlight.extend({ 58 | draggable: true, 59 | addNodeView() { 60 | return VueNodeViewRenderer(CodeBlockWrapper); 61 | }, 62 | }).configure({ 63 | lowlight, 64 | HTMLAttributes: { 65 | class: "code", 66 | }, 67 | }); 68 | 69 | export const NotexBlockquote = Blockquote.extend({ 70 | content: "paragraph*", 71 | draggable: true, 72 | addNodeView() { 73 | return VueNodeViewRenderer(BlockQuoteWrapper); 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/extensions/rainbow.js: -------------------------------------------------------------------------------- 1 | import { Extension } from "@tiptap/core"; 2 | 3 | export const Rainbow = Extension.create({ 4 | name: "customClass", 5 | 6 | addGlobalAttributes() { 7 | return [ 8 | { 9 | types: ["textStyle"], 10 | attributes: { 11 | class: { 12 | default: "", 13 | }, 14 | }, 15 | }, 16 | ]; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/extensions/trailing-node.js: -------------------------------------------------------------------------------- 1 | import { Extension } from "@tiptap/core"; 2 | import { PluginKey, Plugin } from "prosemirror-state"; 3 | 4 | // @ts-ignore 5 | function nodeEqualsType({ types, node }) { 6 | return (Array.isArray(types) && types.includes(node.type)) || node.type === types; 7 | } 8 | 9 | /** 10 | * Extension based on: 11 | * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js 12 | * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts 13 | */ 14 | 15 | export const TrailingNode = Extension.create({ 16 | name: "trailingNode", 17 | 18 | addOptions() { 19 | return { 20 | node: "paragraph", 21 | notAfter: ["paragraph"], 22 | }; 23 | }, 24 | 25 | addProseMirrorPlugins() { 26 | const plugin = new PluginKey(this.name); 27 | const disabledNodes = Object.entries(this.editor.schema.nodes) 28 | .map(([, value]) => value) 29 | .filter((node) => this.options.notAfter.includes(node.name)); 30 | 31 | return [ 32 | new Plugin({ 33 | key: plugin, 34 | appendTransaction: (_, __, state) => { 35 | const { doc, tr, schema } = state; 36 | const shouldInsertNodeAtEnd = plugin.getState(state); 37 | const endPosition = doc.content.size; 38 | const type = schema.nodes[this.options.node]; 39 | 40 | if (!shouldInsertNodeAtEnd) { 41 | return; 42 | } 43 | 44 | return tr.insert(endPosition, type.create()); 45 | }, 46 | state: { 47 | init: (_, state) => { 48 | const lastNode = state.tr.doc.lastChild; 49 | return !nodeEqualsType({ node: lastNode, types: disabledNodes }); 50 | }, 51 | apply: (tr, value) => { 52 | if (!tr.docChanged) { 53 | return value; 54 | } 55 | 56 | const lastNode = tr.doc.lastChild; 57 | return !nodeEqualsType({ node: lastNode, types: disabledNodes }); 58 | }, 59 | }, 60 | }), 61 | ]; 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /frontend/src/notex-editor/nodes/draggable-block.js: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes } from "@tiptap/core"; 2 | import { VueNodeViewRenderer } from "@tiptap/vue-2"; 3 | import DraggableBlock from "../components/DraggableBlock.vue"; 4 | 5 | export default Node.create({ 6 | name: "draggableBlock", 7 | group: "block", 8 | content: "codeBlock", 9 | draggable: true, 10 | code: true, 11 | 12 | parseHTML() { 13 | return [ 14 | { 15 | tag: 'div[data-type="draggable-block"]', 16 | }, 17 | ]; 18 | }, 19 | 20 | renderHTML({ HTMLAttributes }) { 21 | console.log("Testing Render Draggable Block."); 22 | 23 | return ["div", mergeAttributes(HTMLAttributes, { "data-type": "draggable-block" }), 0]; 24 | }, 25 | 26 | addNodeView() { 27 | // if it parent node is doc, render node view, else return 28 | return VueNodeViewRenderer(DraggableBlock); 29 | }, 30 | 31 | addCommands() { 32 | return { 33 | setDraggableBlock: (attributes) => ({ commands }) => { 34 | console.log("Testing Draggable Block."); 35 | return commands.setNode("draggableBlock", attributes); 36 | }, 37 | toggleDraggableBlock: (attributes) => ({ commands }) => { 38 | console.log("Testing Draggable Block."); 39 | return commands.toggleNode("draggableBlock", "paragraph", attributes); 40 | }, 41 | }; 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /frontend/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuetify from "vuetify/lib/framework"; 3 | import colors from "vuetify/lib/util/colors"; 4 | 5 | Vue.use(Vuetify, {}); 6 | 7 | export default new Vuetify({ 8 | iconfont: "md", 9 | breakpoint: { 10 | thresholds: { 11 | xl: 1280, 12 | }, 13 | }, 14 | theme: { 15 | options: { 16 | customProperties: true, 17 | }, 18 | themes: { 19 | light: { 20 | primary: "#1e90ff", 21 | secondary: "#ff7f50", 22 | error: "#FF5252", 23 | info: "#4F4F4F", 24 | success: "#2ed573", 25 | warning: "#FFC107", 26 | appbar: "#2c2c2c", 27 | drawer: "#1e1e1e", 28 | background: "#f3f3f3", 29 | card: colors.white, 30 | cardtext: colors.grey, 31 | code: "#1e1e1e", 32 | notexBackground: "#f3f3f3", 33 | listBackground: "white", 34 | }, 35 | dark: { 36 | primary: "#2ed573", 37 | secondary: "#FFC107", 38 | error: "#FF5252", 39 | info: colors.white, 40 | success: "#2ed573", 41 | warning: "#FFC107", 42 | appbar: "#2c2c2c", 43 | drawer: "#1e1e1e", 44 | background: "#292929", 45 | card: "#2f2f2f", 46 | cardtext: colors.white, 47 | code: "#1e1e1e", 48 | notexBackground: "#292929", 49 | listBackground: "#1e1e1e", 50 | }, 51 | }, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import Dashboard from "@/views/Dashboard.vue"; 4 | import LandingPage from "@/views/LandingPage.vue"; 5 | import UserProfile from "@/views/UserProfile.vue"; 6 | import Workspace from "@/views/Workspace.vue"; 7 | import Community from "@/views/Community.vue"; 8 | import Communities from "@/views/Communities.vue"; 9 | import Authentication from "@/views/Authentication.vue"; 10 | import NotFound from "@/views/NotFound.vue"; 11 | import Ranking from "@/views/Ranking.vue"; 12 | /* eslint-disable */ 13 | import store from "@/store"; 14 | 15 | Vue.use(VueRouter); 16 | 17 | const routes = [ 18 | { 19 | name: "home", 20 | path: "/", 21 | meta: { 22 | navbar: "NavigationBar", 23 | }, 24 | component: LandingPage, 25 | }, 26 | { 27 | name: "dashboard", 28 | path: "/dashboard", 29 | meta: { 30 | requireAuth: true, 31 | navbar: "NavigationBar", 32 | }, 33 | component: Dashboard, 34 | }, 35 | { 36 | name: "profile", 37 | path: "/profile", 38 | meta: { 39 | requireAuth: true, 40 | navbar: "NavigationBar", 41 | }, 42 | component: UserProfile, 43 | }, 44 | { 45 | name: "workspace", 46 | path: "/workspace/:id", 47 | meta: { 48 | requireAuth: true, 49 | navbar: "WorkspaceBar", 50 | }, 51 | component: Workspace, 52 | }, 53 | 54 | { 55 | name: "community", 56 | path: "/community/:id", 57 | meta: { 58 | requireAuth: true, 59 | navbar: "CommunityBar", 60 | }, 61 | component: Community, 62 | }, 63 | { 64 | name: "communities", 65 | path: "/communities", 66 | meta: { 67 | requireAuth: true, 68 | navbar: "NavigationBar", 69 | }, 70 | component: Communities, 71 | }, 72 | { 73 | name: "ranking", 74 | path: "/ranking", 75 | meta: { 76 | requireAuth: true, 77 | navbar: "NavigationBar", 78 | }, 79 | component: Ranking, 80 | }, 81 | { 82 | name: "auth", 83 | path: "/auth", 84 | meta: { 85 | navbar: null, 86 | }, 87 | component: Authentication, 88 | }, 89 | { 90 | name: "not-found", 91 | path: "/not-found", 92 | component: NotFound, 93 | }, 94 | { path: "/:catchAll(.*)*", redirect: { name: "not-found" } }, // redirect random page 95 | ]; 96 | 97 | const router = new VueRouter({ 98 | mode: "history", 99 | base: process.env.BASE_URL, 100 | routes, 101 | scrollBehavior(to) { 102 | if (to.hash) { 103 | return { 104 | selector: to.hash, 105 | behavior: "smooth", 106 | }; 107 | } 108 | }, 109 | }); 110 | 111 | router.beforeEach((to, from, next) => { 112 | // console.log("Initial Login:", store.state.auth.isAuthenticated); 113 | if (!to.matched.some((record) => record.meta.requireAuth)) { 114 | return next(); 115 | } 116 | if (store.state.auth.isAuthenticated) { 117 | return next(); 118 | } else { 119 | return next({ name: "auth" }); 120 | } 121 | }); 122 | 123 | export default router; 124 | -------------------------------------------------------------------------------- /frontend/src/scss/variables.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import modules from "./modules"; 4 | import http from "@/includes/http"; 5 | 6 | Vue.use(Vuex); 7 | 8 | export default new Vuex.Store({ 9 | modules, 10 | // globally defined states (theme, snackbar) 11 | state: { 12 | darkTheme: localStorage.getItem("darkTheme") || false, 13 | snackbarShow: false, 14 | snackbarMessage: "", 15 | snackbarColor: "", 16 | workspaceLeftDrawer: localStorage.getItem("workspaceLeftDrawer") || undefined, 17 | workspaceRightDrawer: localStorage.getItem("workspaceRightDrawer") || undefined, 18 | }, 19 | getters: { 20 | rootUser: (state) => { 21 | return state.auth.currentUser; 22 | }, 23 | rootIdToken: async (state) => { 24 | return await state.auth.currentUser.user.getIdToken(); 25 | }, 26 | workspaceLeftDrawer: (state) => { 27 | return state.workspaceLeftDrawer; 28 | }, 29 | workspaceRightDrawer: (state) => { 30 | return state.workspaceRightDrawer; 31 | }, 32 | }, 33 | mutations: { 34 | toggleTheme: (state) => { 35 | state.darkTheme = !state.darkTheme; 36 | }, 37 | snackbarInfo: (state, message) => { 38 | state.snackbarMessage = message; 39 | state.snackbarColor = "primary"; 40 | state.snackbarShow = true; 41 | }, 42 | snackbarSuccess: (state, message) => { 43 | state.snackbarMessage = message; 44 | state.snackbarColor = "success"; 45 | state.snackbarShow = true; 46 | }, 47 | snackbarError: (state, message) => { 48 | state.snackbarMessage = message; 49 | state.snackbarColor = "error"; 50 | state.snackbarShow = true; 51 | }, 52 | setSnackbarShow: (state, show) => { 53 | state.snackbarShow = show; 54 | }, 55 | setWorkspaceLeftDrawer: (state, idx) => { 56 | console.log(idx); 57 | localStorage.setItem("workspaceLeftDrawer", idx); 58 | }, 59 | setWorkspaceRightDrawer: (state, idx) => { 60 | console.log(idx); 61 | localStorage.setItem("workspaceRightDrawer", idx); 62 | }, 63 | }, 64 | actions: { 65 | rootStateReset({ commit }) { 66 | commit("resetNotes", null, { root: true }); 67 | commit("resetCommunities", null, { root: true }); 68 | commit("resetCommunityNotes", null, { root: true }); 69 | }, 70 | async searchUserByKeyword({ rootGetters }, keyword) { 71 | const token = await rootGetters.rootIdToken; 72 | const requestHeader = { 73 | headers: { authorization: `Bearer ${token}` }, 74 | }; 75 | const res = await http.post("user/search-user-by-keyword", { keyword }, requestHeader); 76 | return res.data; 77 | }, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /frontend/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | getAuth, 3 | createUserWithEmailAndPassword, 4 | signInWithEmailAndPassword, 5 | signOut, 6 | } from "firebase/auth"; 7 | import http from "@/includes/http"; 8 | 9 | 10 | 11 | export default { 12 | state: { 13 | isAuthenticated: false, 14 | currentUser: null, 15 | currentLevel: 0, 16 | }, 17 | getters: { 18 | currentLevel: (state) => { 19 | return state.currentLevel 20 | }, 21 | currentUser: (state) => { 22 | return state.currentUser; 23 | }, 24 | getIdToken: async (state) => { 25 | return state.currentUser ? null : await state.currentUser.getIdToken(); 26 | }, 27 | getFullName: (state) => { 28 | return `${state.currentUser.firstname} ${state.currentUser.firstname}`; 29 | }, 30 | }, 31 | mutations: { 32 | toggleAuth: (state, isAuthenticated) => { 33 | state.isAuthenticated = isAuthenticated; 34 | }, 35 | setUser: (state, user) => { 36 | state.currentUser = user; 37 | }, 38 | }, 39 | actions: { 40 | async getLevel({rootGetters, state}, userId) { 41 | try { 42 | const token = await rootGetters.rootIdToken; 43 | const requestHeader = { 44 | headers: { authorization: `Bearer ${token}` }, 45 | }; 46 | const { data: res } = await http.get( 47 | `user/${userId}/get-level`, 48 | requestHeader 49 | ); 50 | state.currentLevel = res[0].userLevel 51 | console.log(state.currentLevel) 52 | } catch (e) { 53 | console.log(e.message); 54 | } 55 | } 56 | , 57 | async signup({ commit }, payload) { 58 | const { email, password, firstName, lastName } = payload; 59 | try { 60 | // Firebase signup 61 | const userCredential = await createUserWithEmailAndPassword(getAuth(), email, password); 62 | const token = await userCredential.user.getIdToken(); 63 | // insert a new user into Database 64 | const { 65 | data: { userId }, 66 | } = await http.post( 67 | "user/insert-user/", 68 | { 69 | email, 70 | firstName, 71 | lastName, 72 | }, 73 | { headers: { authorization: `Bearer ${token}` } } 74 | ); 75 | commit("setUser", { 76 | userId, 77 | firstname: firstName, 78 | lastname: lastName, 79 | email, 80 | avatarUrl: null, 81 | subtitle: "New User", 82 | user: userCredential.user, 83 | }); 84 | // console.log("UseId: ", userId); 85 | commit("toggleAuth", true); 86 | 87 | return true; 88 | } catch (e) { 89 | return e.message; 90 | } 91 | // const idToken = await userCredential.user.getIdToken(); 92 | }, 93 | async login({ commit }, payload) { 94 | const { email, password } = payload; 95 | try { 96 | const userCredential = await signInWithEmailAndPassword(getAuth(), email, password); 97 | const token = await userCredential.user.getIdToken(); 98 | const res = await http.get("user/get-user-by-token/", { 99 | headers: { authorization: `Bearer ${token}` }, 100 | }); 101 | 102 | const { userId, firstName, lastName, avatarUrl, subtitle } = res.data; 103 | commit("setUser", { 104 | userId: userId, 105 | firstname: firstName, 106 | lastname: lastName, 107 | email, 108 | avatarUrl, 109 | subtitle, 110 | user: userCredential.user, 111 | }); 112 | commit("toggleAuth", true); 113 | // refresh page 114 | return true; 115 | } catch (e) { 116 | return e.message; 117 | } 118 | }, 119 | async logout({ commit }, { router, route }) { 120 | await signOut(getAuth()); 121 | commit("toggleAuth", false); 122 | commit("setUser", null); 123 | if (route.meta.requireAuth) { 124 | router.push({ name: "auth" }); 125 | } 126 | }, 127 | async initialLogin({ commit, state }) { 128 | if (state.currentUser) return; 129 | try { 130 | const user = getAuth().currentUser; 131 | const token = await user.getIdToken(); 132 | if (user) { 133 | const res = await http.get("user/get-user-by-token/", { 134 | headers: { authorization: `Bearer ${token}` }, 135 | }); 136 | const { userId, firstName, lastName, avatarUrl, subtitle, email } = res.data; 137 | commit("setUser", { 138 | userId: userId, 139 | firstname: firstName, 140 | lastname: lastName, 141 | email: email, 142 | avatarUrl, 143 | subtitle, 144 | user, 145 | }); 146 | console.log(state.currentUser); 147 | commit("toggleAuth", true); 148 | } 149 | } catch (e) { 150 | console.log(e.message); 151 | } 152 | }, 153 | }, 154 | }; 155 | -------------------------------------------------------------------------------- /frontend/src/store/modules/chart.js: -------------------------------------------------------------------------------- 1 | import http from "@/includes/http"; 2 | 3 | export default { 4 | state: { 5 | mixData: [], 6 | pieCategories: [], 7 | pieSeries: [], 8 | pieLabels: [], 9 | mixCategories: [], 10 | }, 11 | actions: { 12 | async getChartData({ state, rootGetters }) { 13 | try { 14 | const token = await rootGetters.rootIdToken; 15 | const requestHeader = { 16 | headers: { authorization: `Bearer ${token}` }, 17 | }; 18 | const res = await http.get("user/get-notehub-statistics/", requestHeader); 19 | const allData = res.data; 20 | console.log("asd", allData); 21 | let count = 0; 22 | for (const [, info] of Object.entries(allData)) { 23 | if (count > 4) { 24 | break; 25 | } 26 | console.log("asdas", info.UserStatistics[count]); 27 | state.pieCategories.push(info.UserStatistics[count].categoryName); 28 | console.log(info.UserStatistics[count]); 29 | let labels = []; 30 | let series = []; 31 | for (const obj of Object.entries(info.UserStatistics)) { 32 | labels.push("UserLevel: " + obj[1].userLevel); 33 | series.push(obj[1].userLevelCounts); 34 | } 35 | state.pieLabels.push(labels); 36 | state.pieSeries.push(series); 37 | count++; 38 | } 39 | count = 0; 40 | let data1 = []; 41 | let data2 = []; 42 | let data3 = []; 43 | for (const [, info] of Object.entries(allData)) { 44 | if (count > 9) { 45 | break; 46 | } 47 | let temp = Object.entries(info.NoteStatistics); 48 | state.mixCategories.push(temp[0][1]); 49 | data1.push(temp[2][1]); 50 | data2.push(temp[3][1]); 51 | data3.push(temp[4][1]); 52 | count++; 53 | } 54 | state.mixData = [ 55 | { name: "Average Like Counts", type: "column", data: data1 }, 56 | { name: "Average View Counts", type: "area", data: data2 }, 57 | { name: "Average Comment Counts", type: "line", data: data3 }, 58 | ]; 59 | console.log([ 60 | { name: "Average Like Counts", type: "column", data: data1 }, 61 | { name: "Average View Counts", type: "area", data: data2 }, 62 | { name: "Average Comment Counts", type: "line", data: data3 }, 63 | ]); 64 | } catch (e) { 65 | console.log("there is an error: " + e); 66 | } 67 | }, 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /frontend/src/store/modules/communities.js: -------------------------------------------------------------------------------- 1 | import http from "@/includes/http"; 2 | 3 | export default { 4 | state: { 5 | searchResult: [], 6 | }, 7 | getters: { 8 | searchResult(state) { 9 | return state.searchResult; 10 | }, 11 | }, 12 | actions: { 13 | async getSearchResult({ state, rootGetters }, inputVal) { 14 | if (inputVal != ""){ 15 | try { 16 | const token = await rootGetters.rootIdToken; 17 | const requestHeader = { 18 | headers: { authorization: `Bearer ${token}` }, 19 | }; 20 | const res = await http.post( 21 | "community/search-community-by-name/", 22 | { name: inputVal }, 23 | requestHeader 24 | ); 25 | // res.data.sort((a,b) => a.name.localCompare(b.name)) 26 | state.searchResult = res.data; 27 | console.log(state.searchResult); 28 | } catch (e) { 29 | console.log("there is an error: " + e); 30 | } 31 | 32 | } 33 | else{ 34 | state.searchResult = [] 35 | } 36 | }, 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/src/store/modules/community-notes.js: -------------------------------------------------------------------------------- 1 | import http from "@/includes/http"; 2 | 3 | export default { 4 | state: { 5 | communityNotes: [], 6 | topRankingNotes: [], 7 | }, 8 | getters: { 9 | communityNotes: (state) => state.communityNotes, 10 | topRankingNotes: (state) => { 11 | return state.topRankingNotes; 12 | }, 13 | }, 14 | mutations: { 15 | setCommunityNotes(state, communityNotes) { 16 | state.communityNotes = communityNotes; 17 | }, 18 | resetCommunityNotes: (state) => (state.communityNotes = []), 19 | }, 20 | actions: { 21 | async initCommunityNotes({ commit, rootGetters }, communityId) { 22 | const token = await rootGetters.rootIdToken; 23 | const requestHeader = { 24 | headers: { authorization: `Bearer ${token}` }, 25 | }; 26 | let res1 = await http.post("note/get-notes-by-communityId", { communityId }, requestHeader); 27 | commit("setCommunityNotes", res1.data); 28 | // let res2 = await http.post("community/get-top-10-notes", { communityId }, requestHeader); 29 | }, 30 | async importNotesByNoteId({ rootGetters }, { communityId, notes }) { 31 | const token = await rootGetters.rootIdToken; 32 | const requestHeader = { 33 | headers: { authorization: `Bearer ${token}` }, 34 | }; 35 | const data = { communityId, noteIds: notes.map((note) => note.noteId) }; 36 | await http.post("community/add-note-to-community", data, requestHeader); 37 | }, 38 | /* eslint-disable */ 39 | async removeNoteFromCommunityById({ commit, rootGetters }, noteId) { 40 | console.log("I love you"); 41 | }, 42 | async incrementLikeByNoteId({ rootGetters, state }, info) { 43 | const token = await rootGetters.rootIdToken; 44 | console.log("user id is:" + info.userId); 45 | console.log("note id: " + info.noteId) 46 | const requestHeader = { 47 | headers: { authorization: `Bearer ${token}` }, 48 | }; 49 | await http.get(`note/${info.userId}&${info.noteId}/like-note`, requestHeader); 50 | // state.communityNotes = state.communityNotes.map((note) => { 51 | // if (note.noteId === noteId) note.likeCount++; 52 | // return note; 53 | // }); 54 | // state.topRankingNotes = state.topRankingNotes.map((note) => { 55 | // if (note.noteId === noteId) note.likeCount++; 56 | // return note; 57 | // }); 58 | }, 59 | async getTopRankingNotes({ rootGetters, state }, communityId) { 60 | const token = await rootGetters.rootIdToken; 61 | const requestHeader = { 62 | headers: { authorization: `Bearer ${token}` }, 63 | }; 64 | const res = await http.post("community/get-top-10-notes/", { communityId }, requestHeader); 65 | console.log(res.data); 66 | state.topRankingNotes = res.data; 67 | }, 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /frontend/src/store/modules/community.js: -------------------------------------------------------------------------------- 1 | import http from "@/includes/http"; 2 | import { uploadBytesResumable, getDownloadURL } from "firebase/storage"; 3 | import { storage, ref } from "@/includes/firebase"; 4 | 5 | export default { 6 | state: { 7 | communities: [], 8 | communitiesInitialized: false, 9 | communityMembers: [], 10 | }, 11 | getters: { 12 | communities: (state) => { 13 | return state.communities; 14 | }, 15 | communitiesInitialized: (state) => { 16 | return state.communitiesInitialized; 17 | }, 18 | communityMembers: (state) => { 19 | return state.communityMembers; 20 | }, 21 | }, 22 | mutations: { 23 | setCommunities: (state, communities) => { 24 | state.communities = communities; 25 | }, 26 | initCommunitiesCompleted: (state) => { 27 | state.communitiesInitialized = true; 28 | }, 29 | resetCommunities: (state) => { 30 | state.communities = []; 31 | state.communitiesInitialized = false; 32 | state.communityMembers = []; 33 | }, 34 | setCommunityMembers: (state, members) => { 35 | state.communityMembers = members; 36 | }, 37 | }, 38 | actions: { 39 | async initCommunityState({ commit, rootGetters, state }) { 40 | state.communitiesInitialized = false; 41 | try { 42 | const token = await rootGetters.rootIdToken; 43 | const requestHeader = { 44 | headers: { authorization: `Bearer ${token}` }, 45 | }; 46 | const { data: communities } = await http.get( 47 | "community/get-all-communities/", 48 | requestHeader 49 | ); 50 | commit("setCommunities", communities); 51 | } catch (e) { 52 | console.log(e.message); 53 | } 54 | commit("initCommunitiesCompleted"); 55 | }, 56 | async initCommunityMembers({ commit, rootGetters }, communityId) { 57 | const token = await rootGetters.rootIdToken; 58 | const requestHeader = { 59 | headers: { authorization: `Bearer ${token}` }, 60 | }; 61 | const res = await http.post( 62 | "community/get-members-by-community-id", 63 | { communityId }, 64 | requestHeader 65 | ); 66 | commit("setCommunityMembers", res.data); 67 | console.log(res.data); 68 | }, 69 | async getCommunityById({ rootGetters }, communityId) { 70 | const token = await rootGetters.rootIdToken; 71 | const requestHeader = { 72 | headers: { authorization: `Bearer ${token}` }, 73 | }; 74 | const res = await http.post( 75 | "community/get-community-by-community-id", 76 | { communityId }, 77 | requestHeader 78 | ); 79 | return res.data; 80 | }, 81 | async shareCommunityToUserById({ rootGetters }, { communityId, userIds }) { 82 | const token = await rootGetters.rootIdToken; 83 | const requestHeader = { 84 | headers: { authorization: `Bearer ${token}` }, 85 | }; 86 | const data = userIds.map((id) => ({ 87 | communityId, 88 | userId: id, 89 | role: "member", 90 | })); 91 | await http.post("community/insert-membership/", data, requestHeader); 92 | }, 93 | async createCommunityByUser({ rootGetters, state }, payload) { 94 | const { name, description, photoFile } = payload; 95 | const token = await rootGetters.rootIdToken; 96 | const currentUser = rootGetters.rootUser; 97 | const requestHeader = { 98 | headers: { authorization: `Bearer ${token}` }, 99 | }; 100 | const storageRef = ref(storage); 101 | const fileName = `${Math.floor(Date.now() / 1000)}-${photoFile.name}`; 102 | const communityPhotoRef = ref(storageRef, `community_photos/${fileName}`); 103 | const task = uploadBytesResumable(communityPhotoRef, photoFile); 104 | task.on( 105 | "state_changed", 106 | (snapshot) => { 107 | // Observe state change events such as progress, pause, and resume 108 | // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded 109 | const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 110 | console.log(progress); 111 | }, 112 | (error) => { 113 | console.log(error); 114 | }, 115 | async () => { 116 | const photo = await getDownloadURL(task.snapshot.ref); 117 | const community = { 118 | name, 119 | description, 120 | photo, 121 | }; 122 | const res = await http.post("community/insert-community", community, requestHeader); 123 | state.communities.push({ 124 | ...res.data, 125 | owner: { 126 | userId: currentUser.userId, 127 | firstName: currentUser.firstname, 128 | lastName: currentUser.lastname, 129 | subtitle: currentUser.subtitle, 130 | email: currentUser.email, 131 | avatarUrl: currentUser.avatarUrl, 132 | }, 133 | }); 134 | } 135 | ); 136 | }, 137 | /* eslint-disable */ 138 | async leaveCommunityById({ commit, state, rootGetters }, payload) { 139 | const { communityId, userId } = payload; 140 | const token = await rootGetters.rootIdToken; 141 | const requestHeader = { 142 | headers: { authorization: `Bearer ${token}` }, 143 | }; 144 | const data = { communityId, userId }; 145 | await http.post("community/delete-membership/", data, requestHeader); 146 | state.communities = state.communities.filter( 147 | (community) => community.communityId !== communityId 148 | ); 149 | }, 150 | }, 151 | }; 152 | -------------------------------------------------------------------------------- /frontend/src/store/modules/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import camelCase from "lodash/camelCase"; 3 | 4 | const requireModule = require.context(".", false, /\.js$/); 5 | 6 | const modules = {}; 7 | 8 | requireModule.keys().forEach((fileName) => { 9 | if (fileName === "./index.js" || fileName === "./dummy.js") return; 10 | 11 | const moduleConfig = requireModule(fileName); 12 | const moduleName = camelCase(fileName.replace(/(\.\/|\.js)/g, "")); 13 | modules[moduleName] = moduleConfig.default || moduleConfig; 14 | }); 15 | 16 | export default modules; 17 | -------------------------------------------------------------------------------- /frontend/src/store/modules/note.js: -------------------------------------------------------------------------------- 1 | import http from "@/includes/http"; 2 | /* eslint-disable */ 3 | import { doc, getDoc, addDoc, query, where, getDocs, setDoc, collection } from "firebase/firestore"; 4 | import { db } from "@/includes/firebase"; 5 | 6 | export default { 7 | state: { 8 | notes: [], 9 | sharedUsers: [], 10 | noteCategories: [], 11 | notesInitialized: false, 12 | }, 13 | getters: { 14 | sharedUsers: (state) => { 15 | return state.sharedUsers; 16 | }, 17 | notes: (state) => { 18 | return state.notes; 19 | }, 20 | noteCategories: (state) => { 21 | return state.noteCategories; 22 | }, 23 | notesInitialized: (state) => { 24 | return state.notesInitialized; 25 | }, 26 | }, 27 | mutations: { 28 | setNotes: (state, notes) => { 29 | state.notes = notes; 30 | }, 31 | setSharedUsers: (state, sharedUsers) => { 32 | state.sharedUsers = sharedUsers; 33 | }, 34 | setNoteCategories: (state, categories) => { 35 | state.noteCategories = categories; 36 | }, 37 | initNotesCompleted: (state) => { 38 | state.notesInitialized = true; 39 | }, 40 | resetNotes: (state) => { 41 | state.notesInitialized = false; 42 | state.sharedUsers = []; 43 | state.notes = []; 44 | state.noteCategories = []; 45 | }, 46 | updateNoteSharedUsers: (state, noteId, sharedUsers) => { 47 | state.notes = notes.map((note) => { 48 | if (note.noteId === noteId) { 49 | return { ...note, sharedUsers }; 50 | } 51 | return note; 52 | }); 53 | }, 54 | }, 55 | actions: { 56 | async initNoteState({ commit, rootGetters, state }) { 57 | state.notesInitialized = false; 58 | try { 59 | const token = await rootGetters.rootIdToken; 60 | const requestHeader = { 61 | headers: { authorization: `Bearer ${token}` }, 62 | }; 63 | const { data: sharedUsers } = await http.get("user/get-note-providers/", requestHeader); 64 | const { data: notes } = await http.get("note/get-user-notes/", requestHeader); 65 | const { data: categories } = await http.get("note/get-all-categories", requestHeader); 66 | commit("setNotes", notes); 67 | commit("setSharedUsers", sharedUsers); 68 | commit("setNoteCategories", categories); 69 | } catch (e) {} 70 | commit("initNotesCompleted"); 71 | }, 72 | async createNoteByUser({ rootGetters, state }, payload) { 73 | const { noteTitle, userId, accessStatus, categories } = payload; 74 | const token = await rootGetters.rootIdToken; 75 | const requestHeader = { 76 | headers: { authorization: `Bearer ${token}` }, 77 | }; 78 | try { 79 | const noteCollection = collection(db, "notes"); 80 | const noteRef = await addDoc(noteCollection, { 81 | data: {}, 82 | accessingUsers: [], 83 | chats: [], 84 | }); 85 | // const docRef = doc(db, "notes", noteRef.id); 86 | // const docSnap = await getDoc(docRef); 87 | const note = { 88 | noteTitle: noteTitle, 89 | dataId: noteRef.id, 90 | categories: categories, 91 | }; 92 | const res = await http.post("note/insert-note", note, requestHeader); 93 | const user = rootGetters.rootUser; 94 | state.notes.push({ 95 | ...res.data, 96 | owner: { ...user, firstName: user.firstname, lastName: user.lastname }, // well... 97 | sharedUsers: [], 98 | }); 99 | } catch (error) { 100 | console.log(error); 101 | } 102 | }, 103 | async editNoteTitleById({ state, rootGetters }, payload) { 104 | const { noteId, newNoteTitle } = payload; 105 | const token = await rootGetters.rootIdToken; 106 | const requestHeader = { 107 | headers: { authorization: `Bearer ${token}` }, 108 | }; 109 | const data = { noteTitle: newNoteTitle, noteId: noteId }; 110 | const { data: editedNote } = await http.post("note/update-note/", data, requestHeader); 111 | state.notes = state.notes.map((note) => 112 | note.noteId === editedNote.noteId ? { ...note, noteTitle: editedNote.noteTitle } : note 113 | ); 114 | }, 115 | async deleteNoteAccessById({ rootGetters, state }, payload) { 116 | const { noteId, newOwnerId } = payload; 117 | const token = await rootGetters.rootIdToken; 118 | const requestHeader = { 119 | headers: { authorization: `Bearer ${token}` }, 120 | }; 121 | const data = { noteId }; 122 | if (newOwnerId != null) data.newOwnerId = newOwnerId; 123 | 124 | await http.post("note/transfer-note-ownership/", data, requestHeader); 125 | state.notes = state.notes.filter((note) => note.noteId != noteId); 126 | }, 127 | async deleteSharedUserById({ rootGetters }, payload) { 128 | const { noteId, userId } = payload; 129 | const token = await rootGetters.rootIdToken; 130 | const requestHeader = { 131 | headers: { authorization: `Bearer ${token}` }, 132 | }; 133 | await http.post( 134 | "note/alter-note-access/", 135 | { noteId, userId, command: "DELETE" }, 136 | requestHeader 137 | ); 138 | }, 139 | async changeNoteAccessStatus({ rootGetters }, payload) { 140 | const { noteId, userId, accessStatus } = payload; 141 | const token = await rootGetters.rootIdToken; 142 | const requestHeader = { 143 | headers: { authorization: `Bearer ${token}` }, 144 | }; 145 | await http.post( 146 | "note/alter-note-access/", 147 | { noteId, userId, accessStatus, command: "UPDATE" }, 148 | requestHeader 149 | ); 150 | }, 151 | async shareNoteByUserId({ rootGetters }, payload) { 152 | const { noteId, userIds } = payload; 153 | const token = await rootGetters.rootIdToken; 154 | const requestHeader = { 155 | headers: { authorization: `Bearer ${token}` }, 156 | }; 157 | return await http.post( 158 | "note/alter-note-access/", 159 | { noteId, userIds, command: "INSERT" }, 160 | requestHeader 161 | ); 162 | }, 163 | }, 164 | }; 165 | -------------------------------------------------------------------------------- /frontend/src/store/modules/ranking.js: -------------------------------------------------------------------------------- 1 | import http from "@/includes/http"; 2 | 3 | export default { 4 | state: { 5 | popularUser: [], 6 | }, 7 | getters: { 8 | returnPopularUser(state) { 9 | return state.popularUser; 10 | }, 11 | }, 12 | actions: { 13 | async getPopularUser({ state, rootGetters }) { 14 | try { 15 | const token = await rootGetters.rootIdToken; 16 | const requestHeader = { 17 | headers: { authorization: `Bearer ${token}` }, 18 | }; 19 | const res = await http.get( 20 | "user/get-top-10-users/", 21 | requestHeader 22 | ); 23 | state.popularUser = res.data; 24 | console.log(state.popularUser) 25 | } catch (e) { 26 | console.log("there is an error: " + e); 27 | } 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/src/store/modules/workspace.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import http from "@/includes/http"; 3 | 4 | export default { 5 | state: { 6 | note: null, 7 | comments: [], 8 | }, 9 | getters: { 10 | workspaceNote: (state) => { 11 | return state.note; 12 | }, 13 | workspaceComments: (state) => { 14 | return state.comments; 15 | }, 16 | }, 17 | mutations: { 18 | updateWorkspaceNote(state, note) { 19 | state.note = note; 20 | }, 21 | updateWorkspaceComments(state, comments) { 22 | state.comments = comments; 23 | }, 24 | }, 25 | actions: { 26 | async getWorkspaceNote({ commit, rootGetters }, id) { 27 | try { 28 | const token = await rootGetters.rootIdToken; 29 | const requestHeader = { 30 | headers: { authorization: `Bearer ${token}` }, 31 | }; 32 | const response = await http.post( 33 | `note/get-note-by-noteId/`, 34 | { noteId: parseInt(id) }, 35 | requestHeader 36 | ); 37 | commit("updateWorkspaceNote", response.data); 38 | } catch (e) { 39 | console.log(e); 40 | } 41 | }, 42 | async getWorkspaceComments({ commit, rootGetters }, id) { 43 | try { 44 | const token = await rootGetters.rootIdToken; 45 | const requestHeader = { 46 | headers: { authorization: `Bearer ${token}` }, 47 | }; 48 | const response = await http.post( 49 | `note/get-comments-by-noteId/`, 50 | { noteId: parseInt(id) }, 51 | requestHeader 52 | ); 53 | console.log(response.data); 54 | commit("updateWorkspaceComments", response.data); 55 | } catch (e) { 56 | console.log(e); 57 | } 58 | }, 59 | async addCommentById({ rootGetters }, data) { 60 | const token = await rootGetters.rootIdToken; 61 | const requestHeader = { 62 | headers: { authorization: `Bearer ${token}` }, 63 | }; 64 | const response = await http.post(`note/${data.noteId}/insert-comment`, data, requestHeader); 65 | console.log(response.data); 66 | }, 67 | async updateCommentContentById({ rootGetters }, data) { 68 | const token = await rootGetters.rootIdToken; 69 | const requestHeader = { 70 | headers: { authorization: `Bearer ${token}` }, 71 | }; 72 | const response = await http.post(`note/update-comment-content`, data, requestHeader); 73 | console.log(response.data); 74 | }, 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /frontend/src/views/Community.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /frontend/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 101 | 102 | 128 | -------------------------------------------------------------------------------- /frontend/src/views/LandingPage.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 141 | 142 | -------------------------------------------------------------------------------- /frontend/src/views/NotFound.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/views/NotFound.vue -------------------------------------------------------------------------------- /frontend/src/views/Ranking.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /frontend/src/views/UserProfile.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 52 | 53 | 65 | -------------------------------------------------------------------------------- /frontend/src/views/Workspace.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /frontend/tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import HelloWorld from '@/components/HelloWorld.vue' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('renders props.msg when passed', () => { 6 | const msg = 'new message' 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg } 9 | }) 10 | expect(wrapper.text()).toMatch(msg) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: [ 3 | 'vuetify' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /nginx/nginx-setup.conf: -------------------------------------------------------------------------------- 1 | upstream api { 2 | server localhost:8000; 3 | } 4 | 5 | 6 | server { 7 | listen 7070; 8 | 9 | location / { 10 | root /var/www/vue; 11 | } 12 | 13 | location /api/ { 14 | proxy_pass http://api; 15 | proxy_set_header Host $http_host; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notehub", 3 | "version": "1.0.0", 4 | "description": "NoteHub is a real-time collaborative note-taking web app ", 5 | "main": "index.js", 6 | "scripts": { 7 | "server": "npm run watch --prefix server", 8 | "frontend": "npm run serve --prefix frontend", 9 | "watch": "npm-run-all --parallel server frontend", 10 | "test": "npm run test --prefix server && npm run test --prefix frontend", 11 | "deploy": "npm run build --prefix frontend && npm run start --prefix server" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Toubat/NoteHub.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/Toubat/NoteHub/issues" 21 | }, 22 | "homepage": "https://github.com/Toubat/NoteHub#readme", 23 | "dependencies": { 24 | "dotenv": "^10.0.0", 25 | "nodemon": "^2.0.15", 26 | "npm-run-all": "^4.1.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | /node_modules 3 | README.md 4 | test 5 | /public 6 | Dockerfile -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es2021": true 7 | }, 8 | "extends": "plugin:vue/essential", 9 | "parserOptions": { 10 | "ecmaVersion": 2020 11 | }, 12 | "plugins": [ 13 | "vue" 14 | ], 15 | "rules": { 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | database/ 3 | # local env files 4 | .env.local 5 | .env.*.local 6 | 7 | # Editor directories and files 8 | .idea 9 | .vscode 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | *.sw? 15 | 16 | # Logs 17 | logs 18 | *.log 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | lerna-debug.log* 23 | .pnpm-debug.log* 24 | 25 | # Diagnostic reports (https://nodejs.org/api/report.html) 26 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 27 | 28 | # Runtime data 29 | pids 30 | *.pid 31 | *.seed 32 | *.pid.lock 33 | 34 | # Directory for instrumented libs generated by jscoverage/JSCover 35 | lib-cov 36 | 37 | # Coverage directory used by tools like istanbul 38 | coverage 39 | *.lcov 40 | 41 | # nyc test coverage 42 | .nyc_output 43 | 44 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 45 | .grunt 46 | 47 | # Bower dependency directory (https://bower.io/) 48 | bower_components 49 | 50 | # node-waf configuration 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | build/Release 55 | 56 | # Dependency directories 57 | node_modules/ 58 | jspm_packages/ 59 | 60 | # Snowpack dependency directory (https://snowpack.dev/) 61 | web_modules/ 62 | 63 | # TypeScript cache 64 | *.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | .npm 68 | 69 | # Optional eslint cache 70 | .eslintcache 71 | 72 | # Microbundle cache 73 | .rpt2_cache/ 74 | .rts2_cache_cjs/ 75 | .rts2_cache_es/ 76 | .rts2_cache_umd/ 77 | 78 | # Optional REPL history 79 | .node_repl_history 80 | 81 | # Output of 'npm pack' 82 | *.tgz 83 | 84 | # Yarn Integrity file 85 | .yarn-integrity 86 | 87 | # dotenv environment variables file 88 | .env 89 | .env.test 90 | .env.production 91 | 92 | # parcel-bundler cache (https://parceljs.org/) 93 | .cache 94 | .parcel-cache 95 | 96 | # Next.js build output 97 | .next 98 | out 99 | 100 | # Nuxt.js build / generate output 101 | .nuxt 102 | dist 103 | 104 | # Gatsby files 105 | .cache/ 106 | # Comment in the public line in if your project uses Gatsby and not Next.js 107 | # https://nextjs.org/blog/next-9-1#public-directory-support 108 | # public 109 | 110 | # vuepress build output 111 | .vuepress/dist 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | src/routes/firebase/serviceAccountKey.json -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | WORKDIR /server 3 | COPY package*.json ./ 4 | RUN npm install 5 | COPY . . 6 | CMD [ "npm", "run","watch" ] -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # NoteHub Backend 2 | 3 | ## Project setup 4 | 5 | ``` 6 | npm install 7 | ``` 8 | 9 | ### Hot-reloads for development server 10 | 11 | ``` 12 | npm run watch 13 | ``` 14 | 15 | ### Start server 16 | 17 | ``` 18 | npm run start 19 | ``` 20 | 21 | ### Compiles and minifies for production 22 | 23 | Please refer to the README under NoteHub folder 24 | 25 | ### A database is required to configured in local environment: 26 | 27 | Update all your local database info in `.env` file at `NOTEHUB/server/.env`. 28 | 29 | **Note**: ".env" files in `src` and `models` folders are not intended to be changed in normal case. 30 | They are intended for testing purpose. If you need to test your database connection, you can change them as well. 31 | 32 | If the remote GCP database is enabled, you can uncomment that part of info and comment your info, then a remote database is connected. 33 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "./node_modules/.bin/eslint src/**/*.js --fix", 8 | "watch": "cross-env PORT=8000 nodemon ./src/server.js", 9 | "start": "cross-env PORT=8000 node ./src/server.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@hocuspocus/extension-rocksdb": "^1.0.0-alpha.79", 16 | "@hocuspocus/provider": "^1.0.0-alpha.21", 17 | "@hocuspocus/server": "^1.0.0-alpha.78", 18 | "@hocuspocus/transformer": "^1.0.0-alpha.15", 19 | "@tiptap/extension-document": "^2.0.0-beta.15", 20 | "@tiptap/extension-heading": "^2.0.0-beta.23", 21 | "@tiptap/extension-paragraph": "^2.0.0-beta.22", 22 | "@tiptap/extension-text": "^2.0.0-beta.15", 23 | "@tiptap/starter-kit": "^2.0.0-beta.140", 24 | "cookie-parser": "^1.4.5", 25 | "cors": "^2.8.5", 26 | "csurf": "^1.11.0", 27 | "debounce": "^1.2.1", 28 | "dotenv": "^10.0.0", 29 | "ejs": "^3.1.6", 30 | "express": "^4.17.1", 31 | "express-unless": "^1.0.0", 32 | "express-ws": "^5.0.2", 33 | "firebase": "^9.0.2", 34 | "firebase-admin": "^9.11.1", 35 | "mongodb": "^4.1.4", 36 | "morgan": "^1.10.0", 37 | "mysql2": "^2.3.0", 38 | "nodemon": "^2.0.15", 39 | "sequelize": "^6.6.5", 40 | "websocket": "^1.0.34", 41 | "yjs": "^13.5.20" 42 | }, 43 | "devDependencies": { 44 | "cross-env": "^7.0.3", 45 | "eslint": "^7.32.0", 46 | "eslint-plugin-vue": "^7.17.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/server/public/favicon.ico -------------------------------------------------------------------------------- /server/public/img/auth-background.9c92a012.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/server/public/img/auth-background.9c92a012.jpg -------------------------------------------------------------------------------- /server/public/img/search-background.3114fa6b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/server/public/img/search-background.3114fa6b.png -------------------------------------------------------------------------------- /server/public/index.html: -------------------------------------------------------------------------------- 1 | frontend
-------------------------------------------------------------------------------- /server/src/app.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const express = require("express"); 4 | 5 | const cors = require("cors"); 6 | 7 | const morgan = require("morgan"); 8 | 9 | const csrf = require("csurf"); 10 | 11 | const cookieParser = require("cookie-parser"); 12 | 13 | const admin = require("firebase-admin"); 14 | 15 | const apiRouter = require("./routes/api.router"); 16 | 17 | require("dotenv").config(); 18 | 19 | const connection = require("./models/database"); 20 | 21 | const app = express(); 22 | 23 | app.use( 24 | cors({ 25 | origin: "http://localhost:8080", 26 | }) 27 | ); 28 | 29 | // logging in Apache combined standard 30 | 31 | app.use(morgan("combined")); 32 | 33 | // recognize the incoming Request Object as a JSON Object. 34 | 35 | // parse requests of content-type - application/x-www-form-urlencoded 36 | 37 | app.use(express.json()); 38 | 39 | app.use(express.urlencoded({ extended: true })); 40 | 41 | app.use(cookieParser()); 42 | 43 | //const csrfMiddleware = csrf({ cookie: true }); 44 | 45 | //app.use(csrfMiddleware); 46 | 47 | // The build folder for storing static files generated by Vue 48 | 49 | app.use(express.static(path.join(__dirname, "..", "public"))); 50 | 51 | // Swagger: a API documentation tool 52 | 53 | app.all("*", (req, res, next) => { 54 | //res.cookie("XSRF-TOKEN", req.csrfToken()); 55 | 56 | next(); 57 | }); 58 | 59 | //routes to api 60 | 61 | app.use("/api", apiRouter); 62 | 63 | app.get("/*", (req, res, next) => { 64 | if ( 65 | req.url === "/test123" || 66 | req.url.match(/^\/websocket\/.+/) !== null || 67 | req.upgrade === true 68 | ) { 69 | return next(); 70 | } 71 | res.sendFile(path.join(__dirname, "..", "public", "index.html")); 72 | }); 73 | 74 | module.exports = { app }; 75 | -------------------------------------------------------------------------------- /server/src/hocuspocus-hooks/hooks.js: -------------------------------------------------------------------------------- 1 | /* 2 | A list of hooks provided by hocuspocus 3 | See details at HocusPocus websote 4 | */ 5 | const { Document } = require("@tiptap/extension-document"); 6 | const { Paragraph } = require("@tiptap/extension-paragraph"); 7 | const { Text } = require("@tiptap/extension-text"); 8 | const { Heading } = require("@tiptap/extension-heading"); 9 | const { CodeBlock } = require("@tiptap/extension-code-block"); 10 | const { Blockquote } = require("@tiptap/extension-blockquote"); 11 | 12 | const { selectUserByEmail } = require("../models/user.sql"); 13 | const { getUserInfoFromFirebase } = require("../routes/firebase/firebase.utils"); 14 | const { selectNoteAccessByNoteIdAndUserId } = require("../models/note.sql"); 15 | 16 | const fs = require("fs"); 17 | const path = require("path"); 18 | const mongo = require("../models/mongo.utils"); 19 | 20 | const { TiptapTransformer, ProsemirrorTransformer } = require("@hocuspocus/transformer"); 21 | 22 | var debounce = require("debounce"); 23 | const { ConnectionPoolClearedEvent } = require("mongodb"); 24 | 25 | let debounced; 26 | 27 | async function saveToMongo(data) { 28 | const prosemirrorJSON = TiptapTransformer.fromYdoc(data.document); 29 | // Save your document. In a real-world app this could be a database query 30 | // a webhook or something else 31 | 32 | const out = await mongo.notes.upsert(data.documentName, prosemirrorJSON); 33 | // Maybe you want to store the user who changed the document? 34 | // Guess what, you have access to your custom context from the 35 | // onConnect hook here. See authorization & authentication for more 36 | // details 37 | console.log(`Saving ${data.documentName} last changed by ${data.context.user.name}`); 38 | } 39 | 40 | const hooks = { 41 | async onConnect(data) { 42 | console.log("onConnect: A new connection has been established"); 43 | }, 44 | 45 | async onAuthenticate(data) { 46 | const { token } = data; 47 | console.log("onAuthenticate: Authentication requested"); 48 | 49 | //console.log(token) 50 | 51 | const userInfo = await getUserInfoFromFirebase(token); 52 | if (userInfo.email === "Invalid") { 53 | throw new Error("invalid email"); 54 | } 55 | 56 | const user = await selectUserByEmail(userInfo.email); 57 | 58 | if (user === null) { 59 | throw new Error("invalid user"); 60 | } 61 | 62 | const access = await selectNoteAccessByNoteIdAndUserId(data.documentName, user.userId); 63 | if (access === null) { 64 | throw new Error("You cannot access this note!"); 65 | } 66 | 67 | // If the user is only a viewer 68 | if (access === "viewer") { 69 | data.connection.readOnly = true; 70 | } 71 | console.log("onAuthenticate: Authentication pass"); 72 | return { 73 | user: { 74 | id: user.userId, 75 | name: user.firstName, 76 | }, 77 | }; 78 | }, 79 | 80 | async onLoadDocument(data) { 81 | console.log(`onLoadDocument: "${data.context.user.name}"`); 82 | // Print current working directory: 83 | 84 | // The tiptap collaboration extension uses shared types of a single y-doc 85 | // to store different fields in the same document. 86 | // The default field in tiptap is simply called 'default' 87 | const fieldName = "default"; 88 | 89 | // Check if the given field already exists in the given y-doc. 90 | // Important: Only import a document if it doesn't exist in the primary data storage! 91 | if (!data.document.isEmpty(fieldName)) { 92 | console.log("Document is already in primary dbfield: default, no need to load again"); 93 | return; 94 | } 95 | 96 | const rawJson = await mongo.notes.findOne(data.documentName); 97 | 98 | defaultJsonPath = path.join(__dirname, "startTemplate.json"); 99 | console.log("onLoadDocument finished"); 100 | const fileToLoad = 101 | rawJson && rawJson.default 102 | ? rawJson.default 103 | : JSON.parse(fs.readFileSync(defaultJsonPath, "utf8")); 104 | // Convert the editor format to a y-doc. The TiptapTransformer requires you to pass the list 105 | // of extensions you use in the frontend to create a valid document 106 | return TiptapTransformer.toYdoc(fileToLoad, fieldName, [Document, Paragraph, Text, Heading]); 107 | }, 108 | 109 | async onDisconnect(data) { 110 | // Output some information 111 | console.log(` Websocket disconnection`); 112 | console.log(`"${data.context.user.name}" has disconnected.`); 113 | 114 | // Save the document 115 | saveToMongo(data); 116 | }, 117 | 118 | async onChange(data) { 119 | // return null if debounced is undefined 120 | debounced?.clear(); 121 | debounced = debounce(() => { 122 | console.log(`onChange: ${data.documentName}`); 123 | const prosemirrorJSON = TiptapTransformer.fromYdoc(data.document); 124 | 125 | const size = new TextEncoder().encode(JSON.stringify(prosemirrorJSON)).length; 126 | const kiloBytes = size / 1024; 127 | console.log(`JSON size in KB: ${kiloBytes}`); 128 | saveToMongo(data); 129 | }, 500); 130 | debounced(); 131 | }, 132 | }; 133 | 134 | module.exports = hooks; 135 | -------------------------------------------------------------------------------- /server/src/hocuspocus-hooks/startTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "doc", 3 | "content": [ 4 | { 5 | "type": "heading", 6 | "attrs": { 7 | "textAlign": "left", 8 | "level": 2 9 | }, 10 | "content": [ 11 | { 12 | "type": "text", 13 | "text": "Start your learning journey ~ :)" 14 | } 15 | ] 16 | }, 17 | { 18 | "type": "paragraph", 19 | "attrs": { 20 | "textAlign": "left" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /server/src/models/database.js: -------------------------------------------------------------------------------- 1 | // Database setup for mysql2 2 | const { Sequelize } = require("sequelize"); 3 | require("dotenv").config(); 4 | 5 | const sequelize = new Sequelize( 6 | process.env.DB_DATABASE ? process.env.DB_DATABASE : "mynotehub_db", 7 | process.env.DB_USER ? process.env.DB_USER : "mynotehub_root", 8 | process.env.DB_PASS ? process.env.DB_PASS : "Notehub123123", 9 | { 10 | host: process.env.DB_HOST ? process.env.DB_HOST : "127.0.0.1", 11 | dialect: "mysql", 12 | } 13 | ); 14 | module.exports = sequelize; 15 | /*This function is used to test your connection to the database*/ 16 | // async function test() { 17 | // try { 18 | // await sequelize.authenticate(); 19 | // console.log("Connection has been established successfully."); 20 | // } catch (error) { 21 | // console.error("Unable to connect to the database:", error); 22 | // } 23 | // } 24 | // test(); 25 | -------------------------------------------------------------------------------- /server/src/models/membership.sql.js: -------------------------------------------------------------------------------- 1 | const sequelize = require("./database"); 2 | const { QueryTypes } = require("sequelize"); 3 | 4 | //CREATE Membership 5 | async function insertMembership(info) { 6 | let insertTuples = []; 7 | info.forEach((item) => { 8 | insertTuples.push(`(${item.communityId}, '${item.userId}', '${item.role}')`); 9 | }); 10 | const insertString = insertTuples.join(","); 11 | try { 12 | console.log(`INSERT IGNORE INTO Membership(communityId, userId, role) VALUES ${insertString}`); 13 | await sequelize.query( 14 | `INSERT IGNORE INTO Membership(communityId, userId, role) VALUES ${insertString}`, 15 | { 16 | type: QueryTypes.INSERT, 17 | } 18 | ); 19 | 20 | return `Membership is successfully inserted`; 21 | } catch (e) { 22 | console.error(e); 23 | throw new Error(e.message); 24 | } 25 | } 26 | 27 | //DELETE membership 28 | async function deleteMembership(info) { 29 | try { 30 | await sequelize.query( 31 | `DELETE FROM Membership WHERE userId = '${info.userId}' AND communityId = '${info.communityId}'`, 32 | { 33 | type: QueryTypes.DELETE, 34 | } 35 | ); 36 | 37 | console.log(`${info.communityId} with user ${info.userId} membership is successfully deleted`); 38 | } catch (e) { 39 | throw new Error(e.message); 40 | } 41 | } 42 | 43 | async function selectMembersByCommunityId(commId) { 44 | try { 45 | const data = await sequelize.query( 46 | `SELECT DISTINCT * FROM User NATURAL JOIN Membership WHERE communityId = '${commId}'`, 47 | { type: QueryTypes.SELECT } 48 | ); 49 | const output = [ 50 | { 51 | role: "Owner", 52 | icon: "psychology", 53 | users: data.filter(({ role }) => role === "owner"), 54 | }, 55 | { 56 | role: "Manager", 57 | icon: "manage_accounts", 58 | users: data.filter(({ role }) => role === "manager"), 59 | }, 60 | { 61 | role: "Member", 62 | icon: "groups", 63 | users: data.filter(({ role }) => role === "member"), 64 | }, 65 | ]; 66 | return output; 67 | } catch (e) { 68 | throw new Error(e.message); 69 | } 70 | } 71 | 72 | async function alterMembershipRole(info) { 73 | try { 74 | await sequelize.query( 75 | `UPDATE Membership 76 | SET role = '${info.role}' 77 | WHERE communityId = ${info.communityId}, userId = '${info.userId}'`, 78 | { 79 | type: QueryTypes.UPDATE, 80 | } 81 | ); 82 | } catch (e) { 83 | throw new Error(e.message); 84 | } 85 | } 86 | 87 | module.exports = { 88 | insertMembership, 89 | selectMembersByCommunityId, 90 | deleteMembership, 91 | alterMembershipRole, 92 | }; 93 | -------------------------------------------------------------------------------- /server/src/models/mongo.utils.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require("mongodb"); 2 | const Note = require("./note.repository"); 3 | 4 | 5 | const url = 6 | "mongodb+srv://notehubRoot:notehub123123@cluster0.n6o3n.mongodb.net/notehub?retryWrites=true&w=majority"; 7 | class MongoBot { 8 | constructor() { 9 | this.client = new MongoClient(url, { 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true, 12 | }); 13 | } 14 | async init() { 15 | await this.client.connect(); 16 | console.log("mongodb connected"); 17 | 18 | this.db = this.client.db("notehub"); 19 | this.notes = new Note(this.db); 20 | //return all data in the collections 21 | 22 | //console.log(await this.db.collection("notes").find({})); 23 | } 24 | } 25 | 26 | module.exports = new MongoBot(); 27 | -------------------------------------------------------------------------------- /server/src/models/note.repository.js: -------------------------------------------------------------------------------- 1 | class Note { 2 | constructor(db) { 3 | this.collection = db.collection("notes"); 4 | } 5 | 6 | async upsert(noteId, note) { 7 | const result = await this.collection.updateOne( 8 | { _id: noteId }, 9 | { $set: note }, 10 | { upsert: true } 11 | ); 12 | return result; 13 | } 14 | 15 | async findOne(noteId) { 16 | const note = await this.collection.findOne({ _id: noteId }); 17 | return note; 18 | } 19 | } 20 | 21 | //You can only access methods of this class from mongo.utils. 22 | module.exports = Note; 23 | -------------------------------------------------------------------------------- /server/src/routes/api.router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const userRouter = require("./user/user.router"); 3 | const commRouter = require("./community/community.router"); 4 | const noteRouter = require("./note/note.router"); 5 | const { checkIfAuthenticated } = require("./firebase/firebase.middleware"); 6 | 7 | const apiRouter = express.Router(); 8 | 9 | //apiRouter.use(checkIfAuthenticated); 10 | apiRouter.use("/user", checkIfAuthenticated, userRouter); 11 | 12 | apiRouter.use("/community", checkIfAuthenticated, commRouter); 13 | 14 | apiRouter.use("/note", checkIfAuthenticated, noteRouter); 15 | module.exports = apiRouter; 16 | -------------------------------------------------------------------------------- /server/src/routes/community/community.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | insertCommunity, 3 | selectCommunitiesByUserId, 4 | selectCommunityByCommunityId, 5 | searchCommunityByName, 6 | updateCommunityByCommunityId, 7 | insertCommunityNote, 8 | top10NotesByCommunityId, 9 | } = require("../../models/community.sql"); 10 | 11 | const { 12 | insertMembership, 13 | selectMembersByCommunityId, 14 | deleteMembership, 15 | alterMembershipRole, 16 | } = require("../../models/membership.sql"); 17 | 18 | const { httpSelectNotesByCommunityId } = require("../note/note.controller"); 19 | 20 | async function httpInsertCommunity(req, res) { 21 | const newInfo = req.body; 22 | newInfo.ownerId = req.userId; 23 | newInfo.createdAt = Date.now(); 24 | 25 | if (!newInfo.name || !newInfo.description || !newInfo.photo) { 26 | return res.status(400).json({ error: "Missing info" }); 27 | } 28 | 29 | try { 30 | return res.status(201).json(await insertCommunity(newInfo)); 31 | } catch (e) { 32 | return res.status(400).json({ error: e.message }); 33 | } 34 | } 35 | 36 | async function httpSelectCommunitiesByUserId(req, res) { 37 | try { 38 | return res.status(200).json(await selectCommunitiesByUserId(req.userId)); 39 | } catch (e) { 40 | return res.status(400).json({ error: e.message }); 41 | } 42 | } 43 | 44 | async function httpSelectCommunityByCommunityId(req, res) { 45 | try { 46 | return res.status(200).json(await selectCommunityByCommunityId(req.body.communityId)); 47 | } catch (e) { 48 | return res.status(400).json({ error: e.message }); 49 | } 50 | } 51 | 52 | async function httpSearchCommunityByName(req, res) { 53 | try { 54 | return res.status(200).json(await searchCommunityByName(req.body.name)); 55 | } catch (e) { 56 | return res.status(400).json({ error: e.message }); 57 | } 58 | } 59 | 60 | async function httpUpdateCommunity(req, res) { 61 | const newComm = req.body; 62 | if (newComm.ownerId != req.userId) { 63 | return res.status(400).json({ 64 | error: "You are not the owner, you cannot edit the community info.", 65 | }); 66 | } 67 | 68 | try { 69 | return res.status(200).json(await updateCommunityByCommunityId(newComm)); 70 | } catch (e) { 71 | return res.status(400).json({ error: e.message }); 72 | } 73 | } 74 | 75 | /* Below is Community function */ 76 | async function httpInsertMembership(req, res) { 77 | const newInfo = req.body; 78 | try { 79 | return res.status(201).json(await insertMembership(newInfo)); 80 | } catch (e) { 81 | return res.status(400).json({ error: e.message }); 82 | } 83 | } 84 | 85 | async function httpSelectMembersByCommunityId(req, res) { 86 | try { 87 | return res.status(200).json(await selectMembersByCommunityId(req.body.communityId)); 88 | } catch (e) { 89 | return res.status(400).json({ error: e.message }); 90 | } 91 | } 92 | 93 | async function httpDeleteMembership(req, res) { 94 | try { 95 | return res.status(204).json(await deleteMembership(req.body)); 96 | } catch (e) { 97 | return res.status(400).json({ error: e.message }); 98 | } 99 | } 100 | async function httpAlterMembershipRole(req, res) { 101 | const info = req.body; 102 | try { 103 | return res.status(200).json(await alterMembershipRole(info)); 104 | } catch (e) { 105 | return res.status(400).json({ error: e.message }); 106 | } 107 | } 108 | 109 | async function httpInsertCommunityNote(req, res) { 110 | const newInfo = req.body; 111 | try { 112 | return res.status(200).json(await insertCommunityNote(newInfo)); 113 | } catch (e) { 114 | return res.status(400).json({ error: e.message }); 115 | } 116 | } 117 | 118 | async function httpGetTop10Notes(req, res) { 119 | try { 120 | return res.status(200).json(await top10NotesByCommunityId(req.body.communityId)); 121 | } catch (e) { 122 | return res.status(400).json({ error: e.message }); 123 | } 124 | } 125 | 126 | module.exports = { 127 | httpInsertCommunity, 128 | httpSelectCommunitiesByUserId, 129 | httpSelectCommunityByCommunityId, 130 | httpSearchCommunityByName, 131 | httpUpdateCommunity, 132 | httpSelectMembersByCommunityId, 133 | httpInsertMembership, 134 | httpDeleteMembership, 135 | httpAlterMembershipRole, 136 | httpInsertCommunityNote, 137 | httpGetTop10Notes, 138 | }; 139 | -------------------------------------------------------------------------------- /server/src/routes/community/community.router.js: -------------------------------------------------------------------------------- 1 | const { 2 | httpInsertCommunity, 3 | httpSelectCommunitiesByUserId, 4 | httpSelectCommunityByCommunityId, 5 | httpSearchCommunityByName, 6 | httpUpdateCommunity, 7 | httpSelectMembersByCommunityId, 8 | httpInsertMembership, 9 | httpDeleteMembership, 10 | httpAlterMembershipRole, 11 | httpInsertCommunityNote, 12 | httpGetTop10Notes, 13 | } = require("./community.controller"); 14 | 15 | const express = require("express"); 16 | 17 | const commRouter = express.Router(); 18 | 19 | commRouter.post("/insert-community", httpInsertCommunity); 20 | 21 | commRouter.get("/get-all-communities", httpSelectCommunitiesByUserId); 22 | 23 | commRouter.post( 24 | "/get-community-by-community-id", 25 | httpSelectCommunityByCommunityId 26 | ); 27 | 28 | commRouter.post("/search-community-by-name", httpSearchCommunityByName); 29 | 30 | commRouter.post("/update-community", httpUpdateCommunity); 31 | 32 | commRouter.post("/get-members-by-community-id", httpSelectMembersByCommunityId); 33 | 34 | commRouter.post("/insert-membership", httpInsertMembership); 35 | 36 | commRouter.post("/delete-membership", httpDeleteMembership); 37 | 38 | commRouter.post("/alter-membership", httpAlterMembershipRole); 39 | 40 | commRouter.post("/add-note-to-community", httpInsertCommunityNote); 41 | 42 | commRouter.post("/get-top-10-notes", httpGetTop10Notes); 43 | module.exports = commRouter; 44 | -------------------------------------------------------------------------------- /server/src/routes/firebase/.gitignore: -------------------------------------------------------------------------------- 1 | serviceAccount.json -------------------------------------------------------------------------------- /server/src/routes/firebase/firebase.initialize.js: -------------------------------------------------------------------------------- 1 | var admin = require("firebase-admin"); 2 | 3 | var serviceAccount = require("./serviceAccount.json"); 4 | 5 | admin.initializeApp({ 6 | credential: admin.credential.cert(serviceAccount), 7 | }); 8 | 9 | module.exports = admin; 10 | -------------------------------------------------------------------------------- /server/src/routes/firebase/firebase.middleware.js: -------------------------------------------------------------------------------- 1 | const admin = require("./firebase.initialize"); 2 | 3 | function getAuthToken(req, res, next) { 4 | if ( 5 | req.headers.authorization && 6 | req.headers.authorization.split(" ")[0] === "Bearer" 7 | ) { 8 | req.authToken = req.headers.authorization.split(" ")[1]; 9 | } else { 10 | req.authToken = null; 11 | } 12 | next(); 13 | } 14 | 15 | function checkIfAuthenticated(req, res, next) { 16 | getAuthToken(req, res, async () => { 17 | try { 18 | 19 | const { authToken } = req; 20 | const userInfo = await admin.auth().verifyIdToken(authToken); 21 | req.userId = userInfo.uid; 22 | req.email = userInfo.email; 23 | console.log(`\n${req.email}, ${req.userId} is being authenticated\n`); 24 | next(); 25 | } catch (e) { 26 | console.log(e.message); 27 | return res 28 | .status(401) 29 | .send({ error: "You are not authorized to make this request" }); 30 | } 31 | }); 32 | } 33 | 34 | // function checkIfCurrentUser(req, email) { 35 | // console.log("currentUser:", req.email); 36 | // console.log("requestUser:", email); 37 | // if (email != req.email) { 38 | // return false; 39 | // } 40 | // return true; 41 | // } 42 | 43 | module.exports = { checkIfAuthenticated }; 44 | -------------------------------------------------------------------------------- /server/src/routes/firebase/firebase.utils.js: -------------------------------------------------------------------------------- 1 | const admin = require("./firebase.initialize"); 2 | 3 | function getAuthTokenFromHeader(req) { 4 | if ( 5 | req.headers.authorization && 6 | req.headers.authorization.split(" ")[0] === "Bearer" 7 | ) { 8 | return req.headers.authorization.split(" ")[1]; 9 | } else { 10 | return null; 11 | } 12 | } 13 | 14 | const getUserInfoFromFirebase = async (authToken) => { 15 | try { 16 | return await admin.auth().verifyIdToken(authToken); 17 | } catch (error) { 18 | console.log(error); 19 | return { email: "Invalid" }; 20 | } 21 | }; 22 | 23 | module.exports = { getAuthTokenFromHeader, getUserInfoFromFirebase }; 24 | -------------------------------------------------------------------------------- /server/src/routes/note/note.router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { 3 | httpInsertNote, 4 | httpSelectUserNotes, 5 | httpTransferOwnership, 6 | httpSelectAllAccessorsByNoteId, 7 | httpSelectNoteByNoteId, 8 | httpAlterNoteCommunity, 9 | httpAlterNoteCategories, 10 | httpAlterNoteAccess, 11 | httpSelectNoteAccessByNoteIdAndUserId, 12 | httpSelectAllCategories, 13 | httpUpdateNoteByNoteId, 14 | httpSelectNotesByCommunityId, 15 | httpSelectCommentsByNoteId, 16 | httpInsertComment, 17 | httpLikeNote, 18 | httpUpdateCommentContent, 19 | } = require("./note.controller"); 20 | 21 | const noteRouter = express.Router(); 22 | 23 | noteRouter.post("/insert-note", httpInsertNote); 24 | 25 | noteRouter.get("/get-user-notes", httpSelectUserNotes); 26 | 27 | noteRouter.post("/transfer-note-ownership", httpTransferOwnership); 28 | 29 | noteRouter.post("/get-all-accessors", httpSelectAllAccessorsByNoteId); 30 | 31 | noteRouter.post("/get-note-by-noteId", httpSelectNoteByNoteId); 32 | 33 | noteRouter.post("/alter-note-community", httpAlterNoteCommunity); 34 | 35 | noteRouter.post("/alter-note-categories", httpAlterNoteCategories); 36 | 37 | noteRouter.post("/alter-note-access", httpAlterNoteAccess); 38 | 39 | noteRouter.post("/get-note-access-by-noteId-userId", httpSelectNoteAccessByNoteIdAndUserId); 40 | 41 | noteRouter.get("/get-all-categories", httpSelectAllCategories); 42 | 43 | noteRouter.post("/update-note", httpUpdateNoteByNoteId); 44 | 45 | noteRouter.post("/get-notes-by-communityId", httpSelectNotesByCommunityId); 46 | 47 | noteRouter.post("/get-comments-by-noteId", httpSelectCommentsByNoteId); 48 | 49 | noteRouter.post("/:noteId/insert-comment", httpInsertComment); 50 | 51 | noteRouter.post("/update-comment-content", httpUpdateCommentContent); 52 | 53 | noteRouter.get("/like-note/:userId/:noteId", httpLikeNote); 54 | 55 | module.exports = noteRouter; 56 | -------------------------------------------------------------------------------- /server/src/routes/user/user.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | selectAllUser, 3 | selectUserByEmail, 4 | insertUser, 5 | updateUserByEmail, 6 | deleteUserByEmail, 7 | selectNoteProvidersById, 8 | selectUserByuserId, 9 | searchUserByKeyword, 10 | selectTopUsers, 11 | getUserLevel, 12 | getStatistics 13 | } = require("../../models/user.sql"); 14 | 15 | const { checkIfAuthenticated } = require("../firebase/firebase.middleware"); 16 | 17 | // async function httpSelectAllUsers(req, res) { 18 | // return res.status(200).json(await selectAllUser()); 19 | // } 20 | 21 | async function httpSelectUserByToken(req, res) { 22 | if (!req.email) { 23 | return res.status(200).json({ error: "Missing email" }); 24 | } 25 | try { 26 | return res.status(200).json(await selectUserByEmail(req.email)); 27 | } catch (e) { 28 | return res.status(400).json({ error: e.message }); 29 | } 30 | } 31 | 32 | async function httpInsertUser(req, res) { 33 | const newUser = req.body; 34 | if (!newUser.firstName || !newUser.lastName || !newUser.email) { 35 | return res.status(400).json({ error: "Missing info" }); 36 | } 37 | if (!newUser.subtitle) { 38 | newUser.subtitle = "New user"; 39 | } 40 | newUser.userId = req.userId; 41 | try { 42 | return res.status(201).json(await insertUser(newUser)); 43 | } catch (e) { 44 | return res.status(400).json({ error: e.message }); 45 | } 46 | } 47 | 48 | async function httpUpdateUserByEmail(req, res) { 49 | const userInfo = req.body; 50 | userInfo.email = req.email; 51 | if (!userInfo.email) { 52 | return res.status(400).json({ error: "Missing email" }); 53 | } 54 | try { 55 | return res.status(201).json(await updateUserByEmail(userInfo)); 56 | } catch (e) { 57 | return res.status(400).json({ error: e.message }); 58 | } 59 | } 60 | 61 | async function httpDeleteUserByEmail(req, res) { 62 | /* TODO: Code here needs to check if this request is allowed!*/ 63 | 64 | const userInfo = req.body; 65 | userInfo.email = req.email; 66 | try { 67 | return res.status(204).json(await deleteUserByEmail(userInfo)); 68 | } catch (e) { 69 | return res.status(400).json({ error: e.message }); 70 | } 71 | ``; 72 | } 73 | 74 | async function httpSelectNoteProviders(req, res) { 75 | try { 76 | return res.status(200).json(await selectNoteProvidersById(req.userId)); 77 | } catch (e) { 78 | return res.status(400).json({ error: e.message }); 79 | } 80 | } 81 | 82 | async function httpSearchUserByKeyword(req, res) { 83 | try { 84 | return res.status(200).json(await searchUserByKeyword(req.body.keyword)); 85 | } catch (e) { 86 | return res.status(400).json({ error: e.message }); 87 | } 88 | } 89 | 90 | async function httpSelectTopUsers(req, res) { 91 | try { 92 | return res.status(200).json(await selectTopUsers()); 93 | } catch (e) { 94 | return res.status(400).json({ error: e.message }); 95 | } 96 | } 97 | 98 | async function httpGetUserLevel(req, res) { 99 | try { 100 | return res.status(200).json(await getUserLevel(req.params.userId)); 101 | } catch (e) { 102 | return res.status(400).json({ error: e.message }); 103 | } 104 | } 105 | 106 | async function httpGetNoteHubStats(req, res) { 107 | try { 108 | return res.status(200).json(await getStatistics()); 109 | } catch (e) { 110 | return res.status(400).json({ error: e.message }); 111 | } 112 | } 113 | 114 | module.exports = { 115 | httpInsertUser, 116 | httpUpdateUserByEmail, 117 | httpSelectUserByToken, 118 | httpDeleteUserByEmail, 119 | httpSelectNoteProviders, 120 | httpSearchUserByKeyword, 121 | httpSelectTopUsers, 122 | httpGetUserLevel, 123 | httpGetNoteHubStats, 124 | }; 125 | -------------------------------------------------------------------------------- /server/src/routes/user/user.router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { 3 | httpInsertUser, 4 | httpUpdateUserByEmail, 5 | httpSelectUserByToken, 6 | httpDeleteUserByEmail, 7 | httpSelectNoteProviders, 8 | httpSearchUserByKeyword, 9 | httpSelectTopUsers, 10 | httpGetUserLevel, 11 | httpGetNoteHubStats, 12 | } = require("./user.controller"); 13 | 14 | const userRouter = express.Router(); 15 | 16 | userRouter.post("/insert-user", httpInsertUser); 17 | 18 | userRouter.put("/update-user", httpUpdateUserByEmail); 19 | 20 | userRouter.get("/get-user-by-token", httpSelectUserByToken); 21 | 22 | userRouter.delete("/delete-user-by-email", httpDeleteUserByEmail); 23 | 24 | //userRouter.get("/get-all-users", httpSelectAllUsers); 25 | 26 | userRouter.get("/get-note-providers", httpSelectNoteProviders); 27 | 28 | userRouter.post("/search-user-by-keyword", httpSearchUserByKeyword); 29 | 30 | userRouter.get("/get-top-10-users", httpSelectTopUsers); 31 | 32 | userRouter.get("/:userId/get-level", httpGetUserLevel); 33 | 34 | userRouter.get("/get-notehub-statistics", httpGetNoteHubStats); 35 | 36 | module.exports = userRouter; 37 | -------------------------------------------------------------------------------- /server/src/server.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const expressWebsockets = require("express-ws"); 3 | const { Server } = require("@hocuspocus/server"); 4 | const hooks = require("./hocuspocus-hooks/hooks"); 5 | const { app: expressServer } = require("./app"); 6 | const { RocksDB } = require("@hocuspocus/extension-rocksdb"); 7 | const mongo = require("./models/mongo.utils"); 8 | const PORT = process.env.PORT || 8000; 9 | const https = require("https"); 10 | const fs = require("fs"); 11 | const path = require("path"); 12 | // Configure hocuspocus 13 | const server = Server.configure({ 14 | //port: 1234, 15 | ...hooks, 16 | extensions: [ 17 | new RocksDB({ 18 | path: "./database", 19 | options: { 20 | // This option is only a example. See here for a full list: 21 | // https://www.npmjs.com/package/leveldown#options 22 | createIfMissing: true, 23 | }, 24 | }), 25 | ], 26 | // Move all hooks to the hooks.js file 27 | // async onConnect(data) { 28 | // ... 29 | // } 30 | }); 31 | 32 | // Initialize a HTTPS server 33 | 34 | if (process.env.ON_SERVER === "true") { 35 | var options = { 36 | key: fs.readFileSync(path.join(__dirname, "..", "private.key")), 37 | cert: fs.readFileSync(path.join(__dirname, "..", "key.crt")), 38 | }; 39 | var httpsServer = https.createServer(options, expressServer); 40 | httpsServer.listen(443); 41 | var { app } = expressWebsockets(expressServer, httpsServer); 42 | } else { 43 | var { app } = expressWebsockets(expressServer); 44 | } 45 | app.get("/test123", (request, response) => { 46 | response.send("Hello World!"); 47 | }); 48 | 49 | app.ws("/websocket/note/:noteId", (websocket, request) => { 50 | console.log("requesting upgrade to websocket"); 51 | server.handleConnection(websocket, request, request.params.noteId, {}); 52 | }); 53 | 54 | const start = async () => { 55 | await mongo.init(); 56 | app.listen(PORT, () => { 57 | console.log(`Listening to ${PORT}`); 58 | }); 59 | }; 60 | 61 | start(); 62 | -------------------------------------------------------------------------------- /server/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "doc", 3 | "content": [ 4 | { 5 | "type": "heading", 6 | "attrs": { 7 | "textAlign": "left", 8 | "level": 2 9 | }, 10 | "content": [ 11 | { 12 | "type": "text", 13 | "text": "Test Success" 14 | } 15 | ] 16 | }, 17 | { 18 | "type": "paragraph", 19 | "attrs": { 20 | "textAlign": "left" 21 | } 22 | } 23 | ] 24 | } 25 | --------------------------------------------------------------------------------