├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cshub-client ├── .browserslistrc ├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── README.md ├── babel.config.js ├── package.json ├── postcss.config.js ├── public │ ├── assets │ │ ├── Sailec-Light.otf │ │ ├── github-markdown.min.css │ │ ├── logo.jpg │ │ ├── mathquill.min.css │ │ └── mathquill.min.js │ ├── img │ │ ├── defaultAvatar.png │ │ └── icons │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-192x192.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-512x512.png │ │ │ └── msapplication-icon-144x144.png │ ├── index.html │ └── manifest.json ├── src │ ├── App.vue │ ├── components │ │ ├── admin │ │ │ ├── EmailDomainTable.vue │ │ │ ├── StudyTable.vue │ │ │ ├── TopicView.vue │ │ │ └── UserTable.vue │ │ ├── global │ │ │ ├── NavDrawer.vue │ │ │ ├── NavDrawerItem.vue │ │ │ ├── NotificationDialog.vue │ │ │ └── Toolbar.vue │ │ ├── posts │ │ │ ├── Examples.vue │ │ │ ├── Post.vue │ │ │ ├── PostEditsDialog.vue │ │ │ ├── PostList.vue │ │ │ ├── PostPagination.vue │ │ │ └── PostSaveEditDialog.vue │ │ ├── practice │ │ │ ├── DynamicQuestionUtils.ts │ │ │ ├── EditQuestionListItem.vue │ │ │ ├── Practice.vue │ │ │ ├── QuestionList.vue │ │ │ ├── QuestionListItemMixin.ts │ │ │ ├── ReviewQuestionListItem.vue │ │ │ ├── editors │ │ │ │ ├── DynamicEditor.vue │ │ │ │ ├── EditorAccordion.vue │ │ │ │ ├── Editors.vue │ │ │ │ ├── MultipleChoiceEditor.vue │ │ │ │ ├── OpenNumberEditor.vue │ │ │ │ └── OpenTextEditor.vue │ │ │ ├── question │ │ │ │ ├── CurrentPracticeQuestion.vue │ │ │ │ ├── DNQuestionMixin.ts │ │ │ │ ├── MCQuestionMixin.ts │ │ │ │ ├── ONQuestionMixin.ts │ │ │ │ ├── OTQuestionMixin.ts │ │ │ │ ├── PracticeQuestion.vue │ │ │ │ ├── QuestionMixin.ts │ │ │ │ └── SCQuestionMixin.ts │ │ │ └── viewers │ │ │ │ ├── DynamicViewer.vue │ │ │ │ ├── MultipleChoiceViewer.vue │ │ │ │ ├── OpenNumberViewer.vue │ │ │ │ ├── OpenTextViewer.vue │ │ │ │ └── ViewerMixin.ts │ │ └── quill │ │ │ ├── CustomTooltip.ts │ │ │ ├── IQuillEditSetup.ts │ │ │ └── Quill.vue │ ├── config.sh │ ├── index.d.ts │ ├── main.ts │ ├── plugins │ │ ├── animatecss.ts │ │ ├── codemirror.ts │ │ ├── index.ts │ │ ├── quill │ │ │ ├── ImageResize.min.js │ │ │ └── mathquill4quill.min.js │ │ ├── socket.io.ts │ │ └── vuetify │ │ │ └── vuetify.ts │ ├── registerServiceWorker.ts │ ├── store │ │ ├── index.ts │ │ ├── localStorageData.ts │ │ ├── state │ │ │ ├── dataState.ts │ │ │ ├── practiceState.ts │ │ │ ├── uiState.ts │ │ │ └── userState.ts │ │ └── store.ts │ ├── styleOverwrites.css │ ├── styling │ │ └── vars.scss │ ├── typings │ │ ├── shims-tsx.d.ts │ │ └── shims-vue.d.ts │ ├── utilities │ │ ├── EventBus.ts │ │ ├── Topics.ts │ │ ├── api-wrapper.ts │ │ ├── cache-types.ts │ │ ├── codemirror-colorize.ts │ │ ├── debugConsole.ts │ │ ├── id-generator.ts │ │ ├── index.ts │ │ ├── pipes.ts │ │ ├── socket-wrapper.ts │ │ └── validation.ts │ └── views │ │ ├── UnsavedQuestions.vue │ │ ├── posts │ │ ├── PostCreate.vue │ │ ├── PostView.vue │ │ └── PostsSearch.vue │ │ ├── router │ │ ├── guards │ │ │ ├── adminDashboardGuard.ts │ │ │ ├── onlyIfNotLoggedInGuard.ts │ │ │ ├── setupRequiredDataGuard.ts │ │ │ └── userDashboardGuard.ts │ │ └── router.ts │ │ └── user │ │ ├── AdminDashboard.vue │ │ ├── CreateUserAccount.vue │ │ ├── ForgotPasswordComp.vue │ │ ├── LoginScreen.vue │ │ ├── UnsavedPosts.vue │ │ ├── UserDashboard.vue │ │ └── WIPPostsView.vue ├── tsconfig.json ├── vue.config.js └── yarn.lock ├── cshub-server ├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── package.json ├── src │ ├── SettingsBaseline.ts │ ├── auth │ │ ├── AuthMiddleware.ts │ │ ├── HashPassword.ts │ │ ├── JWTHandler.ts │ │ └── validateRights │ │ │ └── PostAccess.ts │ ├── db │ │ ├── database-query.ts │ │ ├── entities │ │ │ ├── cacheversion.ts │ │ │ ├── edit.ts │ │ │ ├── emaildomain.ts │ │ │ ├── post.ts │ │ │ ├── practice │ │ │ │ ├── answer.ts │ │ │ │ ├── closed-answer.ts │ │ │ │ ├── dynamic-answer.ts │ │ │ │ ├── open-number-answer.ts │ │ │ │ ├── open-text-answer.ts │ │ │ │ ├── question.ts │ │ │ │ └── variable.ts │ │ │ ├── study.ts │ │ │ ├── topic.ts │ │ │ └── user.ts │ │ └── orm-connection.ts │ ├── endpoints │ │ ├── PreRender.ts │ │ ├── Search.ts │ │ ├── emaildomains.ts │ │ ├── index.ts │ │ ├── post │ │ │ ├── EditContent.ts │ │ │ ├── EditPost.ts │ │ │ ├── EditsHandler.ts │ │ │ ├── PostContent.ts │ │ │ ├── PostData.ts │ │ │ ├── PostSettings.ts │ │ │ ├── SquashEdits.ts │ │ │ ├── SubmitPost.ts │ │ │ ├── assets │ │ │ │ ├── katex.min.js │ │ │ │ └── quill.min.js │ │ │ └── index.ts │ │ ├── posts │ │ │ ├── ExamplePosts.ts │ │ │ ├── GetPosts.ts │ │ │ ├── GetUnverifiedPosts.ts │ │ │ ├── TopicPosts.ts │ │ │ ├── WIPPosts.ts │ │ │ └── index.ts │ │ ├── question │ │ │ ├── CheckAnswers.ts │ │ │ ├── EditQuestion.ts │ │ │ ├── GetQuestion.ts │ │ │ ├── GetQuestions.ts │ │ │ ├── QuestionSettings.ts │ │ │ ├── QuestionUtils.ts │ │ │ └── index.ts │ │ ├── study │ │ │ └── Studies.ts │ │ ├── topics │ │ │ ├── EditTopics.ts │ │ │ ├── Topics.ts │ │ │ └── index.ts │ │ ├── user │ │ │ ├── AllUsers.ts │ │ │ ├── ChangeAvatar.ts │ │ │ ├── ChangePassword.ts │ │ │ ├── CreateAccount.ts │ │ │ ├── ForgotPassword.ts │ │ │ ├── ForgotPasswordMail.ts │ │ │ ├── Login.ts │ │ │ ├── Profile.ts │ │ │ ├── UserAdminPage.ts │ │ │ ├── VerifyMail.ts │ │ │ ├── VerifyToken.ts │ │ │ ├── defaultAvatar.png │ │ │ └── index.ts │ │ └── utils.ts │ ├── index.ts │ ├── init.ts │ ├── realtime-edit │ │ ├── CursorList.ts │ │ ├── CursorUpdatedHandler.ts │ │ ├── DataList.ts │ │ ├── DataUpdatedHandler.ts │ │ ├── index.ts │ │ └── socket-receiver.ts │ └── utilities │ │ ├── CORSMiddleware.ts │ │ ├── Logger.ts │ │ ├── LoggingMiddleware.ts │ │ ├── MailConnection.ts │ │ ├── StringUtils.ts │ │ ├── TopicsUtils.ts │ │ ├── VersionMiddleware.ts │ │ ├── mailTemplate.html │ │ └── query-parser.ts ├── tsconfig.json └── yarn.lock ├── cshub-shared ├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── package.json ├── src │ ├── Routes.ts │ ├── api-calls │ │ ├── Requests.ts │ │ ├── SocketRequests.ts │ │ ├── endpoints │ │ │ ├── Search.ts │ │ │ ├── emaildomains.ts │ │ │ ├── index.ts │ │ │ ├── post │ │ │ │ ├── EditContent.ts │ │ │ │ ├── EditPost.ts │ │ │ │ ├── PostContent.ts │ │ │ │ ├── PostData.ts │ │ │ │ ├── PostSettings.ts │ │ │ │ ├── SquashEdits.ts │ │ │ │ ├── SubmitPost.ts │ │ │ │ └── index.ts │ │ │ ├── posts │ │ │ │ ├── ExamplePosts.ts │ │ │ │ ├── GetUnverifiedPosts.ts │ │ │ │ ├── TopicPosts.ts │ │ │ │ ├── WIPPosts.ts │ │ │ │ └── index.ts │ │ │ ├── question │ │ │ │ ├── CheckAnswers.ts │ │ │ │ ├── EditQuestion.ts │ │ │ │ ├── GetQuestion.ts │ │ │ │ ├── GetQuestions.ts │ │ │ │ ├── QuestionSettings.ts │ │ │ │ ├── index.ts │ │ │ │ └── models │ │ │ │ │ ├── CheckAnswer.ts │ │ │ │ │ ├── FullQuestion.ts │ │ │ │ │ ├── PracticeQuestion.ts │ │ │ │ │ └── Variable.ts │ │ │ ├── study │ │ │ │ ├── CreateStudies.ts │ │ │ │ ├── HideStudies.ts │ │ │ │ ├── RenameStudies.ts │ │ │ │ ├── Studies.ts │ │ │ │ └── index.ts │ │ │ ├── topics │ │ │ │ ├── EditTopics.ts │ │ │ │ ├── Topics.ts │ │ │ │ └── index.ts │ │ │ └── user │ │ │ │ ├── AllUsers.ts │ │ │ │ ├── ChangeAvatar.ts │ │ │ │ ├── ChangePassword.ts │ │ │ │ ├── CreateAccount.ts │ │ │ │ ├── ForgotPassword.ts │ │ │ │ ├── ForgotPasswordMail.ts │ │ │ │ ├── Login.ts │ │ │ │ ├── UserAdminPage.ts │ │ │ │ ├── VerifyToken.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── realtime-edit │ │ │ ├── ClientCursorUpdated.ts │ │ │ ├── ClientDataUpdated.ts │ │ │ ├── IRealtimeEdit.ts │ │ │ ├── IRealtimeSelect.ts │ │ │ ├── ServerCursorUpdated.ts │ │ │ ├── ServerDataUpdated.ts │ │ │ ├── TogglePostJoin.ts │ │ │ └── index.ts │ ├── entities │ │ ├── edit.ts │ │ ├── emaildomains.ts │ │ ├── post.ts │ │ ├── question.ts │ │ ├── study.ts │ │ ├── topic.ts │ │ └── user.ts │ ├── models │ │ ├── IApiRequest.ts │ │ ├── IJWTToken.ts │ │ ├── ISocketRequest.ts │ │ ├── ServerError.ts │ │ └── index.ts │ ├── shim.d.ts │ └── utilities │ │ ├── DeltaHandler.ts │ │ ├── DynamicQuestionUtils.ts │ │ ├── EditsHandler.ts │ │ ├── MarkdownLatexQuill.ts │ │ ├── QuillDefaultOptions.ts │ │ └── Random.ts ├── tsconfig.json └── yarn.lock ├── deployment ├── application │ └── docker-compose.yml ├── build.sh ├── dev │ ├── .env_example │ ├── docker-compose.yml │ ├── nginx_example.conf │ └── settings_example.env ├── logging │ ├── .env_example │ ├── docker-compose.yml │ ├── fluentd │ │ └── conf │ │ │ └── fluent.conf │ └── kibana.yml ├── prod │ ├── .env_example │ ├── docker-compose.yml │ ├── nginx_example.conf │ └── settings_example.env ├── run.sh ├── traefik │ ├── .env_example │ ├── docker-compose.yml │ └── traefik.toml └── watchtower │ ├── .env_example │ └── docker-compose.yml └── utilities ├── fixpermissions.sh ├── intellij-runconfigs ├── NodeJS_Debug.xml ├── Vue_CLI_Debug.xml └── Vue_CLI_Server.xml ├── queries └── QUERIES.md └── version.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: File a bug report to improve this platform 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. Pictures of the console log (if it gives an error) would be great. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ Files 2 | .idea 3 | *.iml 4 | settings.env 5 | .env 6 | .htpasswd 7 | nginx.conf 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | 5 | git: 6 | depth: 1 7 | 8 | services: 9 | - docker 10 | 11 | stages: 12 | - shared 13 | - others 14 | 15 | before_script: 16 | - node ./utilities/version.js 17 | 18 | matrix: 19 | include: 20 | - name: "shared-push" 21 | stage: shared 22 | if: type = push 23 | script: 24 | - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" 25 | - docker build -t $DOCKER_USERNAME/shared:$TRAVIS_BRANCH cshub-shared 26 | after_success: 27 | - docker push $DOCKER_USERNAME/shared:$TRAVIS_BRANCH 28 | 29 | - name: "client-push" 30 | if: type = push 31 | stage: others 32 | script: 33 | - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" 34 | - docker build --build-arg BASE_IMAGE=$DOCKER_USERNAME/shared:$TRAVIS_BRANCH -t $DOCKER_USERNAME/client:$TRAVIS_BRANCH cshub-client 35 | after_success: 36 | - docker push $DOCKER_USERNAME/client:$TRAVIS_BRANCH 37 | 38 | - name: "server-push" 39 | if: type = push 40 | stage: others 41 | script: 42 | - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" 43 | - docker build --build-arg BASE_IMAGE=$DOCKER_USERNAME/shared:$TRAVIS_BRANCH -t $DOCKER_USERNAME/server:$TRAVIS_BRANCH cshub-server 44 | after_success: 45 | - docker push $DOCKER_USERNAME/server:$TRAVIS_BRANCH 46 | 47 | - name: "client-pull" 48 | if: type = pull_request 49 | stage: others 50 | script: 51 | - docker build -t shared-$TRAVIS_PULL_REQUEST_SHA cshub-shared 52 | - docker build --build-arg BASE_IMAGE=shared-$TRAVIS_PULL_REQUEST_SHA cshub-client 53 | 54 | - name: "server-pull" 55 | if: type = pull_request 56 | stage: others 57 | script: 58 | - docker build -t shared-$TRAVIS_PULL_REQUEST_SHA cshub-shared 59 | - docker build --build-arg BASE_IMAGE=shared-$TRAVIS_PULL_REQUEST_SHA cshub-server 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Robbin Baauw 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 | # CSHub [![Build Status](https://travis-ci.com/finitum/CSHub.svg?branch=dev)](https://travis-ci.com/finitum/CSHub) 2 | This is a project made for the TU Delft CSE studies, where everyone can post information, summaries and more, for everyone to see and read. 3 | ## Running the project 4 | * Install [NodeJS](https://nodejs.org/en/), and install the dependencies of the server and client projects (`npm install` in the correct directories) 5 | * For the server, run `ts-node`, or use the provided IntelliJ runconfigs 6 | * For the client, run `yarn run serve` :) 7 | 8 | ## Feature Requests / Bug Reports 9 | 10 | Create a GitHub issue on this repository, and we will try to take a look at it. 11 | 12 | ## How does it work? 13 | This project uses TypeScript. This is typed JavaScript, so we can use many advantages of typedness, and use object oriented programming constructs like classes. 14 | 15 | The project is divided into different sections: 16 | 17 | * Client 18 | * Server 19 | * Shared 20 | 21 | In order to facilitate typed communication between server and client, we have a shared package. Here, many models of different parts of the app are defined, so we are sure an object has the properties we need. 22 | It also defines how the api-calls function, as it provides classes that can be passed between server and client. These classes include a certain URL, which both the server and client use for their communication. 23 | This makes sure that the client always calls a URL on the server which the server can handle. 24 | 25 | For the api we make use of express, which gives us a request object we can work with. For the front-end, Vue.JS is used, though also with TypeScript, which is not the default. See the comments in the corresponding files for more information. 26 | -------------------------------------------------------------------------------- /cshub-client/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions 2 | last 2 Firefox versions -------------------------------------------------------------------------------- /cshub-client/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /cshub-client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/recommended", "@vue/prettier", "@vue/typescript"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", 10 | "@typescript-eslint/explicit-function-return-type": "off", 11 | "@typescript-eslint/no-use-before-define": "off", 12 | "@typescript-eslint/interface-name-prefix": "off", 13 | "prettier/prettier": [ 14 | "error", 15 | { 16 | semi: true, 17 | tabWidth: 4, 18 | printWidth: 120 19 | } 20 | ], 21 | "no-dupe-class-members": "off" 22 | }, 23 | parserOptions: { 24 | parser: "@typescript-eslint/parser" 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /cshub-client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | *.local 6 | 7 | # Log files 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | *.sw* 20 | -------------------------------------------------------------------------------- /cshub-client/Dockerfile: -------------------------------------------------------------------------------- 1 | # Available at cshubnl/shared 2 | ARG BASE_IMAGE=cshub-shared 3 | 4 | # build stage 5 | FROM $BASE_IMAGE as build-client 6 | 7 | # Set work dir 8 | WORKDIR /app/cshub-client 9 | 10 | # Copy package files 11 | COPY package.json ./ 12 | COPY yarn.lock ./ 13 | 14 | # Install python as yarn build dependency 15 | RUN apk add python make g++ --no-cache 16 | 17 | # Get dependencies 18 | RUN yarn install 19 | 20 | # Copy source 21 | COPY . . 22 | 23 | # Build source 24 | RUN yarn build 25 | 26 | # production stage 27 | FROM nginx:1.15-alpine as production-stage 28 | 29 | # Add curl for health check 30 | RUN apk add curl --no-cache 31 | 32 | # Copy over build files 33 | WORKDIR /usr/share/nginx/html 34 | COPY --from=build-client /app/cshub-client/dist . 35 | COPY --from=build-client /app/cshub-client/src/config.sh . 36 | 37 | # Make the config.js from env generator executable 38 | RUN ["chmod", "+x", "./config.sh"] 39 | 40 | # Expose port 80 for nginx 41 | EXPOSE 80 42 | 43 | # Curl localhost to check if healthy 44 | HEALTHCHECK CMD curl --fail http://localhost:80/ -A "dontgothroughprerenderplease" || exit 1 45 | 46 | # Runs nginx and generates config from env vars 47 | CMD ["/usr/share/nginx/html/config.sh"] 48 | 49 | -------------------------------------------------------------------------------- /cshub-client/README.md: -------------------------------------------------------------------------------- 1 | # cshub-client 2 | 3 | ## Project setup 4 | ``` 5 | yarn 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | yarn run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | yarn run lint 26 | ``` 27 | -------------------------------------------------------------------------------- /cshub-client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@vue/app", 5 | { 6 | useBuiltIns: "entry" 7 | } 8 | ] 9 | ] 10 | }; 11 | -------------------------------------------------------------------------------- /cshub-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cshub-client", 3 | "version": "0.3.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build --modern", 9 | "dev-build": "vue-cli-service build --mode development", 10 | "lint": "vue-cli-service lint" 11 | }, 12 | "dependencies": { 13 | "@babel/polyfill": "^7.4.4", 14 | "animate.css": "^3.7.0", 15 | "axios": "^0.21.1", 16 | "codemirror": "^5.58.2", 17 | "dayjs": "^1.7.7", 18 | "katex": "^0.11.0", 19 | "localforage": "^1.7.3", 20 | "lodash": "^4.17.21", 21 | "markdown-it": "^9.1.0", 22 | "markdown-it-katex": "^2.0.3", 23 | "mathjs": "^7.5.1", 24 | "quill": "^1.3.7", 25 | "quill-cursors": "^2.1.1", 26 | "quill-delta": "^4.2.1", 27 | "register-service-worker": "^1.6.2", 28 | "sl-vue-tree": "^1.8.4", 29 | "socket.io-client": "^2.2.0", 30 | "vee-validate": "^2.1.2", 31 | "vue": "^2.6.10", 32 | "vue-class-component": "^7.1.0", 33 | "vue-meta": "^2.2.2", 34 | "vue-property-decorator": "^8.2.2", 35 | "vue-router": "^3.1.3", 36 | "vue-socket.io": "^3.0.4", 37 | "vuetify": "^2.0.14", 38 | "vuex": "^3.1.1", 39 | "vuex-class-modules": "^1.1.1" 40 | }, 41 | "devDependencies": { 42 | "@fortawesome/fontawesome-free": "^5.10.1", 43 | "@types/mathjs": "^6.0.1", 44 | "@types/socket.io-client": "^1.4.32", 45 | "@types/codemirror": "^0.0.71", 46 | "@types/katex": "^0.5.0", 47 | "@types/lodash": "^4.14.117", 48 | "@types/markdown-it": "^0.0.7", 49 | "@types/quill": "^2.0.1", 50 | "@vue/cli-plugin-babel": "^3.9.0", 51 | "@vue/cli-plugin-pwa": "^3.9.0", 52 | "@vue/cli-plugin-typescript": "^3.9.0", 53 | "@vue/cli-service": "^3.9.0", 54 | "@vue/eslint-config-prettier": "^4.0.1", 55 | "@vue/eslint-config-typescript": "^4.0.0", 56 | "deepmerge": "^4.0.0", 57 | "eslint": "^5.16.0", 58 | "eslint-plugin-vue": "^5.0.0", 59 | "fibers": "^4.0.1", 60 | "node-sass": "^4.13.1", 61 | "sass": "^1.22.9", 62 | "sass-loader": "^7.3.1", 63 | "typescript": "^3.6.2", 64 | "vue-cli-plugin-vuetify": "^0.6.3", 65 | "vue-template-compiler": "^2.6.10", 66 | "vuetify-loader": "^1.3.0" 67 | }, 68 | "gitSHA": "a28155c13fcb9b682c9ef64318233d1b93b9b234", 69 | "resolutions": { 70 | "**/katex": "^0.11.0" 71 | }, 72 | "description": "## Project setup ``` npm install ```", 73 | "main": ".eslintrc.js", 74 | "keywords": [], 75 | "author": "" 76 | } 77 | -------------------------------------------------------------------------------- /cshub-client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /cshub-client/public/assets/Sailec-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-client/public/assets/Sailec-Light.otf -------------------------------------------------------------------------------- /cshub-client/public/assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-client/public/assets/logo.jpg -------------------------------------------------------------------------------- /cshub-client/public/img/defaultAvatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-client/public/img/defaultAvatar.png -------------------------------------------------------------------------------- /cshub-client/public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-client/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /cshub-client/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-client/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /cshub-client/public/img/icons/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-client/public/img/icons/favicon-192x192.png -------------------------------------------------------------------------------- /cshub-client/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-client/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /cshub-client/public/img/icons/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-client/public/img/icons/favicon-512x512.png -------------------------------------------------------------------------------- /cshub-client/public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-client/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /cshub-client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CSHub 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /cshub-client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CSHub", 3 | "short_name": "CSHub", 4 | "icons": [ 5 | { 6 | "src": "/img/icons/favicon-16x16.png", 7 | "sizes": "16x16", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/img/icons/favicon-32x32.png", 12 | "sizes": "32x32", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/img/icons/favicon-192x192.png", 17 | "sizes": "192x192", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/img/icons/favicon-512x512.png", 22 | "sizes": "512x512", 23 | "type": "image/png" 24 | } 25 | ], 26 | "start_url": "/", 27 | "display": "standalone", 28 | "background_color": "#196c86", 29 | "theme_color": "#00A6D6" 30 | } 31 | -------------------------------------------------------------------------------- /cshub-client/src/components/global/NavDrawerItem.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /cshub-client/src/components/global/NotificationDialog.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 70 | 71 | 77 | -------------------------------------------------------------------------------- /cshub-client/src/components/posts/Examples.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | 28 | 38 | -------------------------------------------------------------------------------- /cshub-client/src/components/posts/PostPagination.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/DynamicQuestionUtils.ts: -------------------------------------------------------------------------------- 1 | import { VariableValue } from "../../../../cshub-shared/src/api-calls/endpoints/question/models/Variable"; 2 | 3 | export const replaceVariablesByValues = (text: string, variablesAndValues: VariableValue[]) => { 4 | variablesAndValues.sort((a, b) => b.name.length - a.name.length); 5 | 6 | let newText = text; 7 | variablesAndValues.forEach(value => { 8 | newText = newText.replace(new RegExp(`\\$${value.name}`, "g"), value.value.toString()); 9 | }); 10 | return newText; 11 | }; 12 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/QuestionListItemMixin.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Component from "vue-class-component"; 3 | import { FullQuestionWithId } from "../../../../cshub-shared/src/api-calls/endpoints/question/models/FullQuestion"; 4 | import { Prop } from "vue-property-decorator"; 5 | import { QuestionType } from "../../../../cshub-shared/src/entities/question"; 6 | 7 | @Component({ 8 | name: QuestionListItemMixin.name 9 | }) 10 | export default class QuestionListItemMixin extends Vue { 11 | @Prop({ 12 | required: true 13 | }) 14 | public questionId!: number; 15 | 16 | public question: FullQuestionWithId | null = null; 17 | 18 | get type(): string { 19 | if (this.question) { 20 | switch (this.question.type) { 21 | case QuestionType.MULTICLOSED: 22 | case QuestionType.SINGLECLOSED: 23 | return "mc"; 24 | case QuestionType.OPENNUMBER: 25 | return "on"; 26 | case QuestionType.OPENTEXT: 27 | return "ot"; 28 | case QuestionType.DYNAMIC: 29 | return "dn"; 30 | } 31 | } 32 | return ""; 33 | } 34 | 35 | get icon(): string { 36 | if (this.question) { 37 | switch (this.question.type) { 38 | case QuestionType.MULTICLOSED: 39 | return "fas fa-list"; 40 | case QuestionType.SINGLECLOSED: 41 | return "fas fa-list-ul"; 42 | case QuestionType.OPENNUMBER: 43 | return "fas fa-calculator"; 44 | case QuestionType.OPENTEXT: 45 | return "fas fa-font"; 46 | case QuestionType.DYNAMIC: 47 | return "fas fa-sync"; 48 | } 49 | } 50 | 51 | return ""; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/editors/EditorAccordion.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/editors/Editors.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/question/DNQuestionMixin.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Component from "vue-class-component"; 3 | import { practiceState } from "../../../store"; 4 | import { QuestionType } from "../../../../../cshub-shared/src/entities/question"; 5 | import { VariableValue } from "../../../../../cshub-shared/src/api-calls/endpoints/question/models/Variable"; 6 | 7 | @Component({ 8 | name: DNQuestionMixin.name 9 | }) 10 | export default class DNQuestionMixin extends Vue { 11 | private privDnAnswer: string | number | null = this.getInitialDnState(); 12 | public variableValues: VariableValue[] = []; 13 | 14 | get dnAnswer(): string | number | null { 15 | return this.privDnAnswer; 16 | } 17 | 18 | set dnAnswer(value: string | number | null) { 19 | if (value !== null) { 20 | let valueAsStringOrNumber: string | number = Number(value); 21 | if (isNaN(valueAsStringOrNumber)) { 22 | valueAsStringOrNumber = value; 23 | } 24 | 25 | const questionIndex = +this.$route.params.index; 26 | practiceState.addAnswer({ 27 | questionIndex, 28 | answer: { 29 | type: QuestionType.DYNAMIC, 30 | answer: valueAsStringOrNumber, 31 | variables: this.variableValues 32 | } 33 | }); 34 | } 35 | } 36 | 37 | private getInitialDnState(): string | number | null { 38 | const currentQuestions = practiceState.currentQuestions; 39 | if (currentQuestions) { 40 | const savedData = currentQuestions[+this.$route.params.index]; 41 | 42 | if (savedData.answer && savedData.answer.type === QuestionType.DYNAMIC) { 43 | return savedData.answer.answer; 44 | } 45 | } 46 | 47 | return ""; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/question/MCQuestionMixin.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Component from "vue-class-component"; 3 | import { practiceState } from "../../../store"; 4 | import { QuestionType } from "../../../../../cshub-shared/src/entities/question"; 5 | import { Watch } from "vue-property-decorator"; 6 | 7 | type MCAnswerType = { [index: number]: boolean }; 8 | 9 | @Component({ 10 | name: MCQuestionMixin.name 11 | }) 12 | export default class MCQuestionMixin extends Vue { 13 | private privMcAnswers: MCAnswerType = this.getInitialMcState(); 14 | 15 | get mcAnswers(): MCAnswerType { 16 | if (practiceState.currentCheckedQuestion) { 17 | if (!practiceState.currentCheckedQuestion.correct) { 18 | const answer = practiceState.currentCheckedQuestion.correctAnswer; 19 | if (answer.type === QuestionType.MULTICLOSED) { 20 | const correctAnswers: MCAnswerType = {}; 21 | 22 | answer.answerIds.forEach(answer => (correctAnswers[answer] = true)); 23 | 24 | return { 25 | ...this.privMcAnswers, 26 | ...correctAnswers 27 | }; 28 | } 29 | } 30 | } 31 | 32 | return this.privMcAnswers; 33 | } 34 | 35 | @Watch("privMcAnswers", { 36 | deep: true 37 | }) 38 | private onPrivMcAnswersUpdate(value: MCAnswerType) { 39 | const questionIndex = +this.$route.params.index; 40 | const answerIds: number[] = []; 41 | 42 | for (const key of Object.keys(value)) { 43 | if (value[+key]) { 44 | answerIds.push(+key); 45 | } 46 | } 47 | 48 | practiceState.addAnswer({ 49 | questionIndex, 50 | answer: { 51 | type: QuestionType.MULTICLOSED, 52 | answerIds: answerIds 53 | } 54 | }); 55 | } 56 | 57 | private getInitialMcState(): MCAnswerType { 58 | const currentQuestions = practiceState.currentQuestions; 59 | if (currentQuestions) { 60 | const savedData = currentQuestions[+this.$route.params.index]; 61 | 62 | if (savedData.answer && savedData.answer.type === QuestionType.MULTICLOSED) { 63 | return savedData.answer.answerIds.reduce((previousValue, currentValue) => { 64 | previousValue[currentValue] = true; 65 | return previousValue; 66 | }, {} as MCAnswerType); 67 | } 68 | } 69 | 70 | return {} as MCAnswerType; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/question/ONQuestionMixin.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Component from "vue-class-component"; 3 | import { practiceState } from "../../../store"; 4 | import { QuestionType } from "../../../../../cshub-shared/src/entities/question"; 5 | import { Watch } from "vue-property-decorator"; 6 | 7 | @Component({ 8 | name: ONQuestionMixin.name 9 | }) 10 | export default class ONQuestionMixin extends Vue { 11 | private privOnAnswer: number | null = this.getInitialOnState(); 12 | 13 | get onAnswer(): number | null { 14 | return this.privOnAnswer; 15 | } 16 | 17 | set onAnswer(value: number | null) { 18 | const questionIndex = +this.$route.params.index; 19 | 20 | if (value !== null) { 21 | const questionIndex = +this.$route.params.index; 22 | practiceState.addAnswer({ 23 | questionIndex, 24 | answer: { 25 | type: QuestionType.OPENNUMBER, 26 | number: Number(value) 27 | } 28 | }); 29 | } 30 | } 31 | 32 | private getInitialOnState(): number | null { 33 | const currentQuestions = practiceState.currentQuestions; 34 | if (currentQuestions) { 35 | const savedData = currentQuestions[+this.$route.params.index]; 36 | 37 | if (savedData.answer && savedData.answer.type === QuestionType.OPENNUMBER) { 38 | return savedData.answer.number; 39 | } 40 | } 41 | 42 | return 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/question/OTQuestionMixin.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Component from "vue-class-component"; 3 | import { practiceState } from "../../../store"; 4 | import { QuestionType } from "../../../../../cshub-shared/src/entities/question"; 5 | 6 | @Component({ 7 | name: OTQuestionMixin.name 8 | }) 9 | export default class OTQuestionMixin extends Vue { 10 | private privOtAnswer: string | null = this.getInitialOtState(); 11 | 12 | get otAnswer(): string | null { 13 | return this.privOtAnswer; 14 | } 15 | 16 | set otAnswer(value: string | null) { 17 | if (value !== null) { 18 | const questionIndex = +this.$route.params.index; 19 | practiceState.addAnswer({ 20 | questionIndex, 21 | answer: { 22 | type: QuestionType.OPENTEXT, 23 | text: value 24 | } 25 | }); 26 | } 27 | } 28 | 29 | private getInitialOtState(): string | null { 30 | const currentQuestions = practiceState.currentQuestions; 31 | if (currentQuestions) { 32 | const savedData = currentQuestions[+this.$route.params.index]; 33 | 34 | if (savedData.answer && savedData.answer.type === QuestionType.OPENTEXT) { 35 | return savedData.answer.text; 36 | } 37 | } 38 | 39 | return ""; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/question/QuestionMixin.ts: -------------------------------------------------------------------------------- 1 | import Component, { mixins } from "vue-class-component"; 2 | import SCQuestionMixin from "./SCQuestionMixin"; 3 | import MCQuestionMixin from "./MCQuestionMixin"; 4 | import OTQuestionMixin from "./OTQuestionMixin"; 5 | import ONQuestionMixin from "./ONQuestionMixin"; 6 | import DNQuestionMixin from "./DNQuestionMixin"; 7 | 8 | @Component({ 9 | name: QuestionMixin.name 10 | }) 11 | export default class QuestionMixin extends mixins( 12 | SCQuestionMixin, 13 | MCQuestionMixin, 14 | OTQuestionMixin, 15 | ONQuestionMixin, 16 | DNQuestionMixin 17 | ) {} 18 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/question/SCQuestionMixin.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Component from "vue-class-component"; 3 | import { practiceState } from "../../../store"; 4 | import { QuestionType } from "../../../../../cshub-shared/src/entities/question"; 5 | 6 | @Component({ 7 | name: SCQuestionMixin.name 8 | }) 9 | export default class SCQuestionMixin extends Vue { 10 | private privScAnswer: number | null = this.getInitialScState(); 11 | 12 | get scAnswer(): number | number[] | null { 13 | const privScAnswer = this.privScAnswer; 14 | 15 | if (practiceState.currentCheckedQuestion && privScAnswer) { 16 | if (!practiceState.currentCheckedQuestion.correct) { 17 | const answer = practiceState.currentCheckedQuestion.correctAnswer; 18 | if (answer.type === QuestionType.SINGLECLOSED) { 19 | return [privScAnswer, answer.answerId]; 20 | } 21 | } else { 22 | return [privScAnswer]; 23 | } 24 | } 25 | 26 | return privScAnswer; 27 | } 28 | 29 | set scAnswer(value: number | number[] | null) { 30 | if (value !== null && !Array.isArray(value)) { 31 | this.privScAnswer = value; 32 | 33 | const questionIndex = +this.$route.params.index; 34 | const currentQuestions = practiceState.currentQuestions; 35 | practiceState.addAnswer({ 36 | questionIndex, 37 | answer: { 38 | type: QuestionType.SINGLECLOSED, 39 | answerId: value 40 | } 41 | }); 42 | } 43 | } 44 | 45 | private getInitialScState(): number | null { 46 | const currentQuestions = practiceState.currentQuestions; 47 | if (currentQuestions) { 48 | const savedData = currentQuestions[+this.$route.params.index]; 49 | 50 | if (savedData.answer && savedData.answer.type === QuestionType.SINGLECLOSED) { 51 | return savedData.answer.answerId; 52 | } 53 | } 54 | 55 | return null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/viewers/DynamicViewer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/viewers/MultipleChoiceViewer.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/viewers/OpenNumberViewer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /cshub-client/src/components/practice/viewers/OpenTextViewer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /cshub-client/src/components/quill/CustomTooltip.ts: -------------------------------------------------------------------------------- 1 | import Quill, { BoundsStatic } from "quill"; 2 | 3 | const Tooltip = Quill.import("ui/tooltip"); 4 | 5 | export class CustomTooltip extends Tooltip { 6 | constructor(quill: Quill, boundsContainer: BoundsStatic, innerElement: HTMLElement) { 7 | super(quill, boundsContainer); 8 | super.root.remove(); 9 | super.root = quill.addContainer(innerElement); 10 | if (this.quill.root === this.quill.scrollingContainer) { 11 | this.quill.root.addEventListener("scroll", () => { 12 | this.root.style.marginTop = `${-1 * this.quill.root.scrollTop}px`; 13 | }); 14 | } 15 | this.hide(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cshub-client/src/components/quill/IQuillEditSetup.ts: -------------------------------------------------------------------------------- 1 | export interface IQuillEditSetup { 2 | showToolbar: boolean; 3 | allowEdit: boolean; 4 | postHash: number; 5 | } 6 | -------------------------------------------------------------------------------- /cshub-client/src/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # With help of https://www.brandonbarnett.io/blog/2018/05/accessing-environment-variables-from-a-webpack-bundle-in-a-docker-container/ 3 | echo "window.appConfig = { VUE_APP_API_URL: '${VUE_APP_API_URL}'} " >> config.js 4 | cat config.js 5 | exec nginx -g 'daemon off;' 6 | -------------------------------------------------------------------------------- /cshub-client/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Vue } from "vue/types/vue"; 2 | 3 | declare var process: { 4 | env: { 5 | NODE_ENV: string; 6 | VUE_APP_API_URL: string; 7 | VUE_APP_DEBUG: string; 8 | VUE_APP_VERSION: string; 9 | VUE_APP_BUILDDATE: string; 10 | }; 11 | }; 12 | 13 | declare module "vue/types/vue" { 14 | export interface Vue { 15 | $socket: any; 16 | sockets: any; 17 | } 18 | } 19 | 20 | declare module "vue/types/options" { 21 | interface ComponentOptions { 22 | sockets?: any; 23 | } 24 | } 25 | 26 | declare var window: { 27 | appConfig: any; 28 | }; 29 | -------------------------------------------------------------------------------- /cshub-client/src/main.ts: -------------------------------------------------------------------------------- 1 | import Component from "vue-class-component"; 2 | Component.registerHooks(["beforeRouteEnter", "beforeRouteLeave", "beforeRouteUpdate", "metaInfo"]); 3 | 4 | import Meta from "vue-meta"; 5 | import "@babel/polyfill"; 6 | import Vue from "vue"; 7 | import "./plugins"; 8 | 9 | Vue.use(Meta, { 10 | keyName: "metaInfo", 11 | refreshOnceOnNavigation: true 12 | }); 13 | 14 | import App from "./App.vue"; 15 | import router from "./views/router/router"; 16 | import store from "./store/store"; 17 | import "./registerServiceWorker"; 18 | import "./styleOverwrites.css"; 19 | import "./utilities/pipes"; 20 | 21 | import vuetify from "./plugins/vuetify/vuetify"; 22 | 23 | Vue.config.productionTip = false; 24 | 25 | new Vue({ 26 | router, 27 | store, 28 | vuetify, 29 | render: (h: any) => h(App) 30 | }).$mount("#app"); 31 | -------------------------------------------------------------------------------- /cshub-client/src/plugins/animatecss.ts: -------------------------------------------------------------------------------- 1 | import "animate.css/animate.min.css"; 2 | -------------------------------------------------------------------------------- /cshub-client/src/plugins/codemirror.ts: -------------------------------------------------------------------------------- 1 | import "codemirror/lib/codemirror"; 2 | import "codemirror/addon/runmode/runmode"; 3 | 4 | import "codemirror/mode/cypher/cypher"; 5 | import "codemirror/mode/htmlembedded/htmlembedded"; 6 | import "codemirror/mode/xml/xml"; 7 | import "codemirror/mode/htmlmixed/htmlmixed"; 8 | import "codemirror/mode/javascript/javascript"; 9 | import "codemirror/mode/sql/sql"; 10 | import "codemirror/mode/css/css"; 11 | import "codemirror/mode/clike/clike"; 12 | import "codemirror/mode/http/http"; 13 | import "codemirror/mode/php/php"; 14 | import "codemirror/mode/pug/pug"; 15 | import "codemirror/mode/dockerfile/dockerfile"; 16 | import "codemirror/mode/shell/shell"; 17 | import "codemirror/mode/cmake/cmake"; 18 | import "codemirror/mode/dart/dart"; 19 | import "codemirror/mode/diff/diff"; 20 | import "codemirror/mode/groovy/groovy"; 21 | import "codemirror/mode/go/go"; 22 | import "codemirror/mode/nginx/nginx"; 23 | import "codemirror/mode/powershell/powershell"; 24 | import "codemirror/mode/python/python"; 25 | import "codemirror/mode/yaml/yaml"; 26 | import "codemirror/mode/haskell/haskell"; 27 | 28 | import "codemirror/lib/codemirror.css"; 29 | import "codemirror/theme/darcula.css"; 30 | -------------------------------------------------------------------------------- /cshub-client/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./vuetify/vuetify"; 2 | export * from "./animatecss"; 3 | export * from "./socket.io"; 4 | export * from "./codemirror"; 5 | -------------------------------------------------------------------------------- /cshub-client/src/plugins/quill/mathquill4quill.min.js: -------------------------------------------------------------------------------- 1 | export function mathquill4quill(e, t) { 2 | function n(e) { 3 | return e.container.getElementsByClassName("ql-tooltip")[0]; 4 | } 5 | e.prototype.enableMathQuillFormulaAuthoring = function() { 6 | var e = n(this).getElementsByTagName("input")[0]; 7 | e.style.display = "none"; 8 | var l, 9 | i, 10 | a = document.createElement("span"); 11 | !(function(e) { 12 | (e.style.border = "1px solid #ccc"), 13 | (e.style.fontSize = "13px"), 14 | (e.style.minHeight = "26px"), 15 | (e.style.margin = "0px"), 16 | (e.style.padding = "3px 5px"), 17 | (e.style.width = "170px"); 18 | })(a), 19 | (l = a), 20 | (i = e).parentNode.insertBefore(l, i.nextSibling); 21 | var o = t.getInterface(2).MathField(a, { 22 | handlers: { 23 | edit: function() { 24 | e.value = o.latex(); 25 | } 26 | } 27 | }); 28 | (function(e) { 29 | return n(e).getElementsByClassName("ql-action")[0]; 30 | })(this).addEventListener("click", function() { 31 | o.latex(""); 32 | }); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /cshub-client/src/plugins/socket.io.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import VueSocketIO from "vue-socket.io"; 3 | import Vue from "vue"; 4 | 5 | const socket = new VueSocketIO({ 6 | connection: process.env.VUE_APP_API_URL || (window as any).appConfig.VUE_APP_API_URL 7 | }); 8 | 9 | Vue.use(socket); 10 | -------------------------------------------------------------------------------- /cshub-client/src/plugins/vuetify/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vuetify from "vuetify/lib"; 2 | import "@fortawesome/fontawesome-free/css/fontawesome.min.css"; 3 | import "@fortawesome/fontawesome-free/css/solid.min.css"; 4 | import "@fortawesome/fontawesome-free/css/brands.min.css"; 5 | import "@fortawesome/fontawesome-free/css/regular.min.css"; 6 | import Vue from "vue"; 7 | import { LocalStorageData } from "../../store/localStorageData"; 8 | 9 | Vue.use(Vuetify); 10 | 11 | export default new Vuetify({ 12 | theme: { 13 | themes: { 14 | light: { 15 | primary: "#00A6D6", 16 | secondary: "#424242", 17 | accent: "#26b3dc", 18 | error: "#FF5252", 19 | info: "#2196F3", 20 | success: "#4CAF50", 21 | warning: "#FFC107" 22 | } 23 | }, 24 | dark: localStorage.getItem(LocalStorageData.DARK) === "true" 25 | }, 26 | customProperties: true, 27 | icons: { 28 | iconfont: "fa" 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /cshub-client/src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-console */ 2 | 3 | import { register } from "register-service-worker"; 4 | 5 | if (process.env.NODE_ENV === "production") { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready() { 8 | console.log( 9 | "App is being served from cache by a service worker.\n" + 10 | "For more details, visit https://goo.gl/AFskqB" 11 | ); 12 | }, 13 | cached() { 14 | console.log("Content has been cached for offline use."); 15 | }, 16 | updated(event) { 17 | const promiseChain = caches.keys().then(cacheNames => { 18 | // Step through each cache name and delete it 19 | return Promise.all(cacheNames.map(cacheName => caches.delete(cacheName))); 20 | }); 21 | }, 22 | offline() { 23 | console.log("No internet connection found. App is running in offline mode."); 24 | }, 25 | error(error) { 26 | console.error("Error during service worker registration:", error); 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /cshub-client/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { uiStateModule as uiState } from "./state/uiState"; 2 | import { dataStateModule as dataState } from "./state/dataState"; 3 | import { userStateModule as userState } from "./state/userState"; 4 | import { practiceStateModule as practiceState } from "./state/practiceState"; 5 | 6 | export { uiState, dataState, userState, practiceState }; 7 | -------------------------------------------------------------------------------- /cshub-client/src/store/localStorageData.ts: -------------------------------------------------------------------------------- 1 | export enum LocalStorageData { 2 | EMAIL = "EMAIL", 3 | DARK = "DARK", 4 | STUDY = "STUDY" 5 | } 6 | -------------------------------------------------------------------------------- /cshub-client/src/store/state/dataState.ts: -------------------------------------------------------------------------------- 1 | import { ITopic } from "../../../../cshub-shared/src/entities/topic"; 2 | import { Module, Mutation, VuexModule } from "vuex-class-modules"; 3 | import store from "../store"; 4 | import { IStudy } from "../../../../cshub-shared/src/entities/study"; 5 | 6 | export interface IDataState { 7 | topTopic: ITopic | null; 8 | studies: IStudy[] | null; 9 | hasConnection: boolean; 10 | searchQuery: string; 11 | } 12 | 13 | @Module 14 | class DataState extends VuexModule implements IDataState { 15 | private _topics: ITopic | null = null; 16 | 17 | private _studies: IStudy[] | null = null; 18 | 19 | private _hasConnection = true; 20 | 21 | private _searchQuery = ""; 22 | 23 | get topTopic(): ITopic | null { 24 | return this._topics; 25 | } 26 | 27 | @Mutation 28 | public setTopics(value: ITopic) { 29 | this._topics = value; 30 | } 31 | 32 | get studies(): IStudy[] | null { 33 | return this._studies; 34 | } 35 | 36 | @Mutation 37 | public setStudies(value: IStudy[]) { 38 | this._studies = value; 39 | } 40 | 41 | get hasConnection(): boolean { 42 | return this._hasConnection; 43 | } 44 | 45 | @Mutation 46 | public setConnection(value: boolean) { 47 | this._hasConnection = value; 48 | } 49 | 50 | get searchQuery(): string { 51 | return this._searchQuery; 52 | } 53 | 54 | @Mutation 55 | public setSearchQuery(value: string) { 56 | this._searchQuery = value; 57 | } 58 | } 59 | 60 | export const dataStateModule = new DataState({ 61 | store, 62 | name: "dataStateModule" 63 | }); 64 | -------------------------------------------------------------------------------- /cshub-client/src/store/state/userState.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from "../../../../cshub-shared/src/entities/user"; 2 | import { Module, Mutation, VuexModule } from "vuex-class-modules"; 3 | import { IStudy } from "../../../../cshub-shared/src/entities/study"; 4 | import store from "../store"; 5 | import { uiState } from "../index"; 6 | 7 | export interface IUserState { 8 | userModel: IUser | null; 9 | hasCheckedToken: boolean; 10 | } 11 | 12 | @Module 13 | class UserState extends VuexModule implements IUserState { 14 | private _userModel: IUser | null = null; 15 | private _hasCheckedToken = false; 16 | 17 | get userModel(): IUser | null { 18 | return this._userModel; 19 | } 20 | 21 | @Mutation 22 | public setUserModel(value: IUser) { 23 | this._userModel = value; 24 | } 25 | 26 | @Mutation 27 | public clearUserModel() { 28 | this._userModel = null; 29 | } 30 | 31 | get hasCheckedToken(): boolean { 32 | return this._hasCheckedToken; 33 | } 34 | 35 | @Mutation 36 | public setHasCheckedToken(value: boolean) { 37 | this._hasCheckedToken = value; 38 | } 39 | 40 | get studyAdmins(): IStudy[] { 41 | if (this._userModel && this._userModel.studies) { 42 | return this._userModel.studies; 43 | } 44 | return []; 45 | } 46 | 47 | get isAdmin(): boolean { 48 | if (this._userModel) { 49 | return this._userModel.admin; 50 | } 51 | return false; 52 | } 53 | 54 | get isStudyAdmin(): boolean { 55 | if (this.isAdmin) { 56 | return true; 57 | } 58 | if (this.studyAdmins.length > 0) { 59 | const currStudy = uiState.studyNr; 60 | const studyAdmin = this.studyAdmins.findIndex(study => study.id === currStudy); 61 | return studyAdmin !== -1; 62 | } 63 | return false; 64 | } 65 | 66 | get isLoggedIn(): boolean { 67 | if (this._userModel) { 68 | return this._userModel.id !== 0; 69 | } 70 | return false; 71 | } 72 | } 73 | 74 | export const userStateModule = new UserState({ 75 | store, 76 | name: "userStateModule" 77 | }); 78 | -------------------------------------------------------------------------------- /cshub-client/src/store/store.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | 4 | import { IDataState } from "./state/dataState"; 5 | import { IUserState } from "./state/userState"; 6 | import { IUIState } from "./state/uiState"; 7 | import { IPracticeState } from "./state/practiceState"; 8 | 9 | export interface IRootState { 10 | user: IUserState; 11 | ui: IUIState; 12 | data: IDataState; 13 | practice: IPracticeState; 14 | } 15 | 16 | Vue.use(Vuex); 17 | 18 | export default new Vuex.Store({}); 19 | -------------------------------------------------------------------------------- /cshub-client/src/styleOverwrites.css: -------------------------------------------------------------------------------- 1 | .v-treeview-node__label { 2 | font-size: inherit !important; 3 | } 4 | 5 | .v-input__control { 6 | flex-grow: 1 !important; 7 | } 8 | 9 | .fullHeight { 10 | height: 100%; 11 | } 12 | 13 | .v-tabs-items { 14 | background-color: unset !important; 15 | } 16 | 17 | .v-tabs > .v-tabs-bar { 18 | background-color: unset !important; 19 | } 20 | 21 | .v-application code { 22 | color: unset !important; 23 | background-color: unset !important; 24 | box-shadow: unset !important; 25 | } 26 | 27 | .loginMailSelect .v-select__selection { 28 | max-width: 100%; 29 | } 30 | 31 | .loginMailSelect .v-select__selections input { 32 | display: none; 33 | } 34 | -------------------------------------------------------------------------------- /cshub-client/src/styling/vars.scss: -------------------------------------------------------------------------------- 1 | // Dark theme 2 | $bg-dark: #303030; 3 | $fg-dark: white; 4 | 5 | // Light theme 6 | $bg-light: #fafafa; 7 | $fg-light: rgba(0,0,0,0.78); 8 | 9 | $grey: #9e9e9e; 10 | $lightergrey: rgb(75, 75, 75); 11 | -------------------------------------------------------------------------------- /cshub-client/src/typings/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | 8 | // tslint:disable no-empty-interface 9 | interface ElementClass extends Vue {} 10 | 11 | interface IntrinsicElements { 12 | [elem: string]: any; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cshub-client/src/typings/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/EventBus.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | export const EventBus = new Vue(); 3 | 4 | export const STUDY_CHANGED = "STUDY_CHANGED"; 5 | export const QUESTIONS_CHANGED = "QUESTIONS_CHANGED"; 6 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/Topics.ts: -------------------------------------------------------------------------------- 1 | // This is a recursive function which will get the topic from its hash, if not, check the children (by calling itself on the children) 2 | import { ITopic } from "../../../cshub-shared/src/entities/topic"; 3 | 4 | export const getTopicFromHash = (topicHash: number, topics: ITopic[]): ITopic | null => { 5 | for (const topic of topics) { 6 | if (topic.hash === topicHash) { 7 | return topic; 8 | } else if (typeof topic.children !== "undefined" && topic.children !== null) { 9 | const currTopic = getTopicFromHash(topicHash, topic.children); 10 | if (currTopic !== null) { 11 | return currTopic; 12 | } 13 | } 14 | } 15 | return null; 16 | }; 17 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/cache-types.ts: -------------------------------------------------------------------------------- 1 | export enum CacheTypes { 2 | POSTS = "POST_", 3 | TOPICS = "TOPICS_", // _studyId 4 | TOPICPOST = "TOPICPOST_", // _topicHash 5 | EXAMPLES = "EXAMPLES_", // _topicHash 6 | STUDIES = "STUDIES" 7 | } 8 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/codemirror-colorize.ts: -------------------------------------------------------------------------------- 1 | import { uiState } from "../store"; 2 | 3 | const isBlock = /^(p|li|div|h\\d|pre|blockquote|td)$/; 4 | 5 | const textContent = (node: Node, out: string[]) => { 6 | if (node.nodeType === 3) { 7 | const nodeValue = node.nodeValue; 8 | if (nodeValue) { 9 | return out.push(nodeValue); 10 | } 11 | } 12 | for (let ch = node.firstChild; ch; ch = ch.nextSibling) { 13 | textContent(ch, out); 14 | if (isBlock.test(node.nodeType.toString())) { 15 | out.push("\n"); 16 | } 17 | } 18 | }; 19 | 20 | export const colorize = (collection: any, codemirror: any) => { 21 | if (!collection) { 22 | collection = document.body.getElementsByTagName("pre"); 23 | } 24 | 25 | const theme = uiState.darkMode ? "darcula" : "default"; 26 | 27 | for (const node of collection) { 28 | let mode = node.getAttribute("data-lang"); 29 | if (!mode) { 30 | if (node.children.length > 0 && node.children[0].tagName === "CODE") { 31 | mode = "null"; 32 | } else { 33 | continue; 34 | } 35 | } 36 | 37 | const text: string[] = []; 38 | textContent(node, text); 39 | node.innerHTML = ""; 40 | codemirror.runMode(text.join(""), mode, node, { 41 | theme 42 | }); 43 | 44 | node.className = `cm-s-${theme}`; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/debugConsole.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import { userState } from "../store"; 3 | 4 | const showDebugConsole = () => { 5 | return process.env.VUE_APP_DEBUG === "true" || userState.isAdmin; 6 | }; 7 | 8 | export const logStringConsole = (input: string, location?: string) => { 9 | if (showDebugConsole()) { 10 | if (!location) { 11 | location = "unknown"; 12 | } 13 | console.log(`%c (${dayjs().format()}) Log: "${input}" at ${location}`, "font: 1em Verdana; color: blue"); 14 | } 15 | }; 16 | 17 | export const logObjectConsole = (input: any, location?: string) => { 18 | if (showDebugConsole()) { 19 | if (!location) { 20 | location = "unknown"; 21 | } 22 | console.log( 23 | `%c (${dayjs().format()}) Object log: at ${location}, ${JSON.stringify(input)}`, 24 | "font: 1em Verdana; color: blue" 25 | ); 26 | } 27 | }; 28 | 29 | export const errorLogStringConsole = (input: string, location: string) => { 30 | if (showDebugConsole()) { 31 | if (!location) { 32 | location = "unknown"; 33 | } 34 | console.error( 35 | `%c (${dayjs().format()}) Error: "${input}" at ${location}`, 36 | "font: 1.3em Verdana bold; color: red" 37 | ); 38 | } 39 | }; 40 | 41 | export const errorLogObjectConsole = (input: any, location?: string) => { 42 | if (showDebugConsole()) { 43 | if (!location) { 44 | location = "unknown"; 45 | } 46 | console.error( 47 | `%c (${dayjs().format()}) Object error: at ${location}, ${JSON.stringify(input)}`, 48 | "font: 1.3em Verdana bold; color: red" 49 | ); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/id-generator.ts: -------------------------------------------------------------------------------- 1 | export const idGenerator = (): string => { 2 | let id = ""; 3 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 4 | 5 | for (let i = 0; i < 10; i++) { 6 | id += possible.charAt(Math.floor(Math.random() * possible.length)); 7 | } 8 | 9 | return id; 10 | }; 11 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./debugConsole"; 2 | export * from "./validation"; 3 | export * from "./api-wrapper"; 4 | export * from "./pipes"; 5 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/pipes.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import dayjs from "dayjs"; 3 | 4 | Vue.filter("formatDate", (value: string): string => { 5 | return dayjs(value).format("DD-MM-YYYY, H:mm"); 6 | }); 7 | 8 | Vue.filter("roundNumber", (value: number, decimals: number): string => { 9 | if (decimals !== null) { 10 | return value.toFixed(decimals); 11 | } else { 12 | return value.toFixed(2); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/socket-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { ISocketRequest } from "../../../cshub-shared/src/models"; 2 | import { logStringConsole } from "./debugConsole"; 3 | 4 | export class SocketWrapper { 5 | public static emitSocket(request: ISocketRequest, sockets: any) { 6 | sockets.emit(request.URL, request, request.callback); 7 | } 8 | 9 | public static reconnectSocket(sockets: any) { 10 | logStringConsole("Reconnecting socket"); 11 | sockets.close(); 12 | sockets.open(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cshub-client/src/utilities/validation.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VeeValidate from "vee-validate"; 3 | 4 | Vue.use(VeeValidate, { inject: true, delay: 1 }); 5 | -------------------------------------------------------------------------------- /cshub-client/src/views/UnsavedQuestions.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /cshub-client/src/views/router/guards/adminDashboardGuard.ts: -------------------------------------------------------------------------------- 1 | import { Route } from "vue-router"; 2 | import { userState } from "../../../store"; 3 | import router from "../router"; 4 | import { Routes } from "../../../../../cshub-shared/src/Routes"; 5 | 6 | export const adminBeforeEnter = (to: Route, from: Route, next: () => any) => { 7 | if (userState.isLoggedIn && userState.isStudyAdmin) { 8 | next(); 9 | } else if (!userState.isStudyAdmin && userState.isLoggedIn) { 10 | router.push(Routes.INDEX); 11 | } else { 12 | router.push(Routes.LOGIN); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /cshub-client/src/views/router/guards/onlyIfNotLoggedInGuard.ts: -------------------------------------------------------------------------------- 1 | import { Route } from "vue-router"; 2 | import router from "../router"; 3 | import { userState } from "../../../store"; 4 | import { Routes } from "../../../../../cshub-shared/src/Routes"; 5 | 6 | export const onlyIfNotLoggedIn = (to: Route, from: Route, next: () => any) => { 7 | if (!userState.isLoggedIn) { 8 | next(); 9 | } else { 10 | router.push(Routes.INDEX); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /cshub-client/src/views/router/guards/userDashboardGuard.ts: -------------------------------------------------------------------------------- 1 | import { Route } from "vue-router"; 2 | import { userState } from "../../../store"; 3 | import router from "../router"; 4 | import { Routes } from "../../../../../cshub-shared/src/Routes"; 5 | 6 | export const userBeforeEnter = (to: Route, from: Route, next: () => any) => { 7 | if (userState.isLoggedIn) { 8 | next(); 9 | } else { 10 | router.push(Routes.LOGIN); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /cshub-client/src/views/user/UnsavedPosts.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /cshub-client/src/views/user/WIPPostsView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /cshub-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "experimentalDecorators": true, 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "vuetify" 18 | ], 19 | "lib": [ 20 | "esnext", 21 | "dom", 22 | "dom.iterable", 23 | "scripthost" 24 | ] 25 | }, 26 | "include": [ 27 | "src/**/*.ts", 28 | "src/**/*.tsx", 29 | "src/**/*.vue", 30 | "../cshub-shared/src/**/*.ts", 31 | "tests/**/*.ts", 32 | "tests/**/*.tsx" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /cshub-client/vue.config.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | 3 | process.env.VUE_APP_VERSION = JSON.parse(readFileSync("package.json"))["gitSHA"]; 4 | process.env.VUE_APP_BUILDDATE = new Date().toLocaleString(); 5 | 6 | module.exports = { 7 | pwa: { 8 | themeColor: "#00A6D6", 9 | name: "CSHub", 10 | workboxPluginMode: "GenerateSW", 11 | appleMobileWebAppCapable: true, 12 | assetsVersion: Date.now(), 13 | iconPaths: { 14 | favicon32: "img/icons/favicon-32x32.png", 15 | favicon16: "img/icons/favicon-16x16.png", 16 | appleTouchIcon: "img/icons/apple-touch-icon-152x152.png", 17 | maskIcon: "img/icons/safari-pinned-tab.svg", 18 | msTileImage: "img/icons/msapplication-icon-144x144.png" 19 | } 20 | }, 21 | baseUrl: undefined, 22 | outputDir: undefined, 23 | assetsDir: undefined, 24 | runtimeCompiler: undefined, 25 | productionSourceMap: false, 26 | parallel: undefined, 27 | css: undefined 28 | }; 29 | -------------------------------------------------------------------------------- /cshub-server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /cshub-server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@robbinbaauw"], 3 | rules: { 4 | "@typescript-eslint/explicit-module-boundary-types": [ 5 | "error", 6 | { 7 | allowArgumentsExplicitlyTypedAsAny: true, 8 | }, 9 | ], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /cshub-server/.gitignore: -------------------------------------------------------------------------------- 1 | src/settings.ts 2 | settings.env 3 | logs 4 | 5 | # Default gitignore: 6 | # compiled output 7 | /dist 8 | /tmp 9 | /out-tsc 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | yarn-error.log 37 | testem.log 38 | /typings 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /cshub-server/Dockerfile: -------------------------------------------------------------------------------- 1 | # Available at cshubnl/shared 2 | ARG BASE_IMAGE=cshub-shared 3 | 4 | # build stage 5 | FROM $BASE_IMAGE as build-server 6 | 7 | # Set the current working directory 8 | WORKDIR /app/cshub-server 9 | 10 | # Copy package files 11 | COPY package.json ./ 12 | COPY yarn.lock ./ 13 | 14 | # Install dependencies and subsequently remove the build dependencies 15 | RUN apk add python make g++ --no-cache -t builddep && yarn install && apk del builddep && yarn cache clean 16 | 17 | # Copy source files 18 | COPY . . 19 | COPY ./src/SettingsBaseline.ts ./src/settings.ts 20 | 21 | # Install TSC, Compiles TS, Remove TSC 22 | RUN yarn global add typescript@4.0.3 && tsc && yarn global remove typescript && yarn cache clean 23 | RUN mkdir -p /app/cshub-server/dist/cshub-server/src/endpoints/post/assets 24 | COPY ./src/endpoints/post/assets/* /app/cshub-server/dist/cshub-server/src/endpoints/post/assets/ 25 | 26 | CMD ["node", "dist/cshub-server/src/index.js"] 27 | -------------------------------------------------------------------------------- /cshub-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cshub-server", 3 | "version": "0.3.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "tsc --module commonjs", 7 | "dev": "ts-node src/index.ts", 8 | "lint": "eslint \"./src/**/*.ts\"", 9 | "lint-fix": "eslint --fix \"./src/**/*.ts\"" 10 | }, 11 | "dependencies": { 12 | "@sendgrid/mail": "^6.3.1", 13 | "async": "^2.6.1", 14 | "body-parser": "^1.18.3", 15 | "class-transformer": "^0.3.1", 16 | "cookie-parser": "^1.4.3", 17 | "dayjs": "^1.7.7", 18 | "escape-html": "^1.0.3", 19 | "express": "^4.16.2", 20 | "express-mung": "^0.5.1", 21 | "is-plain-object": "^3.0.0", 22 | "jsdom": "^13.2.0", 23 | "jsonwebtoken": "^8.3.0", 24 | "katex": "^0.11.0", 25 | "lodash": "^4.17.21", 26 | "markdown-it": "^11.0.1", 27 | "markdown-it-katex": "latest", 28 | "mathjs": "^7.5.1", 29 | "mysql": "^2.17.1", 30 | "nodemailer": "^6.4.16", 31 | "quill": "2.0.0-dev.3", 32 | "randomcolor": "^0.5.3", 33 | "reflect-metadata": "^0.1.13", 34 | "sharp": "^0.26.1", 35 | "socket.io": "^2.4.0", 36 | "tunnel-ssh": "^4.1.4", 37 | "typeorm": "^0.2.18", 38 | "winston": "^3.2.1" 39 | }, 40 | "devDependencies": { 41 | "@robbinbaauw/eslint-config": "^0.0.1", 42 | "@types/async": "^2.0.49", 43 | "@types/cookie-parser": "^1.4.1", 44 | "@types/cors": "^2.8.4", 45 | "@types/express": "^4.16.0", 46 | "@types/jsdom": "^12.2.1", 47 | "@types/jsonwebtoken": "^7.2.8", 48 | "@types/lodash": "^4.14.137", 49 | "@types/node": "^10.12.18", 50 | "@types/randomcolor": "^0.5.0", 51 | "@types/socket.io": "^2.1.0", 52 | "@types/ssh2": "^0.5.36", 53 | "@typescript-eslint/eslint-plugin": "^4.3.0", 54 | "@typescript-eslint/parser": "^4.3.0", 55 | "eslint": "^7.10.0", 56 | "eslint-plugin-prettier": "^3.1.3", 57 | "prettier": "^2.1.2", 58 | "quill-delta": "^4.1.0", 59 | "ts-node": "^9.0.0", 60 | "typescript": "^4.0.3" 61 | }, 62 | "gitSHA": "a28155c13fcb9b682c9ef64318233d1b93b9b234", 63 | "resolutions": { 64 | "**/katex": "^0.11.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cshub-server/src/auth/AuthMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request } from "express"; 2 | import dayjs from "dayjs"; 3 | 4 | import { IJWTToken } from "../../../cshub-shared/src/models"; 5 | 6 | import { sign, validateAccessToken } from "./JWTHandler"; 7 | import { Settings } from "../settings"; 8 | import { logMiddleware } from "../utilities/LoggingMiddleware"; 9 | 10 | export function addAuthMiddleware(app: Application): void { 11 | app.use((req, res, next) => { 12 | const tokenValidity = checkTokenValidityFromRequest(req); 13 | 14 | if (tokenValidity) { 15 | const newtoken: string = sign(tokenValidity.user); 16 | 17 | res.cookie("token", newtoken, { 18 | maxAge: Settings.TOKENAGEMILLISECONDS, 19 | domain: Settings.DOMAIN, 20 | }); 21 | 22 | logMiddleware(req, tokenValidity); 23 | } else { 24 | logMiddleware(req, null); 25 | res.clearCookie("token"); 26 | } 27 | 28 | next(); 29 | }); 30 | } 31 | 32 | export type ValidationType = false | IJWTToken; 33 | 34 | export const checkTokenValidityFromJWT = (jwt: string): ValidationType => { 35 | if (!jwt) { 36 | return false; 37 | } 38 | 39 | // This checks the incoming JWT token, validates it, checks if it's still valid. 40 | // If valid, create a new one (so no cookie stealing) 41 | // If invalid, remove the cookie 42 | const tokenObj = validateAccessToken(jwt); 43 | 44 | if ( 45 | tokenObj !== undefined && 46 | dayjs(tokenObj.expirydate * 1000).isAfter(dayjs()) && 47 | tokenObj.user.verified && 48 | !tokenObj.user.blocked 49 | ) { 50 | return tokenObj; 51 | } else { 52 | return false; 53 | } 54 | }; 55 | 56 | export const checkTokenValidityFromRequest = (req: Request): ValidationType => { 57 | if (req.cookies === null) { 58 | return false; 59 | } 60 | 61 | const cookie = req.cookies["token"]; 62 | if (cookie === undefined) { 63 | return false; 64 | } 65 | 66 | return checkTokenValidityFromJWT(cookie); 67 | }; 68 | -------------------------------------------------------------------------------- /cshub-server/src/auth/HashPassword.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { Settings } from "../settings"; 3 | 4 | export const hashPassword = (input: string): Promise => { 5 | return new Promise((resolve, reject) => { 6 | crypto.pbkdf2( 7 | input, 8 | Settings.PASSWORDSALT, 9 | Settings.PASSWORDITERATIONS, 10 | 64, 11 | "sha512", 12 | (err: Error | null, derivedKey: Buffer) => { 13 | if (err !== null) { 14 | reject(); 15 | } 16 | resolve(derivedKey.toString("hex")); 17 | }, 18 | ); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /cshub-server/src/auth/JWTHandler.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import dayjs from "dayjs"; 3 | 4 | import { IJWTToken } from "../../../cshub-shared/src/models"; 5 | import { IUser } from "../../../cshub-shared/src/entities/user"; 6 | 7 | import { Settings } from "../settings"; 8 | 9 | // Sign the object, add the expirydate of 2 hours and then convert to unix timeformat 10 | export const sign = (obj: IUser): string => { 11 | const newObj: IUser = JSON.parse(JSON.stringify(obj)); 12 | newObj.avatar = ""; 13 | 14 | const jwtobj: IJWTToken = { 15 | user: newObj, 16 | expirydate: dayjs().add(Settings.TOKENAGEMILLISECONDS, "millisecond").unix(), 17 | }; 18 | 19 | return jwt.sign(jwtobj, Settings.JWTHASH); 20 | }; 21 | 22 | export const validateAccessToken = (accessToken: string): IJWTToken | undefined => { 23 | try { 24 | return jwt.verify(accessToken, Settings.JWTHASH) as IJWTToken; 25 | } catch (e) { 26 | // console.warn('Dropping unverified accessToken', e); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /cshub-server/src/auth/validateRights/PostAccess.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseResultSet, query } from "../../db/database-query"; 2 | import { checkTokenValidityFromJWT } from "../AuthMiddleware"; 3 | import { Request } from "express"; 4 | import { getStudiesFromTopic } from "../../utilities/TopicsUtils"; 5 | 6 | export interface PostAccessType { 7 | canEdit: boolean; 8 | canSave: boolean; 9 | } 10 | 11 | export const hasAccessToTopicRequest = (topicHash: number, req: Request): Promise => { 12 | if (req.cookies === null) { 13 | return Promise.resolve({ canEdit: false, canSave: false }); 14 | } 15 | 16 | return hasAccessToTopicJWT(topicHash, req.cookies["token"]); 17 | }; 18 | 19 | export const hasAccessToTopicJWT = (topicHash: number, jwt: string): Promise => { 20 | const tokenResult = checkTokenValidityFromJWT(jwt); 21 | 22 | if (!tokenResult) { 23 | return Promise.resolve({ canEdit: false, canSave: false }); 24 | } 25 | 26 | // Check if user is global admin 27 | if (tokenResult.user.admin) { 28 | return Promise.resolve({ canEdit: true, canSave: true }); 29 | } 30 | 31 | // Check if user is study admin 32 | return getStudiesFromTopic(topicHash).then((studies) => { 33 | for (const study of studies) { 34 | const isStudyAdmin = tokenResult.user.studies.map((currStudy) => currStudy.id).includes(study.id); 35 | if (isStudyAdmin) { 36 | return { canEdit: true, canSave: true }; 37 | } 38 | } 39 | 40 | return { canEdit: true, canSave: false }; 41 | }); 42 | }; 43 | 44 | export const hasAccessToPostRequest = (postHash: number, req: Request): Promise => { 45 | if (req.cookies === null) { 46 | return Promise.resolve({ canEdit: false, canSave: false }); 47 | } 48 | 49 | return hasAccessToPostJWT(postHash, req.cookies["token"]); 50 | }; 51 | 52 | // Test whether the user has enough rights to access this post 53 | // A (study) admin has the ability to save 54 | export const hasAccessToPostJWT = (postHash: number, jwt: string): Promise => { 55 | return query( 56 | ` 57 | SELECT deleted, t.hash 58 | FROM posts p 59 | INNER JOIN topics t on p.topic = t.id 60 | WHERE p.hash = ? 61 | `, 62 | postHash, 63 | ).then((databaseResult: DatabaseResultSet) => { 64 | if (databaseResult.getNumberFromDB("deleted") === 1) { 65 | return { canEdit: false, canSave: false }; 66 | } 67 | 68 | return hasAccessToTopicJWT(databaseResult.getNumberFromDB("hash"), jwt); 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/cacheversion.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; 2 | 3 | @Entity({ 4 | name: "cacheversion", 5 | }) 6 | export class CacheVersion { 7 | @PrimaryGeneratedColumn() 8 | id!: number; 9 | 10 | @Column({ 11 | type: "text", 12 | }) 13 | type!: string; 14 | 15 | @Column() 16 | version!: number; 17 | } 18 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/edit.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; 2 | import { User } from "./user"; 3 | import { Post } from "./post"; 4 | import { IEdit } from "../../../../cshub-shared/src/entities/edit"; 5 | 6 | import Delta from "quill-delta"; 7 | import { Exclude, Expose } from "class-transformer"; 8 | 9 | @Exclude() 10 | @Entity({ 11 | name: "edits", 12 | }) 13 | export class Edit implements IEdit { 14 | @Expose() 15 | @PrimaryGeneratedColumn() 16 | id!: number; 17 | 18 | @Expose() 19 | @ManyToMany((type) => User, (user) => user.edits) 20 | @JoinTable({ name: "editusers" }) 21 | editusers!: User[]; 22 | 23 | @Expose() 24 | @Column({ 25 | type: "longtext", 26 | }) 27 | content!: Delta; 28 | 29 | @Expose() 30 | @Column({ 31 | type: "int", // Otherwise it overrides the value 32 | default: false, 33 | }) 34 | approved!: boolean; 35 | 36 | @Expose() 37 | @Column({ 38 | type: "datetime", 39 | default: () => "CURRENT_TIMESTAMP", 40 | }) 41 | @Index() 42 | datetime!: Date; 43 | 44 | @Expose() 45 | @Column({ 46 | type: "longtext", 47 | nullable: true, 48 | }) 49 | htmlContent!: string; 50 | 51 | // Not sent to client 52 | @ManyToOne((type) => Post, (post) => post.id) 53 | @JoinColumn({ name: "post" }) 54 | @Index() 55 | post?: Post; 56 | 57 | @Column({ 58 | type: "longtext", 59 | nullable: true, 60 | }) 61 | indexwords?: string; 62 | } 63 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/emaildomain.ts: -------------------------------------------------------------------------------- 1 | import { Exclude, Expose } from "class-transformer"; 2 | import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; 3 | import { IEmailDomain } from "../../../../cshub-shared/src/entities/emaildomains"; 4 | import { User } from "./user"; 5 | 6 | @Exclude() 7 | @Entity({ 8 | name: "emaildomains", 9 | }) 10 | export class EmailDomain implements IEmailDomain { 11 | @Expose() 12 | @PrimaryGeneratedColumn() 13 | id!: number; 14 | 15 | @Expose() 16 | @Column({ 17 | type: "varchar", 18 | length: 64, 19 | unique: true, 20 | }) 21 | domain!: string; 22 | 23 | @OneToMany((type) => User, (user) => user.domain) 24 | users?: User[]; 25 | } 26 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/post.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; 2 | import { Topic } from "./topic"; 3 | import { IPost } from "../../../../cshub-shared/src/entities/post"; 4 | import { Exclude, Expose } from "class-transformer"; 5 | 6 | @Exclude() 7 | @Entity({ 8 | name: "posts", 9 | }) 10 | @Index("uq_title_topic", ["title", "topic"], { 11 | unique: true, 12 | }) 13 | export class Post implements IPost { 14 | @Expose() 15 | @PrimaryGeneratedColumn() 16 | id!: number; 17 | 18 | @Expose() 19 | @ManyToOne((type) => Topic, (topic) => topic.posts, { 20 | nullable: false, 21 | }) 22 | @JoinColumn({ name: "topic" }) 23 | @Index() 24 | topic!: Topic; 25 | 26 | @Expose() 27 | @Column({ 28 | type: "datetime", 29 | default: () => "CURRENT_TIMESTAMP", 30 | }) 31 | datetime!: Date; 32 | 33 | @Expose() 34 | @Column({ 35 | type: "varchar", 36 | length: 127, 37 | }) 38 | title!: string; 39 | 40 | @Expose() 41 | @Column({ 42 | unique: true, 43 | }) 44 | hash!: number; 45 | 46 | @Expose() 47 | @Column({ 48 | default: 0, 49 | }) 50 | @Index() 51 | postVersion!: number; 52 | 53 | @Expose() 54 | @Column({ 55 | type: "int", // Otherwise it overrides the value 56 | default: false, 57 | }) 58 | @Index() 59 | deleted!: boolean; 60 | 61 | @Expose() 62 | @Column({ 63 | type: "int", // Otherwise it overrides the value 64 | default: true, 65 | }) 66 | @Index() 67 | wip!: boolean; 68 | 69 | @Expose() 70 | @Column({ 71 | type: "int", // Otherwise it overrides the value 72 | default: false, 73 | }) 74 | @Index() 75 | isIndex!: boolean; 76 | 77 | @Expose() 78 | @Column({ 79 | default: false, 80 | }) 81 | @Index() 82 | isExample!: boolean; 83 | } 84 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/practice/closed-answer.ts: -------------------------------------------------------------------------------- 1 | import { Answer } from "./answer"; 2 | 3 | export class ClosedAnswer extends Answer { 4 | constructor(public closedAnswerText: string, public correct: boolean) { 5 | super(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/practice/dynamic-answer.ts: -------------------------------------------------------------------------------- 1 | import { Answer } from "./answer"; 2 | import { Variable } from "./variable"; 3 | 4 | export class DynamicAnswer extends Answer { 5 | constructor(public dynamicAnswerExpression: string, public dynamicAnswerVariables: Variable[]) { 6 | super(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/practice/open-number-answer.ts: -------------------------------------------------------------------------------- 1 | import { Answer } from "./answer"; 2 | 3 | export class OpenNumberAnswer extends Answer { 4 | constructor(public openAnswerNumber: number, public precision: number) { 5 | super(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/practice/open-text-answer.ts: -------------------------------------------------------------------------------- 1 | import { Answer } from "./answer"; 2 | 3 | export class OpenTextAnswer extends Answer { 4 | constructor(public openAnswerText: string) { 5 | super(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/practice/question.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | JoinColumn, 5 | ManyToOne, 6 | OneToMany, 7 | OneToOne, 8 | PrimaryGeneratedColumn, 9 | RelationId, 10 | } from "typeorm"; 11 | import { Topic } from "../topic"; 12 | import { QuestionType } from "../../../../../cshub-shared/src/entities/question"; 13 | import { Exclude, Expose } from "class-transformer"; 14 | import { Answer } from "./answer"; 15 | 16 | @Exclude() 17 | @Entity({ 18 | name: "question", 19 | }) 20 | export class Question { 21 | @Expose() 22 | @PrimaryGeneratedColumn() 23 | id!: number; 24 | 25 | @Expose() 26 | @Column({ 27 | type: "text", 28 | }) 29 | question!: string; 30 | 31 | @Expose() 32 | @Column({ 33 | type: "text", 34 | }) 35 | type!: QuestionType; 36 | 37 | @Expose() 38 | @OneToMany((type) => Answer, (answer) => answer.question, { 39 | nullable: false, 40 | cascade: true, 41 | }) 42 | answers!: Answer[]; 43 | 44 | @Column({ 45 | type: "text", 46 | }) 47 | explanation!: string; 48 | 49 | @ManyToOne((type) => Topic, (topic) => topic.questions, { 50 | nullable: false, 51 | }) 52 | topic!: Topic; 53 | 54 | @RelationId((question: Question) => question.topic) 55 | topicId!: number; 56 | 57 | @Column({ 58 | default: false, 59 | }) 60 | active!: boolean; 61 | 62 | @Column({ 63 | default: false, 64 | }) 65 | deleted!: boolean; 66 | 67 | // not the nicest solution, but it works. This marks which question will be set to inactive if this question is accepted 68 | @OneToOne((type) => Question, (question) => question.replacedByQuestion, { 69 | nullable: true, 70 | }) 71 | @JoinColumn() 72 | replacesQuestion?: Question; 73 | 74 | // not the nicest solution, but it works. This marks which question will be set to inactive if this question is accepted 75 | @OneToOne((type) => Question, (question) => question.replacesQuestion, { 76 | nullable: true, 77 | }) 78 | replacedByQuestion?: Question; 79 | 80 | @RelationId((question: Question) => question.replacesQuestion) 81 | replacesQuestionId?: number; 82 | } 83 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/practice/variable.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; 2 | import { Answer } from "./answer"; 3 | 4 | @Entity({ 5 | name: "variable", 6 | }) 7 | export class Variable { 8 | @PrimaryGeneratedColumn() 9 | id!: number; 10 | 11 | @ManyToOne((type) => Answer, (answer) => answer.dynamicAnswerVariables, { 12 | nullable: false, 13 | onDelete: "RESTRICT", 14 | onUpdate: "RESTRICT", 15 | }) 16 | answer!: Answer; 17 | 18 | @Column({ 19 | nullable: false, 20 | }) 21 | name!: string; 22 | 23 | @Column({ 24 | nullable: false, 25 | }) 26 | expression!: string; 27 | } 28 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/study.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | JoinColumn, 5 | JoinTable, 6 | ManyToMany, 7 | OneToOne, 8 | PrimaryGeneratedColumn, 9 | RelationId, 10 | } from "typeorm"; 11 | import { Topic } from "./topic"; 12 | import { User } from "./user"; 13 | import { IStudy } from "../../../../cshub-shared/src/entities/study"; 14 | import { Exclude, Expose } from "class-transformer"; 15 | 16 | @Exclude() 17 | @Entity({ 18 | name: "studies", 19 | }) 20 | export class Study implements IStudy { 21 | @Expose() 22 | @PrimaryGeneratedColumn() 23 | id!: number; 24 | 25 | @Expose() 26 | @Column({ 27 | type: "text", 28 | }) 29 | name!: string; 30 | 31 | @Expose() 32 | @OneToOne((type) => Topic, (topic) => topic.study, { 33 | nullable: false, 34 | }) 35 | @JoinColumn() 36 | topTopic!: Topic; 37 | 38 | @RelationId((study: Study) => study.topTopic) 39 | topTopicId!: number; 40 | 41 | // Not sent to client 42 | @ManyToMany((type) => User, (user) => user.studies) 43 | @JoinTable() 44 | admins?: User[]; 45 | 46 | @Expose() 47 | @Column({ 48 | type: "boolean", 49 | default: false, 50 | }) 51 | hidden!: boolean; 52 | } 53 | -------------------------------------------------------------------------------- /cshub-server/src/db/entities/topic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | JoinColumn, 5 | ManyToOne, 6 | OneToMany, 7 | OneToOne, 8 | PrimaryGeneratedColumn, 9 | RelationId, 10 | } from "typeorm"; 11 | import { Post } from "./post"; 12 | import { Study } from "./study"; 13 | import { ITopic } from "../../../../cshub-shared/src/entities/topic"; 14 | import { Question } from "./practice/question"; 15 | import { Exclude, Expose } from "class-transformer"; 16 | 17 | @Exclude() 18 | @Entity({ 19 | name: "topics", 20 | }) 21 | export class Topic implements ITopic { 22 | @Expose() 23 | @PrimaryGeneratedColumn() 24 | id!: number; 25 | 26 | @Expose() 27 | @Column({ 28 | type: "text", 29 | }) 30 | name!: string; 31 | 32 | @Expose() 33 | @ManyToOne((type) => Topic, (topic) => topic.children, { 34 | nullable: true, 35 | onDelete: "RESTRICT", 36 | onUpdate: "RESTRICT", 37 | }) 38 | @JoinColumn({ name: "parentid" }) 39 | parent!: Topic | null; 40 | 41 | @Column({ type: "int", nullable: true }) 42 | parentid!: number | null; 43 | 44 | @Expose() 45 | @OneToMany((type) => Topic, (topic) => topic.parent) 46 | children!: Topic[]; 47 | 48 | @Expose() 49 | @Column({ 50 | unique: true, 51 | }) 52 | hash!: number; 53 | 54 | @OneToOne((type) => Study, (study) => study.topTopic, { 55 | nullable: true, 56 | }) 57 | study?: Study; 58 | 59 | // Not sent to client 60 | @OneToMany((type) => Post, (post) => post.topic) 61 | posts?: Post[]; 62 | 63 | @OneToMany((type) => Question, (question) => question.topic) 64 | questions?: Question[]; 65 | } 66 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "express"; 2 | 3 | import { registerPostEndpoints } from "./post"; 4 | import { registerPostsEndpoints } from "./posts"; 5 | 6 | import { registerPreRenderEndpoint } from "./PreRender"; 7 | import { registerSearchEndpoint } from "./Search"; 8 | import { registerEmailDomainsEndpoints } from "./emaildomains"; 9 | import { registerStudiesEndpoints } from "./study/Studies"; 10 | import { registerTopicsEndpoints } from "./topics"; 11 | import { registerUserEndpoints } from "./user"; 12 | import { registerQuestionEndpoints } from "./question"; 13 | 14 | export function registerEndpoints(app: Application): void { 15 | registerPostEndpoints(app); 16 | registerPostsEndpoints(app); 17 | registerUserEndpoints(app); 18 | registerTopicsEndpoints(app); 19 | registerStudiesEndpoints(app); 20 | registerQuestionEndpoints(app); 21 | 22 | registerEmailDomainsEndpoints(app); 23 | 24 | registerPreRenderEndpoint(app); 25 | registerSearchEndpoint(app); 26 | } 27 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/post/PostData.ts: -------------------------------------------------------------------------------- 1 | import logger from "../../utilities/Logger"; 2 | import { Application, Request, Response } from "express"; 3 | import { PostData, GetPostCallBack } from "../../../../cshub-shared/src/api-calls"; 4 | 5 | import { getRepository } from "typeorm"; 6 | import { Post } from "../../db/entities/post"; 7 | 8 | export function registerPostDataEndpoint(app: Application): void { 9 | app.get(PostData.getURL, (req: Request, res: Response) => { 10 | const hash = Number(req.params.hash); 11 | 12 | // Get all the post data from database 13 | getPostData(hash).then((data) => { 14 | if (data === null) { 15 | res.status(404).send(); 16 | } else { 17 | res.json(data); 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | export const getPostData = (postHash: number): Promise => { 24 | const postRepository = getRepository(Post); 25 | 26 | return postRepository 27 | .findOne({ 28 | relations: ["topic"], 29 | where: { 30 | hash: postHash, 31 | }, 32 | }) 33 | .then((post) => { 34 | if (!post) { 35 | return null; 36 | } 37 | 38 | return new GetPostCallBack(post); 39 | }) 40 | .catch((err) => { 41 | logger.error(`Retreiving post data failed`); 42 | logger.error(err); 43 | return null; 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/post/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "express"; 2 | 3 | import { registerEditContentEndpoint } from "./EditContent"; 4 | import { registerEditPostEndpoint } from "./EditPost"; 5 | import { registerPostContentEndpoint } from "./PostContent"; 6 | import { registerPostDataEndpoint } from "./PostData"; 7 | import { registerPostSettingsEndpoint } from "./PostSettings"; 8 | import { registerSquashEditsEndpoint } from "./SquashEdits"; 9 | import { registerSubmitPostEndpoint } from "./SubmitPost"; 10 | 11 | export function registerPostEndpoints(app: Application): void { 12 | registerEditContentEndpoint(app); 13 | registerEditPostEndpoint(app); 14 | registerPostContentEndpoint(app); 15 | registerPostDataEndpoint(app); 16 | registerPostSettingsEndpoint(app); 17 | registerSquashEditsEndpoint(app); 18 | registerSubmitPostEndpoint(app); 19 | } 20 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/posts/ExamplePosts.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | 3 | import { ExamplePosts } from "../../../../cshub-shared/src/api-calls/endpoints/posts/ExamplePosts"; 4 | import { getPosts } from "./GetPosts"; 5 | import { query } from "../../db/database-query"; 6 | 7 | export function registerExamplePostsEndpoint(app: Application): void { 8 | app.get(ExamplePosts.getURL, (req: Request, res: Response) => { 9 | getPosts(req, res, (topicHashes, currentTopicHash) => { 10 | return query( 11 | ` 12 | SELECT T1.hash 13 | FROM posts T1 14 | INNER JOIN topics T2 ON T1.topic = T2.id 15 | WHERE deleted = 0 16 | AND T1.wip = 0 17 | AND T1.isExample = 1 18 | AND T2.hash IN (?) 19 | ORDER BY T1.isIndex DESC, T1.datetime DESC 20 | `, 21 | topicHashes, 22 | ); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/posts/TopicPosts.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | 3 | import { TopicPosts } from "../../../../cshub-shared/src/api-calls"; 4 | import { getPosts } from "./GetPosts"; 5 | import { query } from "../../db/database-query"; 6 | 7 | export function registerTopicPostsEndpoint(app: Application): void { 8 | app.get(TopicPosts.getURL, (req: Request, res: Response) => { 9 | getPosts(req, res, (topicHashes, currentTopicHash) => { 10 | return query( 11 | ` 12 | SELECT T1.hash 13 | FROM posts T1 14 | INNER JOIN topics T2 ON T1.topic = T2.id 15 | WHERE deleted = 0 16 | AND T1.wip = 0 17 | AND T1.isExample = 0 18 | AND T2.hash IN (?) 19 | AND (T1.isIndex = 0 OR T1.topic = (SELECT id FROM topics WHERE hash = ?)) 20 | ORDER BY T1.isIndex DESC, T1.datetime DESC 21 | `, 22 | topicHashes, 23 | currentTopicHash, 24 | ); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/posts/WIPPosts.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | import { DatabaseResultSet, query } from "../../db/database-query"; 3 | import { GetUnverifiedPostsCallBack, WIPPosts } from "../../../../cshub-shared/src/api-calls"; 4 | import { customValidator } from "../../utilities/StringUtils"; 5 | import { parseStringQuery } from "../../utilities/query-parser"; 6 | 7 | export function registerWIPPostsEndpoint(app: Application): void { 8 | app.get(WIPPosts.getURL, (req: Request, res: Response) => { 9 | const studyIdQuery = parseStringQuery(req, res, WIPPosts.studyQueryParam); 10 | if (!studyIdQuery) return; 11 | const studyId = +studyIdQuery; 12 | 13 | if (!customValidator({ input: studyId }).valid) { 14 | res.sendStatus(400); 15 | return; 16 | } 17 | 18 | // language=MySQL 19 | query( 20 | ` 21 | WITH RECURSIVE studyTopics (id, parentid) AS ( 22 | SELECT t1.id, t1.parentid 23 | FROM topics t1 24 | WHERE id = (SELECT topTopicId FROM studies WHERE id = ?) 25 | 26 | UNION ALL 27 | 28 | SELECT t2.id, t2.parentid 29 | FROM topics t2 30 | INNER JOIN studyTopics ON t2.parentid = studyTopics.id 31 | ) 32 | 33 | SELECT hash 34 | FROM posts T1 35 | WHERE T1.wip = 1 36 | AND T1.deleted = 0 37 | AND T1.topic IN (SELECT id FROM studyTopics) 38 | ORDER BY T1.datetime DESC 39 | `, 40 | studyId, 41 | ).then((result: DatabaseResultSet) => { 42 | const hashes: number[] = []; 43 | 44 | result.convertRowsToResultObjects().forEach((x) => { 45 | hashes.push(x.getNumberFromDB("hash")); 46 | }); 47 | 48 | res.json(new GetUnverifiedPostsCallBack(hashes)); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/posts/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "express"; 2 | 3 | import { registerGetUnverifiedPostsEndpoint } from "./GetUnverifiedPosts"; 4 | import { registerWIPPostsEndpoint } from "./WIPPosts"; 5 | import { registerTopicPostsEndpoint } from "./TopicPosts"; 6 | import { registerExamplePostsEndpoint } from "./ExamplePosts"; 7 | 8 | export function registerPostsEndpoints(app: Application): void { 9 | registerGetUnverifiedPostsEndpoint(app); 10 | registerWIPPostsEndpoint(app); 11 | registerTopicPostsEndpoint(app); 12 | registerExamplePostsEndpoint(app); 13 | } 14 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/question/EditQuestion.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | 3 | import { ServerError } from "../../../../cshub-shared/src/models/ServerError"; 4 | import { AddQuestion, EditQuestion } from "../../../../cshub-shared/src/api-calls/endpoints/question"; 5 | import { insertQuestions, validateNewQuestion } from "./QuestionUtils"; 6 | import { AlreadySentError } from "../utils"; 7 | import { checkTokenValidityFromRequest } from "../../auth/AuthMiddleware"; 8 | 9 | export function registerEditQuestionEndpoints(app: Application): void { 10 | app.put(EditQuestion.getURL, (req: Request, res: Response) => { 11 | const editQuestion = req.body as EditQuestion; 12 | 13 | const authorized = checkTokenValidityFromRequest(req); 14 | if (!authorized) { 15 | return res.sendStatus(401); 16 | } 17 | 18 | if (editQuestion.question === null || req.params.id === undefined) { 19 | res.status(400).send(new ServerError("No question!")); 20 | return; 21 | } 22 | 23 | try { 24 | validateNewQuestion(editQuestion.question, res); 25 | insertQuestions( 26 | { 27 | question: editQuestion.question, 28 | originalId: Number(req.params.id), 29 | }, 30 | req, 31 | res, 32 | ); 33 | } catch (err) { 34 | if (!(err instanceof AlreadySentError)) { 35 | res.status(500).send(new ServerError("Server did oopsie")); 36 | } 37 | } 38 | }); 39 | 40 | app.post(AddQuestion.getURL, (req: Request, res: Response) => { 41 | const addQuestions = req.body as AddQuestion; 42 | 43 | const authorized = checkTokenValidityFromRequest(req); 44 | if (!authorized) { 45 | return res.sendStatus(401); 46 | } 47 | 48 | if (!addQuestions.question || !addQuestions.topicHash || isNaN(addQuestions.topicHash)) { 49 | res.status(400).send(new ServerError("Missing properties")); 50 | return; 51 | } 52 | 53 | try { 54 | validateNewQuestion(addQuestions.question, res); 55 | insertQuestions( 56 | { 57 | question: addQuestions.question, 58 | }, 59 | req, 60 | res, 61 | addQuestions.topicHash, 62 | ); 63 | } catch (err) { 64 | if (!(err instanceof AlreadySentError)) { 65 | res.status(500).send(new ServerError("Server did oopsie")); 66 | } 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/question/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "express"; 2 | 3 | import { registerCheckAnswersEndpoint } from "./CheckAnswers"; 4 | import { registerEditQuestionEndpoints } from "./EditQuestion"; 5 | import { registerGetQuestionsEndpoints } from "./GetQuestions"; 6 | import { registerQuestionSettingsEndpoint } from "./QuestionSettings"; 7 | import { registerGetQuestionEndpoints } from "./GetQuestion"; 8 | 9 | export function registerQuestionEndpoints(app: Application): void { 10 | registerCheckAnswersEndpoint(app); 11 | registerEditQuestionEndpoints(app); 12 | registerGetQuestionsEndpoints(app); 13 | registerQuestionSettingsEndpoint(app); 14 | registerGetQuestionEndpoints(app); 15 | } 16 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/topics/Topics.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | 3 | import logger from "../../utilities/Logger"; 4 | 5 | import { Topics, GetTopicsCallBack } from "../../../../cshub-shared/src/api-calls"; 6 | import { getTopicTree } from "../../utilities/TopicsUtils"; 7 | import { getRepository } from "typeorm"; 8 | import { CacheVersion } from "../../db/entities/cacheversion"; 9 | import { Topic } from "../../db/entities/topic"; 10 | 11 | export function registerTopicsEndpoint(app: Application): void { 12 | app.get(Topics.getURL, async (req: Request, res: Response) => { 13 | let version = -1; 14 | const versionHeader = req.header(Topics.topicVersionHeader); 15 | if (versionHeader) { 16 | version = +versionHeader; 17 | logger.info("Received TopicVersion: " + version); 18 | } 19 | 20 | let study: number | undefined = undefined; 21 | const studyQueryParam = req.query[Topics.studyQueryParam]; 22 | if (studyQueryParam) { 23 | study = +studyQueryParam; 24 | logger.info("Received Study: " + study); 25 | } 26 | 27 | const repository = getRepository(CacheVersion); 28 | 29 | const versionData = await repository.findOne({ 30 | where: { 31 | type: "TOPICS", 32 | }, 33 | }); 34 | 35 | const makeJsonifiable = (topic: Topic) => { 36 | topic.parent = null; 37 | 38 | for (const child of topic.children) { 39 | makeJsonifiable(child); 40 | } 41 | }; 42 | 43 | if (!versionData) { 44 | const cacheVersion = new CacheVersion(); 45 | cacheVersion.version = 0; 46 | cacheVersion.type = "TOPICS"; 47 | repository.save(cacheVersion); 48 | } else if (versionData && versionData.version === version) { 49 | res.status(304).send(); // Not Modified 50 | } else { 51 | const topicTree = await getTopicTree(study); 52 | 53 | if (topicTree === null) { 54 | logger.error(`No topics found`); 55 | res.status(500).send(); 56 | } else { 57 | if (topicTree.length > 1) { 58 | logger.error("More than 1 top topic?"); 59 | res.status(500).send(); 60 | return; 61 | } 62 | 63 | const topTopic = topicTree[0]; 64 | 65 | makeJsonifiable(topTopic); 66 | 67 | res.json(new GetTopicsCallBack(versionData ? versionData.version : 0, topTopic)); 68 | } 69 | } 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/topics/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "express"; 2 | 3 | import { registerTopicsEndpoint } from "./Topics"; 4 | import { registerEditTopicsEndpoint } from "./EditTopics"; 5 | 6 | export function registerTopicsEndpoints(app: Application): void { 7 | registerTopicsEndpoint(app); 8 | registerEditTopicsEndpoint(app); 9 | } 10 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/user/AllUsers.ts: -------------------------------------------------------------------------------- 1 | import { AllUsers, AllUsersCallBack } from "../../../../cshub-shared/src/api-calls"; 2 | import { Application, Request, Response } from "express"; 3 | import { checkTokenValidityFromRequest } from "../../auth/AuthMiddleware"; 4 | import { getRepository } from "typeorm"; 5 | import { User } from "../../db/entities/user"; 6 | 7 | export function registerAllUsersEndpoint(app: Application): void { 8 | app.get(AllUsers.getURL, (req: Request, res: Response) => { 9 | const page = Number(req.params.page); 10 | let rowsPerPage = Number(req.query.rowsPerPage); 11 | 12 | const token = checkTokenValidityFromRequest(req); 13 | 14 | // Check if token is valid and user is admin 15 | if (token && token.user.admin) { 16 | rowsPerPage = rowsPerPage === -1 ? 4242424242 : rowsPerPage; 17 | 18 | const userRepository = getRepository(User); 19 | 20 | userRepository 21 | .find({ 22 | relations: ["studies"], 23 | skip: (page - 1) * rowsPerPage, 24 | take: rowsPerPage, 25 | }) 26 | .then((data) => { 27 | userRepository.count().then((countResult) => { 28 | res.json(new AllUsersCallBack(data, countResult)); 29 | }); 30 | }); 31 | } else { 32 | res.sendStatus(403); 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/user/ChangeAvatar.ts: -------------------------------------------------------------------------------- 1 | import { ChangeAvatar, ChangeAvatarCallback } from "../../../../cshub-shared/src/api-calls"; 2 | import { Application, Request, Response } from "express"; 3 | import { checkTokenValidityFromRequest } from "../../auth/AuthMiddleware"; 4 | import sharp from "sharp"; 5 | import { query } from "../../db/database-query"; 6 | 7 | export function registerChangeAvatarEndpoint(app: Application): void { 8 | app.post(ChangeAvatar.getURL, (req: Request, res: Response) => { 9 | const userDashboardChangeAvatarRequest = req.body as ChangeAvatar; 10 | 11 | const token = checkTokenValidityFromRequest(req); 12 | 13 | if (token) { 14 | const base64stripped = userDashboardChangeAvatarRequest.imageb64.replace(/data:image\/(.+);base64,/, ""); 15 | 16 | const imageBuff = Buffer.from(base64stripped, "base64"); 17 | 18 | let bufferData; 19 | 20 | sharp(imageBuff) 21 | .resize({ 22 | width: 100, 23 | }) 24 | .jpeg({ 25 | quality: 40, 26 | }) 27 | .toBuffer() 28 | .then((bufferDataJPG) => { 29 | bufferData = bufferDataJPG; 30 | return query( 31 | ` 32 | UPDATE users 33 | SET avatar = ? 34 | WHERE id = ? 35 | `, 36 | bufferData, 37 | token.user.id, 38 | ); 39 | }) 40 | .then(() => { 41 | res.json(new ChangeAvatarCallback(Buffer.from(bufferData).toString("base64"))); 42 | }) 43 | .catch(() => { 44 | res.status(400).json(new ChangeAvatarCallback(false)); 45 | }); 46 | } else { 47 | res.status(401).send(); 48 | } 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/user/ForgotPassword.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | 3 | import { 4 | ForgotPassword, 5 | ForgotPasswordCallback, 6 | ForgotPasswordResponseTypes, 7 | } from "../../../../cshub-shared/src/api-calls"; 8 | import { validateMultipleInputs } from "../../utilities/StringUtils"; 9 | import { DatabaseResultSet, query } from "../../db/database-query"; 10 | import { hashPassword } from "../../auth/HashPassword"; 11 | 12 | export function registerForgotPasswordEndpoint(app: Application): void { 13 | app.post(ForgotPassword.getURL, (req: Request, res: Response) => { 14 | const forgotPassword = req.body as ForgotPassword; 15 | 16 | const validator = validateMultipleInputs( 17 | { 18 | input: forgotPassword.password, 19 | validationObject: { 20 | minlength: 8, 21 | }, 22 | }, 23 | { input: forgotPassword.accId }, 24 | { input: forgotPassword.hash }, 25 | ); 26 | 27 | // Checking the input, see createaccount for a (bit) more in depth explanation 28 | if (validator.valid) { 29 | query( 30 | ` 31 | SELECT id 32 | FROM users 33 | WHERE id = ? AND passresethash = ? 34 | `, 35 | forgotPassword.accId, 36 | forgotPassword.hash, 37 | ).then((resDatabase: DatabaseResultSet) => { 38 | if (resDatabase.convertRowsToResultObjects().length > 0) { 39 | hashPassword(forgotPassword.password) 40 | .then((hashedValue: string) => { 41 | return query( 42 | ` 43 | UPDATE users 44 | SET password = ?, passresethash = NULL 45 | WHERE id = ? 46 | `, 47 | hashedValue, 48 | forgotPassword.accId, 49 | ); 50 | }) 51 | .then(() => { 52 | res.json(new ForgotPasswordCallback(ForgotPasswordResponseTypes.CHANGED)); 53 | }); 54 | } else { 55 | res.status(400).json(new ForgotPasswordCallback(ForgotPasswordResponseTypes.INVALIDINPUT)); 56 | } 57 | }); 58 | } else { 59 | res.status(400).json(new ForgotPasswordCallback(ForgotPasswordResponseTypes.INVALIDINPUT)); 60 | } 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/user/ForgotPasswordMail.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | 3 | import { 4 | ForgotPasswordMail, 5 | ForgotPasswordMailCallback, 6 | ForgotPasswordMailResponseTypes, 7 | } from "../../../../cshub-shared/src/api-calls"; 8 | import { customValidator } from "../../utilities/StringUtils"; 9 | import { DatabaseResultSet, query } from "../../db/database-query"; 10 | import { sendPasswordResetMail } from "../../utilities/MailConnection"; 11 | 12 | export function registerForgotPasswordMail(app: Application): void { 13 | app.post(ForgotPasswordMail.getURL, (req: Request, res: Response) => { 14 | const forgotPassword = req.body as ForgotPasswordMail; 15 | 16 | // Checking the input, see createaccount for a (bit) more in depth explanation 17 | if (customValidator({ input: forgotPassword.email }).valid) { 18 | query( 19 | ` 20 | SELECT id, firstname 21 | FROM users 22 | WHERE email = ? and domainId = ? 23 | `, 24 | forgotPassword.email, 25 | forgotPassword.domain.id, 26 | ).then((resDatabase: DatabaseResultSet) => { 27 | if (resDatabase.convertRowsToResultObjects().length > 0) { 28 | sendPasswordResetMail( 29 | forgotPassword.email, 30 | resDatabase.getStringFromDB("firstname"), 31 | resDatabase.getNumberFromDB("id"), 32 | ); 33 | res.json(new ForgotPasswordMailCallback(ForgotPasswordMailResponseTypes.SENT)); 34 | } else { 35 | res.status(400).json( 36 | new ForgotPasswordMailCallback(ForgotPasswordMailResponseTypes.EMAILDOESNTEXIST), 37 | ); 38 | } 39 | }); 40 | } else { 41 | res.status(400).json(new ForgotPasswordMailCallback(ForgotPasswordMailResponseTypes.INVALIDINPUT)); 42 | } 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/user/Profile.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | 3 | import { Requests } from "../../../../cshub-shared/src/api-calls"; 4 | import logger from "../../utilities/Logger"; 5 | import { ServerError } from "../../../../cshub-shared/src/models/ServerError"; 6 | import { DatabaseResultSet, query } from "../../db/database-query"; 7 | import * as fs from "fs"; 8 | 9 | export function registerProfileEndpoint(app: Application): void { 10 | app.get(Requests.PROFILE, (req: Request, res: Response) => { 11 | const userId = parseInt(req.params.userId, 10); 12 | 13 | if (!isNaN(userId)) { 14 | query( 15 | ` 16 | SELECT avatar 17 | FROM users 18 | WHERE id = ? 19 | `, 20 | userId, 21 | ).then((result: DatabaseResultSet) => { 22 | if (result.getRows().length === 0) { 23 | logger.error("User doesn't exist"); 24 | res.status(404).send(new ServerError("User does not exist")); 25 | } else { 26 | const avatar = result.getBlobFromDB("avatar"); 27 | 28 | if (avatar !== null) { 29 | res.writeHead(200, { 30 | "Content-Type": "image/png", 31 | "Content-Length": avatar.length, 32 | }); 33 | res.end(avatar); 34 | } else { 35 | fs.readFile(`${__dirname}/defaultAvatar.png`, (err, avatar) => { 36 | res.writeHead(200, { 37 | "Content-Type": "image/png", 38 | "Content-Length": avatar.length, 39 | }); 40 | res.end(avatar); 41 | }); 42 | } 43 | } 44 | }); 45 | } else { 46 | logger.error("Userid not a number"); 47 | res.status(400).send(new ServerError("The userid in the URL is not a number")); 48 | } 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/user/VerifyMail.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | 3 | import logger from "../..//utilities/Logger"; 4 | 5 | import { Requests } from "../../../../cshub-shared/src/api-calls"; 6 | 7 | import { Settings } from "../../settings"; 8 | import { query } from "../../db/database-query"; 9 | import { parseStringQuery } from "../../utilities/query-parser"; 10 | 11 | export function registerVerifyMailEndpoint(app: Application): void { 12 | app.get(Requests.VERIFYMAIL, (req: Request, res: Response) => { 13 | const hashQuery = parseStringQuery(req, res, "hash"); 14 | if (!hashQuery) return; 15 | const hash = +hashQuery; 16 | 17 | const userIDQuery = parseStringQuery(req, res, "accId"); 18 | if (!userIDQuery) return; 19 | const userID = +userIDQuery; 20 | 21 | if (!isNaN(hash) && !isNaN(userID)) { 22 | query( 23 | ` 24 | UPDATE users 25 | SET verified = 1 26 | WHERE verifyhash = ? AND id = ? 27 | `, 28 | hash, 29 | userID, 30 | ) 31 | .then(() => { 32 | res.redirect(`${Settings.SITEPROTOCOL}://${Settings.SITEADDRESS}`); 33 | }) 34 | .catch((err) => { 35 | logger.error("Error while verifying email"); 36 | logger.error(err); 37 | res.status(500).send(); 38 | }); 39 | } else { 40 | logger.error("Error while verifying email, wrong hashes"); 41 | res.status(500).send(); 42 | } 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/user/VerifyToken.ts: -------------------------------------------------------------------------------- 1 | import { Application, Request, Response } from "express"; 2 | 3 | import { VerifyToken, VerifyUserTokenCallback } from "../../../../cshub-shared/src/api-calls"; 4 | 5 | import { checkTokenValidityFromRequest } from "../../auth/AuthMiddleware"; 6 | 7 | export function registerVerifyTokenEndpoint(app: Application): void { 8 | app.get(VerifyToken.getURL, (req: Request, res: Response) => { 9 | const tokenVailidity = checkTokenValidityFromRequest(req); 10 | 11 | if (tokenVailidity) { 12 | res.json(new VerifyUserTokenCallback(tokenVailidity.user)); 13 | } else { 14 | res.json(new VerifyUserTokenCallback(false)); 15 | } 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/user/defaultAvatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobbinBaauw/CSHub/6ba18cc0c35715572cf43676285d939ff3cfe42b/cshub-server/src/endpoints/user/defaultAvatar.png -------------------------------------------------------------------------------- /cshub-server/src/endpoints/user/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "express"; 2 | 3 | import { registerChangeAvatarEndpoint } from "./ChangeAvatar"; 4 | import { registerChangePasswordEndpoint } from "./ChangePassword"; 5 | import { registerCreateAccountEndpoint } from "./CreateAccount"; 6 | import { registerForgotPasswordEndpoint } from "./ForgotPassword"; 7 | import { registerForgotPasswordMail } from "./ForgotPasswordMail"; 8 | import { registerAllUsersEndpoint } from "./AllUsers"; 9 | import { registerLoginEndpoints } from "./Login"; 10 | import { registerProfileEndpoint } from "./Profile"; 11 | import { registerVerifyMailEndpoint } from "./VerifyMail"; 12 | import { registerVerifyTokenEndpoint } from "./VerifyToken"; 13 | import { registerUserAdminEndpoints } from "./UserAdminPage"; 14 | 15 | export function registerUserEndpoints(app: Application): void { 16 | registerChangeAvatarEndpoint(app); 17 | registerChangePasswordEndpoint(app); 18 | registerCreateAccountEndpoint(app); 19 | registerForgotPasswordEndpoint(app); 20 | registerForgotPasswordMail(app); 21 | registerAllUsersEndpoint(app); 22 | registerLoginEndpoints(app); 23 | registerProfileEndpoint(app); 24 | registerVerifyMailEndpoint(app); 25 | registerVerifyTokenEndpoint(app); 26 | registerUserAdminEndpoints(app); 27 | } 28 | -------------------------------------------------------------------------------- /cshub-server/src/endpoints/utils.ts: -------------------------------------------------------------------------------- 1 | import { classToPlain } from "class-transformer"; 2 | import mung from "express-mung"; 3 | import { Application } from "express"; 4 | 5 | export function addClassTransformMiddleware(app: Application): void { 6 | app.use( 7 | mung.json(function transform(body: any) { 8 | return classToPlain(body); 9 | }), 10 | ); 11 | } 12 | 13 | export class AlreadySentError extends Error {} 14 | -------------------------------------------------------------------------------- /cshub-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | 3 | import http from "http"; 4 | import express from "express"; 5 | import bodyParser from "body-parser"; 6 | import cookieParser from "cookie-parser"; 7 | 8 | import { Settings } from "./settings"; 9 | import logger from "./utilities/Logger"; 10 | 11 | import { connectDb } from "./db/orm-connection"; 12 | 13 | import { addAuthMiddleware } from "./auth/AuthMiddleware"; 14 | import { addVersionMiddleware } from "./utilities/VersionMiddleware"; 15 | import { addClassTransformMiddleware } from "./endpoints/utils"; 16 | import { addCorsMiddleware } from "./utilities/CORSMiddleware"; 17 | 18 | import { registerSockets } from "./realtime-edit/socket-receiver"; 19 | import { registerEndpoints } from "./endpoints"; 20 | import { initializeDatabase } from "./init"; 21 | 22 | function createApp() { 23 | const app = express(); 24 | 25 | app.use(bodyParser.urlencoded({ extended: true })); 26 | app.use(bodyParser.json({ limit: "1mb" })); 27 | app.use(cookieParser()); 28 | 29 | addCorsMiddleware(app); 30 | addAuthMiddleware(app); 31 | addVersionMiddleware(app); 32 | addClassTransformMiddleware(app); 33 | 34 | return app; 35 | } 36 | 37 | connectDb().then(async () => { 38 | await initializeDatabase(); 39 | 40 | const app = createApp(); 41 | const server = http.createServer(app).listen(Settings.PORT); 42 | 43 | registerEndpoints(app); 44 | registerSockets(server); 45 | 46 | logger.info("Master started with settings:"); 47 | logger.info(JSON.stringify(Settings)); 48 | }); 49 | -------------------------------------------------------------------------------- /cshub-server/src/realtime-edit/CursorList.ts: -------------------------------------------------------------------------------- 1 | import { IRealtimeSelect } from "../../../cshub-shared/src/api-calls"; 2 | 3 | export class CursorList { 4 | private readonly selectObj: { [postId: number]: IRealtimeSelect[] } = {}; 5 | 6 | public addPost(postHash: number): void { 7 | this.selectObj[postHash] = []; 8 | } 9 | 10 | public getSelectList(postHash: number): IRealtimeSelect[] { 11 | if (Object.prototype.hasOwnProperty.call(this.selectObj, postHash)) { 12 | return this.selectObj[postHash]; 13 | } else { 14 | this.addPost(postHash); 15 | return []; 16 | } 17 | } 18 | 19 | public addPostCursor(select: IRealtimeSelect): number { 20 | const newLength = this.getSelectList(select.postHash).push(select); 21 | return newLength - 1; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cshub-server/src/realtime-edit/index.ts: -------------------------------------------------------------------------------- 1 | import "./socket-receiver"; 2 | -------------------------------------------------------------------------------- /cshub-server/src/utilities/CORSMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "express"; 2 | import { Settings } from "../settings"; 3 | 4 | export function addCorsMiddleware(app: Application): void { 5 | app.use((req, res, next) => { 6 | // Add some headers so we don't have to deal with CORS problems as much 7 | res.header("Access-Control-Allow-Credentials", "true"); 8 | res.header("Access-Control-Allow-Origin", `${Settings.SITEPROTOCOL}://${Settings.SITEADDRESS}`); 9 | res.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); 10 | res.header( 11 | "Access-Control-Allow-Headers", 12 | "X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept, version, x-topic-version, x-post-version, x-exclude-last-edit, x-include-last-edit, x-studies-version", 13 | ); 14 | 15 | next(); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /cshub-server/src/utilities/Logger.ts: -------------------------------------------------------------------------------- 1 | import winston, { format } from "winston"; 2 | import { Settings } from "../settings"; 3 | 4 | const enumerateErrorFormat = format.printf((info) => { 5 | const log = `${info.level}: ${info.message}`; 6 | return info.stack ? `${log}\n${info.stack}` : log; 7 | }); 8 | 9 | const liveJsonFormat = format.combine(format.timestamp(), format.json()); 10 | const debugFormat = format.combine( 11 | winston.format.errors({ stack: true }), 12 | winston.format.cli(), 13 | winston.format.colorize(), 14 | enumerateErrorFormat, 15 | ); 16 | 17 | const usedFormat = Settings.LIVE ? liveJsonFormat : debugFormat; 18 | 19 | const logger = winston.createLogger({ 20 | level: Settings.LOGLEVEL, 21 | format: usedFormat, 22 | transports: [ 23 | new winston.transports.Console({ 24 | format: usedFormat, 25 | handleExceptions: false, 26 | }), 27 | ], 28 | }); 29 | 30 | export default logger; 31 | -------------------------------------------------------------------------------- /cshub-server/src/utilities/LoggingMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { IJWTToken } from "../../../cshub-shared/src/models"; 2 | import { Request } from "express"; 3 | import logger from "./Logger"; 4 | 5 | export const logMiddleware = (req: Request, userObj: IJWTToken | null = null): void => { 6 | if (req.method !== "OPTIONS") { 7 | const userData = userObj !== null ? `, uid: ${userObj.user.id}` : ""; 8 | logger.info(`[${req.headers["x-forwarded-for"] || req.connection.remoteAddress}${userData}] - ${req.path}`); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /cshub-server/src/utilities/VersionMiddleware.ts: -------------------------------------------------------------------------------- 1 | import logger from "../utilities/Logger"; 2 | import { readFileSync } from "fs"; 3 | import { ServerError } from "../../../cshub-shared/src/models/ServerError"; 4 | import { Application } from "express"; 5 | 6 | const SHA = JSON.parse(readFileSync("./package.json").toString())["gitSHA"]; 7 | 8 | export function addVersionMiddleware(app: Application): void { 9 | app.use((req, res, next) => { 10 | const headerVersion = req.header("Version"); 11 | const versionMatch = SHA === headerVersion || typeof headerVersion === "undefined"; 12 | 13 | const msg = versionMatch ? "Versions match" : "Version mismatch"; 14 | 15 | if (!versionMatch) { 16 | logger.info(msg); 17 | res.status(500).send( 18 | new ServerError( 19 | "There was a version mismatch between the server and client, this could mean you run an outdated version, which can be fixed by refreshing / force refreshing", 20 | true, 21 | ), 22 | ); 23 | } else { 24 | next(); 25 | } 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /cshub-server/src/utilities/query-parser.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { ServerError } from "../../../cshub-shared/src/models/ServerError"; 3 | 4 | export function parseStringQuery(req: Request, res: Response, query: string): string | false { 5 | const queryParam = req.query[query]; 6 | if (!queryParam || typeof queryParam !== "string") { 7 | res.send(400).json(new ServerError(`Invalid query param ${query}`)); 8 | return false; 9 | } 10 | return queryParam; 11 | } 12 | -------------------------------------------------------------------------------- /cshub-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "lib": ["es7"], 6 | "outDir": "./dist", 7 | "sourceMap": false, 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true 13 | }, 14 | "include": [ 15 | "src/**/*.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /cshub-shared/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /cshub-shared/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@robbinbaauw"], 3 | rules: { 4 | "@typescript-eslint/explicit-module-boundary-types": [ 5 | "error", 6 | { 7 | allowArgumentsExplicitlyTypedAsAny: true, 8 | }, 9 | ], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /cshub-shared/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /cshub-shared/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build shared module 2 | FROM node:12-alpine 3 | 4 | WORKDIR /app/cshub-shared 5 | 6 | COPY . . 7 | 8 | RUN yarn install && yarn cache clean 9 | -------------------------------------------------------------------------------- /cshub-shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cshub-shared", 3 | "version": "0.3.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "docker-build": "docker build -t cshub-shared -t cshubnl/shared:dev ../cshub-shared && docker build -t cshubnl/client:dev ../cshub-client && docker build -t cshubnl/server:dev ../cshub-server", 7 | "docker-push": "docker push cshubnl/shared:dev && docker push cshubnl/client:dev && docker push cshubnl/server:dev", 8 | "lint": "eslint \"./src/**/*.ts\"", 9 | "lint-fix": "eslint --fix \"./src/**/*.ts\"" 10 | }, 11 | "devDependencies": { 12 | "@types/markdown-it": "^0.0.7", 13 | "@types/mathjs": "^6.0.1", 14 | "@types/quill": "^2.0.1", 15 | "dayjs": "^1.7.7", 16 | "mathjs": "^7.5.1", 17 | "quill-delta": "^4.1.0" 18 | }, 19 | "dependencies": { 20 | "@robbinbaauw/eslint-config": "^0.0.1", 21 | "@typescript-eslint/eslint-plugin": "^4.3.0", 22 | "@typescript-eslint/parser": "^4.3.0", 23 | "eslint": "^7.10.0", 24 | "eslint-plugin-prettier": "^3.1.3", 25 | "prettier": "^2.1.2", 26 | "katex": "^0.11.0", 27 | "markdown-it": "latest", 28 | "markdown-it-katex": "latest", 29 | "mathjs": "^7.5.1", 30 | "parchment": "^1.1.4", 31 | "typescript": "^4.0.3" 32 | }, 33 | "resolutions": { 34 | "**/katex": "0.11.0" 35 | }, 36 | "gitSHA": "a28155c13fcb9b682c9ef64318233d1b93b9b234" 37 | } 38 | -------------------------------------------------------------------------------- /cshub-shared/src/Routes.ts: -------------------------------------------------------------------------------- 1 | export enum Routes { 2 | INDEX = "/", 3 | LOGIN = "/login", 4 | CREATEACCOUNT = "/createaccount", 5 | POST = "/post", 6 | POSTCREATE = "/post/create", 7 | QUESTION = "/question", 8 | TOPIC = "/topic", 9 | USERDASHBOARD = "/user", 10 | ADMINDASHBOARD = "/admin", 11 | UNSAVEDPOSTS = "/unsavedposts", 12 | UNSAVEDQUESTIONS = "/unsavedquestions", 13 | WIPPOSTS = "/wipposts", 14 | SEARCH = "/search", 15 | FORGOTPASSWORD = "/forgotpassword", 16 | } 17 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/SocketRequests.ts: -------------------------------------------------------------------------------- 1 | export class SocketRequests { 2 | public static readonly CLIENTDATAUPDATED: string = "/clientdata"; 3 | public static readonly CLIENTCURSORUPDATED: string = "/clientcursor"; 4 | public static readonly SERVERDATAUPDATED: string = "/serverdata"; 5 | public static readonly SERVERCURSORUPDATED: string = "/servercursorupdated"; 6 | public static readonly TOGGLEPOST: string = "/togglepost"; 7 | } 8 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/Search.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../models"; 2 | import { Requests } from "../Requests"; 3 | 4 | export class GetSearchPostsCallback { 5 | constructor(public hashes: number[]) {} 6 | } 7 | 8 | export class Search implements IApiRequest { 9 | public static getURL: string = Requests.SEARCH; 10 | public URL: string = Search.getURL; 11 | 12 | constructor(query: string, studyNr: number) { 13 | this.URL += `?query=${query}&studyNr=${studyNr}`; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/emaildomains.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../models"; 2 | import { Requests } from "../Requests"; 3 | import { IEmailDomain } from "../../entities/emaildomains"; 4 | 5 | export class GetEmailDomainsCallback { 6 | constructor(public domains: IEmailDomain[]) {} 7 | } 8 | 9 | export class GetEmailDomains implements IApiRequest { 10 | public static getURL: string = Requests.EMAILDOMAINS; 11 | public URL: string = GetEmailDomains.getURL; 12 | 13 | /** 14 | * @see IApiRequest.response 15 | */ 16 | response?: GetEmailDomainsCallback; 17 | } 18 | 19 | export class PutEmailDomains implements IApiRequest { 20 | public static getURL: string = Requests.EMAILDOMAINS; 21 | public URL: string = PutEmailDomains.getURL; 22 | 23 | constructor(public domain: IEmailDomain) {} 24 | } 25 | 26 | export class PostEmailDomainsCallback { 27 | constructor(public domain: IEmailDomain) {} 28 | } 29 | 30 | export class PostEmailDomains implements IApiRequest { 31 | public static getURL: string = Requests.EMAILDOMAINS; 32 | public URL: string = PostEmailDomains.getURL; 33 | 34 | constructor(public domain: string) {} 35 | 36 | /** 37 | * @see IApiRequest.response 38 | */ 39 | response?: PostEmailDomainsCallback; 40 | } 41 | 42 | export class DeleteEmailDomains implements IApiRequest { 43 | public static getURL: string = Requests.EMAILDOMAINS; 44 | public URL: string = PostEmailDomains.getURL; 45 | 46 | constructor(public domainid: number) {} 47 | } 48 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Search"; 2 | 3 | export * from "./post"; 4 | export * from "./posts"; 5 | export * from "./topics"; 6 | export * from "./user"; 7 | export * from "./study"; 8 | export * from "./question"; 9 | export * from "./emaildomains"; 10 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/post/EditContent.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | import { IEdit } from "../../../entities/edit"; 4 | 5 | export class GetEditContentCallback { 6 | constructor(public edits: IEdit[]) {} 7 | } 8 | 9 | export class EditContent implements IApiRequest { 10 | public static getURL: string = Requests.EDITCONTENT; 11 | public URL: string = EditContent.getURL; 12 | 13 | public headers: any = {}; 14 | public static readonly includeLastEditHeader = "X-Include-Last-Edit"; 15 | 16 | constructor(postHash: number, includeLastEdit: boolean) { 17 | this.URL = this.URL.replace(/:hash/, postHash.toString()); 18 | this.headers[EditContent.includeLastEditHeader] = !includeLastEdit; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/post/EditPost.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | 5 | export class EditPost implements IApiRequest { 6 | public static getURL: string = Requests.EDITPOST; 7 | public URL: string = EditPost.getURL; 8 | 9 | constructor(postHash: number, public postTitle: string, public postTopicHash: number, public deleteEdit: boolean) { 10 | this.URL = this.URL.replace(/:hash/, postHash.toString()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/post/PostContent.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | import { IPost } from "../../../entities/post"; 4 | 5 | export enum PostVersionTypes { 6 | UPDATEDPOST, 7 | POSTDELETED, 8 | } 9 | 10 | export class GetPostContentCallBack { 11 | constructor( 12 | public data: 13 | | { 14 | type: PostVersionTypes.UPDATEDPOST; 15 | content: { 16 | html: string; 17 | approved: boolean; 18 | }; 19 | postUpdated: IPost; 20 | } 21 | | { 22 | type: PostVersionTypes.POSTDELETED; 23 | }, 24 | ) {} 25 | } 26 | 27 | export class PostContent implements IApiRequest { 28 | public static getURL: string = Requests.POSTCONTENT; 29 | public URL: string = PostContent.getURL; 30 | 31 | public headers: any = {}; 32 | public static readonly postVersionHeader = "X-Post-Version"; 33 | 34 | constructor(postHash: number, postVersion: number) { 35 | this.URL = this.URL.replace(/:hash/, postHash.toString()); 36 | this.headers[PostContent.postVersionHeader] = postVersion; 37 | } 38 | 39 | /** 40 | * @see IApiRequest.response 41 | */ 42 | response?: GetPostContentCallBack; 43 | } 44 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/post/PostData.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | import { IPost } from "../../../entities/post"; 5 | 6 | export class GetPostCallBack { 7 | constructor(public post: IPost) {} 8 | } 9 | 10 | export class PostData implements IApiRequest { 11 | public static getURL: string = Requests.POSTDATA; 12 | public URL: string = PostData.getURL; 13 | 14 | constructor(postHash: number) { 15 | this.URL = this.URL.replace(/:hash/, postHash.toString()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/post/PostSettings.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | 4 | export enum PostSettingsEditType { 5 | HIDE, 6 | WIP, 7 | } 8 | 9 | export class PostSettings implements IApiRequest { 10 | public static getURL: string = Requests.POSTSETTINGS; 11 | public URL: string = PostSettings.getURL; 12 | 13 | constructor(postHash: number, editType: PostSettingsEditType) { 14 | this.URL = this.URL.replace(/:hash/, postHash.toString()); 15 | this.URL = this.URL.replace(/:action/, PostSettingsEditType[editType].toLowerCase()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/post/SquashEdits.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | 4 | export class SquashEdits implements IApiRequest { 5 | public static getURL: string = Requests.SQUASHEDITS; 6 | public URL: string = SquashEdits.getURL; 7 | 8 | constructor(postHash: number, public editIds: number[]) { 9 | this.URL = this.URL.replace(/:hash/, postHash.toString()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/post/SubmitPost.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | 5 | export enum SubmitPostResponse { 6 | SUCCESS, 7 | TITLEALREADYINUSE, 8 | INVALIDINPUT, 9 | ALREADYHASINDEX, 10 | } 11 | 12 | export class CreatePostCallback { 13 | constructor(public response: SubmitPostResponse, public postHash?: number) {} 14 | } 15 | 16 | export class SubmitPost implements IApiRequest { 17 | public static getURL: string = Requests.SUBMITPOST; 18 | public URL: string = SubmitPost.getURL; 19 | 20 | constructor( 21 | public postTitle: string, 22 | public postTopicHash: number, 23 | public isIndex: boolean, 24 | public isExample: boolean, 25 | ) {} 26 | } 27 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/post/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./EditContent"; 2 | export * from "./EditPost"; 3 | export * from "./PostContent"; 4 | export * from "./PostData"; 5 | export * from "./PostSettings"; 6 | export * from "./SquashEdits"; 7 | export * from "./SubmitPost"; 8 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/posts/ExamplePosts.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | import { PostHashes } from "./TopicPosts"; 4 | 5 | export class ExamplePosts implements IApiRequest { 6 | public static getURL: string = Requests.GETEXAMPLES; 7 | public URL: string = ExamplePosts.getURL; 8 | 9 | constructor(topicHash: number) { 10 | this.URL = this.URL.replace(/:topichash/, topicHash.toString()); 11 | } 12 | 13 | /** 14 | * @see IApiRequest.response 15 | */ 16 | response?: PostHashes; 17 | } 18 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/posts/GetUnverifiedPosts.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | 4 | export class GetUnverifiedPostsCallBack { 5 | constructor(public postHashes: number[]) {} 6 | } 7 | 8 | export class GetUnverifiedPosts implements IApiRequest { 9 | public static getURL: string = Requests.GETUNVERIFIEDPOSTS; 10 | 11 | public static readonly studyQueryParam = "studyNr"; 12 | 13 | public URL: string = GetUnverifiedPosts.getURL; 14 | 15 | public params: { [key: string]: string } = {}; 16 | 17 | constructor(study: number) { 18 | this.params[GetUnverifiedPosts.studyQueryParam] = study.toString(10); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/posts/TopicPosts.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | 5 | export class PostHashes { 6 | constructor(public postHashes: number[]) {} 7 | } 8 | 9 | export class TopicPosts implements IApiRequest { 10 | public static getURL: string = Requests.TOPICPOSTS; 11 | public URL: string = TopicPosts.getURL; 12 | 13 | constructor(topicHash: number) { 14 | this.URL = this.URL.replace(/:topichash/, topicHash.toString()); 15 | } 16 | 17 | /** 18 | * @see IApiRequest.response 19 | */ 20 | response?: PostHashes; 21 | } 22 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/posts/WIPPosts.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | 4 | export class WIPPostsCallBack { 5 | constructor(public postHashes: number[]) {} 6 | } 7 | 8 | export class WIPPosts implements IApiRequest { 9 | public static getURL: string = Requests.WIPPOSTS; 10 | 11 | public static readonly studyQueryParam = "studyNr"; 12 | 13 | public URL: string = WIPPosts.getURL; 14 | 15 | public params: { [key: string]: string } = {}; 16 | 17 | constructor(study: number) { 18 | this.params[WIPPosts.studyQueryParam] = study.toString(10); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/posts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./GetUnverifiedPosts"; 2 | export * from "./TopicPosts"; 3 | export * from "./WIPPosts"; 4 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/CheckAnswers.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | import { CheckedAnswerType, ToCheckAnswerType } from "./models/CheckAnswer"; 4 | 5 | export class CheckAnswersCallback { 6 | constructor(public answers: CheckedAnswerType[]) {} 7 | } 8 | 9 | export class CheckAnswers implements IApiRequest { 10 | public static getURL: string = Requests.CHECKANSWERS; 11 | 12 | public URL: string = CheckAnswers.getURL; 13 | 14 | constructor(public answers: ToCheckAnswerType[]) {} 15 | 16 | /** 17 | * @see IApiRequest.response 18 | */ 19 | response?: CheckAnswersCallback; 20 | } 21 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/EditQuestion.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | import { FullQuestion } from "./models/FullQuestion"; 4 | 5 | export class AddQuestion implements IApiRequest { 6 | public static getURL: string = Requests.ADDQUESTIONS; 7 | 8 | public URL: string = AddQuestion.getURL; 9 | 10 | // topicHash is used here instead of topicHash in question, so we only have to check once :) 11 | constructor(public question: FullQuestion, public topicHash: number) {} 12 | } 13 | 14 | export class EditQuestion implements IApiRequest { 15 | public static getURL: string = Requests.EDITQUESTION; 16 | public URL: string = EditQuestion.getURL; 17 | 18 | constructor(public question: FullQuestion, originalId: number) { 19 | this.URL = this.URL.replace(/:id/, originalId.toString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/GetQuestion.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | import { FullQuestionWithId } from "./models/FullQuestion"; 4 | import { PracticeQuestion } from "./models/PracticeQuestion"; 5 | 6 | export class GetFullQuestionCallback { 7 | constructor(public question: FullQuestionWithId) {} 8 | } 9 | 10 | export class GetFullQuestion implements IApiRequest { 11 | public static getURL: string = Requests.FULLQUESTION; 12 | 13 | public URL: string = GetFullQuestion.getURL; 14 | 15 | constructor(questionId: number) { 16 | this.URL = this.URL.replace(/:id/, questionId.toString()); 17 | } 18 | 19 | /** 20 | * @see IApiRequest.response 21 | */ 22 | response?: GetFullQuestionCallback; 23 | } 24 | 25 | export class GetQuestionCallback { 26 | constructor(public question: PracticeQuestion) {} 27 | } 28 | 29 | export class GetQuestion implements IApiRequest { 30 | public static getURL: string = Requests.QUESTION; 31 | 32 | public URL: string = GetQuestion.getURL; 33 | 34 | constructor(questionId: number) { 35 | this.URL = this.URL.replace(/:id/, questionId.toString()); 36 | } 37 | 38 | /** 39 | * @see IApiRequest.response 40 | */ 41 | response?: GetQuestionCallback; 42 | } 43 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/GetQuestions.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | 4 | export class GetQuestionsCallback { 5 | constructor(public questionIds: number[]) {} 6 | } 7 | 8 | export class GetQuestions implements IApiRequest { 9 | public static getURL: string = Requests.QUESTIONS; 10 | 11 | public static readonly topicQueryParam = "topic"; 12 | public static readonly questionAmountQueryParam = "amount"; 13 | 14 | public URL: string = GetQuestions.getURL; 15 | 16 | public params: { [key: string]: string } = {}; 17 | 18 | constructor(topic: number, amount?: number) { 19 | this.params[GetQuestions.topicQueryParam] = topic.toString(10); 20 | 21 | if (amount) { 22 | this.params[GetQuestions.questionAmountQueryParam] = amount.toString(10); 23 | } 24 | } 25 | 26 | /** 27 | * @see IApiRequest.response 28 | */ 29 | response?: GetQuestionsCallback; 30 | } 31 | 32 | export class GetEditableQuestions implements IApiRequest { 33 | public static getURL: string = Requests.EDITABLEQUESTIONS; 34 | 35 | public static readonly topicQueryParam = "topic"; 36 | 37 | public URL: string = GetEditableQuestions.getURL; 38 | 39 | public params: { [key: string]: string } = {}; 40 | 41 | constructor(topic: number) { 42 | this.params[GetEditableQuestions.topicQueryParam] = topic.toString(10); 43 | } 44 | 45 | /** 46 | * @see IApiRequest.response 47 | */ 48 | response?: GetQuestionsCallback; 49 | } 50 | 51 | export class GetUnpublishedQuestions implements IApiRequest { 52 | public static getURL: string = Requests.UNPUBLISHEDQUESTIONS; 53 | public URL: string = GetUnpublishedQuestions.getURL; 54 | 55 | public static readonly studyQueryParam = "study"; 56 | 57 | public params: { [key: string]: string } = {}; 58 | 59 | constructor(study: number) { 60 | this.params[GetUnpublishedQuestions.studyQueryParam] = study.toString(10); 61 | } 62 | 63 | /** 64 | * @see IApiRequest.response 65 | */ 66 | response?: GetQuestionsCallback; 67 | } 68 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/QuestionSettings.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | 4 | export enum QuestionSettingsEditType { 5 | APPROVE, 6 | DELETE, 7 | } 8 | 9 | export class QuestionSettings implements IApiRequest { 10 | public static getURL: string = Requests.QUESTIONSETTINGS; 11 | public URL: string = QuestionSettings.getURL; 12 | 13 | constructor(questionId: number, editType: QuestionSettingsEditType) { 14 | this.URL = this.URL.replace(/:id/, questionId.toString()); 15 | this.URL = this.URL.replace(/:action/, QuestionSettingsEditType[editType].toLowerCase()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CheckAnswers"; 2 | export * from "./EditQuestion"; 3 | export * from "./GetQuestions"; 4 | export * from "./GetQuestion"; 5 | export * from "./QuestionSettings"; 6 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/models/CheckAnswer.ts: -------------------------------------------------------------------------------- 1 | import { QuestionType } from "../../../../entities/question"; 2 | import { VariableValue } from "./Variable"; 3 | 4 | export interface ToCheckAnswerType { 5 | questionId: number; 6 | answer: CheckAnswerType; 7 | } 8 | 9 | export interface CheckedAnswerType extends ToCheckAnswerType { 10 | explanation: string; 11 | correct: boolean | null; 12 | correctAnswer: CheckAnswerType; 13 | } 14 | 15 | export type CheckAnswerType = 16 | | { 17 | type: QuestionType.SINGLECLOSED; 18 | answerId: number; 19 | } 20 | | { 21 | type: QuestionType.MULTICLOSED; 22 | answerIds: number[]; 23 | } 24 | | { 25 | type: QuestionType.OPENNUMBER; 26 | number: number; 27 | } 28 | | { 29 | type: QuestionType.OPENTEXT; 30 | text: string; 31 | } 32 | | { 33 | type: QuestionType.DYNAMIC; 34 | answer: number | string; 35 | variables: VariableValue[]; 36 | }; 37 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/models/FullQuestion.ts: -------------------------------------------------------------------------------- 1 | import { QuestionType } from "../../../../entities/question"; 2 | import { VariableExpression } from "./Variable"; 3 | 4 | export interface FullClosedAnswerType { 5 | answerText: string; 6 | correct: boolean; 7 | answerId: number; 8 | } 9 | 10 | export type FullAnswerType = 11 | | { 12 | type: QuestionType.MULTICLOSED | QuestionType.SINGLECLOSED; 13 | answers: FullClosedAnswerType[]; 14 | } 15 | | { 16 | type: QuestionType.OPENNUMBER; 17 | number: number; 18 | precision: number; 19 | } 20 | | { 21 | type: QuestionType.OPENTEXT; 22 | answer: string; 23 | } 24 | | { 25 | type: QuestionType.DYNAMIC; 26 | answerExpression: string; 27 | variableExpressions: VariableExpression[]; 28 | }; 29 | 30 | export type FullQuestion = { 31 | question: string; 32 | explanation: string; 33 | replacesQuestion?: number; 34 | } & FullAnswerType; 35 | 36 | export type FullQuestionWithId = { id: number } & FullQuestion; 37 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/models/PracticeQuestion.ts: -------------------------------------------------------------------------------- 1 | import { QuestionType } from "../../../../entities/question"; 2 | import { VariableValue } from "./Variable"; 3 | 4 | export type PracticeAnswerType = 5 | | { 6 | type: QuestionType.MULTICLOSED | QuestionType.SINGLECLOSED; 7 | answers: { 8 | answer: string; 9 | id: number; 10 | }[]; 11 | } 12 | | { 13 | type: QuestionType.OPENNUMBER; 14 | precision: number; 15 | } 16 | | { 17 | type: QuestionType.OPENTEXT; 18 | } 19 | | { 20 | type: QuestionType.DYNAMIC; 21 | variables: VariableValue[]; 22 | }; 23 | 24 | export type PracticeQuestion = { 25 | id: number; 26 | question: string; 27 | } & PracticeAnswerType; 28 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/question/models/Variable.ts: -------------------------------------------------------------------------------- 1 | export interface VariableValue { 2 | value: number | string; 3 | name: string; 4 | } 5 | 6 | export interface VariableExpression { 7 | expression: string; 8 | name: string; 9 | } 10 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/study/CreateStudies.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | import { IStudy } from "../../../entities/study"; 5 | 6 | export class CreateStudiesCallback { 7 | constructor(public study: IStudy) {} 8 | } 9 | 10 | export class CreateStudies implements IApiRequest { 11 | public static postURL: string = Requests.CREATESTUDIES; 12 | 13 | public URL: string = CreateStudies.postURL; 14 | 15 | constructor(public name: string, public hidden: boolean) {} 16 | /** 17 | * @see IApiRequest.response 18 | */ 19 | response?: CreateStudiesCallback; 20 | } 21 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/study/HideStudies.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | 5 | export class HideStudies implements IApiRequest { 6 | public static postURL: string = Requests.HIDESTUDIES; 7 | 8 | public URL: string = HideStudies.postURL; 9 | 10 | constructor(studyId: number) { 11 | this.URL = this.URL.replace(/:id/, studyId.toString()); 12 | } 13 | } 14 | 15 | export class UnhideStudies implements IApiRequest { 16 | public static postURL: string = Requests.UNHIDESTUDIES; 17 | 18 | public URL: string = UnhideStudies.postURL; 19 | 20 | constructor(studyId: number) { 21 | this.URL = this.URL.replace(/:id/, studyId.toString()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/study/RenameStudies.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | 4 | export class RenameStudies implements IApiRequest { 5 | public static postURL: string = Requests.RENAMESTUDIES; 6 | 7 | public URL: string = RenameStudies.postURL; 8 | 9 | constructor(studyId: number, public newName: string) { 10 | this.URL = this.URL.replace(/:id/, studyId.toString()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/study/Studies.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | import { IStudy } from "../../../entities/study"; 5 | 6 | export class GetStudiesCallback { 7 | constructor(public version: number, public studies: IStudy[]) {} 8 | } 9 | 10 | export class GetAllStudiesCallback { 11 | constructor(public studies: IStudy[]) {} 12 | } 13 | 14 | export class Studies implements IApiRequest { 15 | public static getURL: string = Requests.GETSTUDIES; 16 | 17 | public static readonly studiesVersionHeader = "X-Studies-Version"; 18 | 19 | public URL: string = Studies.getURL; 20 | 21 | public headers: { [key: string]: string } = {}; 22 | 23 | constructor(studyVersion: number) { 24 | this.headers[Studies.studiesVersionHeader] = studyVersion.toString(10); 25 | } 26 | 27 | /** 28 | * @see IApiRequest.response 29 | */ 30 | response?: GetStudiesCallback; 31 | } 32 | 33 | export class AllStudies implements IApiRequest { 34 | public static getURL: string = Requests.GETALLSTUDIES; 35 | 36 | public URL: string = AllStudies.getURL; 37 | 38 | /** 39 | * @see IApiRequest.response 40 | */ 41 | response?: GetStudiesCallback; 42 | } 43 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/study/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CreateStudies"; 2 | export * from "./RenameStudies"; 3 | export * from "./HideStudies"; 4 | export * from "./Studies"; 5 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/topics/EditTopics.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | 4 | export interface TopicOrNew { 5 | id: number | null; 6 | hash: number | null; 7 | parent: TopicOrNew | null; 8 | children: TopicOrNew[]; 9 | name: string; 10 | } 11 | 12 | export class RestructureTopics implements IApiRequest { 13 | public static postURL: string = Requests.RESTRUCTURETOPICS; 14 | 15 | public URL: string = RestructureTopics.postURL; 16 | 17 | constructor(studyId: number, public topTopic: TopicOrNew) { 18 | this.URL = this.URL.replace(/:id/, studyId.toString()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/topics/Topics.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | import { ITopic } from "../../../entities/topic"; 5 | 6 | export class GetTopicsCallBack { 7 | constructor(public version: number, public topTopic: ITopic) {} 8 | } 9 | 10 | export class Topics implements IApiRequest { 11 | public static getURL: string = Requests.TOPICS; 12 | 13 | public static readonly topicVersionHeader = "X-Topic-Version"; 14 | public static readonly studyQueryParam = "studyNr"; 15 | 16 | public URL: string = Topics.getURL; 17 | 18 | public headers: { [key: string]: string } = {}; 19 | public params: { [key: string]: string } = {}; 20 | 21 | constructor(topicVersion: number, study: number) { 22 | this.headers[Topics.topicVersionHeader] = topicVersion.toString(10); 23 | this.params[Topics.studyQueryParam] = study.toString(10); 24 | } 25 | 26 | /** 27 | * @see IApiRequest.response 28 | */ 29 | response?: GetTopicsCallBack; 30 | } 31 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/topics/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Topics"; 2 | export * from "./EditTopics"; 3 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/AllUsers.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | import { IUser } from "../../../entities/user"; 4 | 5 | export class AllUsersCallBack { 6 | constructor(public users: IUser[], public totalItems: number) {} 7 | } 8 | 9 | export class AllUsers implements IApiRequest { 10 | public static getURL: string = Requests.ALLUSERS; 11 | public URL: string = AllUsers.getURL; 12 | 13 | constructor(rowsPerPage: number, page: number) { 14 | this.URL = this.URL.replace(/:page/, page.toString()); 15 | this.URL += "?rowsPerPage=" + rowsPerPage; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/ChangeAvatar.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | 5 | export class ChangeAvatarCallback { 6 | constructor(public response: false | string) {} 7 | } 8 | 9 | export class ChangeAvatar implements IApiRequest { 10 | public static getURL: string = Requests.CHANGEAVATAR; 11 | public URL: string = ChangeAvatar.getURL; 12 | 13 | constructor(public imageb64: string) {} 14 | } 15 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/ChangePassword.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | 5 | export enum ChangePasswordResponseTypes { 6 | INVALIDINPUT, 7 | SUCCESS, 8 | WRONGPASSWORD, 9 | } 10 | 11 | export class ChangePasswordCallback { 12 | constructor(public response: ChangePasswordResponseTypes) {} 13 | } 14 | 15 | export class ChangePassword implements IApiRequest { 16 | public static getURL: string = Requests.CHANGEPASSWORD; 17 | public URL: string = ChangePassword.getURL; 18 | 19 | constructor(public currentPassword: string, public newPassword: string) {} 20 | } 21 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/CreateAccount.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | import { IEmailDomain } from "../../../entities/emaildomains"; 5 | 6 | export enum CreateAccountResponseTypes { 7 | INVALIDINPUT, 8 | SUCCESS, 9 | ALREADYEXISTS, 10 | } 11 | 12 | export class CreateAccountCallBack { 13 | constructor(public response: CreateAccountResponseTypes) {} 14 | } 15 | 16 | export class CreateAccount implements IApiRequest { 17 | public static getURL: string = Requests.CREATEACCOUNT; 18 | public URL: string = CreateAccount.getURL; 19 | 20 | constructor( 21 | public email: string, 22 | public password: string, 23 | public firstname: string, 24 | public lastname: string, 25 | public domain: IEmailDomain, 26 | ) {} 27 | } 28 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/ForgotPassword.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | 5 | export enum ForgotPasswordResponseTypes { 6 | CHANGED, 7 | INVALIDINPUT, 8 | } 9 | 10 | export class ForgotPasswordCallback { 11 | constructor(public response: ForgotPasswordResponseTypes) {} 12 | } 13 | 14 | export class ForgotPassword implements IApiRequest { 15 | public static getURL: string = Requests.FORGOTPASSWORD; 16 | public URL: string = ForgotPassword.getURL; 17 | 18 | constructor(public password: string, public hash: number, public accId: number) {} 19 | } 20 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/ForgotPasswordMail.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | import { IEmailDomain } from "../../../entities/emaildomains"; 5 | 6 | export enum ForgotPasswordMailResponseTypes { 7 | SENT, 8 | EMAILDOESNTEXIST, 9 | INVALIDINPUT, 10 | } 11 | 12 | export class ForgotPasswordMailCallback { 13 | constructor(public response: ForgotPasswordMailResponseTypes) {} 14 | } 15 | 16 | export class ForgotPasswordMail implements IApiRequest { 17 | public static getURL: string = Requests.FORGOTPASSWORDMAIL; 18 | public URL: string = ForgotPasswordMail.getURL; 19 | 20 | constructor(public email: string, public domain: IEmailDomain) {} 21 | } 22 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/Login.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | import { IUser } from "../../../entities/user"; 5 | import { IEmailDomain } from "../../../entities/emaildomains"; 6 | 7 | export enum LoginResponseTypes { 8 | INCORRECTPASS, 9 | ACCOUNTNOTVERIFIED, 10 | ACCOUNTBLOCKED, 11 | SUCCESS, 12 | INVALIDINPUT, 13 | } 14 | 15 | export class LoginCallBack { 16 | constructor(public response: LoginResponseTypes, public userModel?: IUser) {} 17 | } 18 | 19 | export class Login implements IApiRequest { 20 | public static getURL: string = Requests.LOGIN; 21 | public URL: string = Login.getURL; 22 | 23 | constructor(public email: string, public password: string, public domain: IEmailDomain) {} 24 | } 25 | 26 | export class Logout implements IApiRequest { 27 | public static getURL: string = Requests.LOGOUT; 28 | public URL: string = Logout.getURL; 29 | } 30 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/UserAdminPage.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | import { Requests } from "../../Requests"; 3 | import { IUser } from "../../../entities/user"; 4 | import { IStudy } from "../../../entities/study"; 5 | 6 | export class VerifyUser implements IApiRequest { 7 | public static putURL: string = Requests.VERIFYUSER; 8 | public URL: string = VerifyUser.putURL; 9 | 10 | constructor(public user: IUser, public verified: boolean) {} 11 | } 12 | 13 | export class BlockUser implements IApiRequest { 14 | public static putURL: string = Requests.BLOCKUSER; 15 | public URL: string = BlockUser.putURL; 16 | 17 | constructor(public user: IUser, public blocked: boolean) {} 18 | } 19 | 20 | export class SetAdminUser implements IApiRequest { 21 | public static putURL: string = Requests.SETADMINUSER; 22 | public URL: string = SetAdminUser.putURL; 23 | 24 | constructor(public user: IUser, public admin: boolean) {} 25 | } 26 | 27 | export class SetStudyAdminUser implements IApiRequest { 28 | public static putURL: string = Requests.SETSTUDYADMINUSER; 29 | public URL: string = SetStudyAdminUser.putURL; 30 | 31 | constructor(public user: IUser, public studies: IStudy[]) {} 32 | } 33 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/VerifyToken.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "../../../models"; 2 | 3 | import { Requests } from "../../Requests"; 4 | import { IUser } from "../../../entities/user"; 5 | 6 | export class VerifyUserTokenCallback { 7 | constructor(public response: false | IUser) {} 8 | } 9 | 10 | export class VerifyToken implements IApiRequest { 11 | public static getURL: string = Requests.VERIFYTOKEN; 12 | public URL: string = VerifyToken.getURL; 13 | } 14 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/endpoints/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ChangeAvatar"; 2 | export * from "./ChangePassword"; 3 | export * from "./CreateAccount"; 4 | export * from "./ForgotPassword"; 5 | export * from "./ForgotPasswordMail"; 6 | export * from "./AllUsers"; 7 | export * from "./Login"; 8 | export * from "./VerifyToken"; 9 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./endpoints"; 2 | export * from "./realtime-edit"; 3 | export * from "./Requests"; 4 | export * from "./SocketRequests"; 5 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/realtime-edit/ClientCursorUpdated.ts: -------------------------------------------------------------------------------- 1 | import { SocketRequests } from "../SocketRequests"; 2 | import { IRealtimeSelect } from "./IRealtimeSelect"; 3 | 4 | export class ClientCursorUpdated { 5 | public static getURL: string = SocketRequests.CLIENTCURSORUPDATED; 6 | public URL: string = ClientCursorUpdated.getURL; 7 | 8 | constructor(public selection: IRealtimeSelect) {} 9 | } 10 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/realtime-edit/ClientDataUpdated.ts: -------------------------------------------------------------------------------- 1 | import { SocketRequests } from "../SocketRequests"; 2 | import { IRealtimeEdit } from "./IRealtimeEdit"; 3 | 4 | export class ClientDataUpdated { 5 | public static getURL: string = SocketRequests.CLIENTDATAUPDATED; 6 | public URL: string = ClientDataUpdated.getURL; 7 | 8 | constructor(public edit: IRealtimeEdit) {} 9 | } 10 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/realtime-edit/IRealtimeEdit.ts: -------------------------------------------------------------------------------- 1 | import Delta from "quill-delta"; 2 | import { Dayjs } from "dayjs"; 3 | 4 | export interface IRealtimeEdit { 5 | postHash: number; 6 | userId?: number; 7 | timestamp: Dayjs | null; 8 | delta?: Delta; 9 | prevServerGeneratedId?: number; 10 | serverGeneratedId?: number; 11 | prevUserGeneratedId?: number; 12 | userGeneratedId: number; 13 | } 14 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/realtime-edit/IRealtimeSelect.ts: -------------------------------------------------------------------------------- 1 | import { RangeStatic } from "quill"; 2 | import { IUser } from "../../entities/user"; 3 | 4 | export interface IRealtimeSelect { 5 | color: string; 6 | user: IUser; 7 | userName: string; 8 | postHash: number; 9 | active: boolean; 10 | selection: RangeStatic; 11 | } 12 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/realtime-edit/ServerCursorUpdated.ts: -------------------------------------------------------------------------------- 1 | import { SocketRequests } from "../SocketRequests"; 2 | import { IRealtimeSelect } from "./IRealtimeSelect"; 3 | 4 | export class ServerCursorUpdated { 5 | public static getURL: string = SocketRequests.SERVERCURSORUPDATED; 6 | public URL: string = ServerCursorUpdated.getURL; 7 | 8 | constructor(public select: IRealtimeSelect, public callback: () => void) {} 9 | } 10 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/realtime-edit/ServerDataUpdated.ts: -------------------------------------------------------------------------------- 1 | import { SocketRequests } from "../SocketRequests"; 2 | import { IRealtimeEdit } from "./IRealtimeEdit"; 3 | 4 | export class ServerDataUpdated { 5 | public static getURL: string = SocketRequests.SERVERDATAUPDATED; 6 | public URL: string = ServerDataUpdated.getURL; 7 | 8 | constructor( 9 | public editOrError: 10 | | { 11 | error: false; 12 | edit: IRealtimeEdit; 13 | } 14 | | { 15 | error: true; 16 | message: string; 17 | }, 18 | ) {} 19 | } 20 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/realtime-edit/TogglePostJoin.ts: -------------------------------------------------------------------------------- 1 | import { ISocketRequest } from "../../models/ISocketRequest"; 2 | import { SocketRequests } from "../SocketRequests"; 3 | import { IRealtimeEdit } from "./IRealtimeEdit"; 4 | import { IRealtimeSelect } from "./IRealtimeSelect"; 5 | 6 | export class TogglePostJoin implements ISocketRequest { 7 | public static getURL: string = SocketRequests.TOGGLEPOST; 8 | public URL: string = TogglePostJoin.getURL; 9 | 10 | constructor( 11 | public postHash: number, 12 | public join: boolean, 13 | public callback: (serverData: IRealtimeEdit, select: IRealtimeSelect[]) => void, 14 | ) {} 15 | } 16 | -------------------------------------------------------------------------------- /cshub-shared/src/api-calls/realtime-edit/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ClientCursorUpdated"; 2 | export * from "./ClientDataUpdated"; 3 | export * from "./IRealtimeEdit"; 4 | export * from "./TogglePostJoin"; 5 | export * from "./ServerDataUpdated"; 6 | export * from "./ServerCursorUpdated"; 7 | export * from "./IRealtimeSelect"; 8 | -------------------------------------------------------------------------------- /cshub-shared/src/entities/edit.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from "./user"; 2 | import Delta from "quill-delta"; 3 | 4 | export interface IEdit { 5 | id: number; 6 | 7 | editusers: IUser[]; 8 | 9 | content: Delta; 10 | 11 | approved: boolean; 12 | 13 | datetime: Date; 14 | 15 | htmlContent: string; 16 | } 17 | -------------------------------------------------------------------------------- /cshub-shared/src/entities/emaildomains.ts: -------------------------------------------------------------------------------- 1 | export interface IEmailDomain { 2 | id: number; 3 | domain: string; 4 | } 5 | -------------------------------------------------------------------------------- /cshub-shared/src/entities/post.ts: -------------------------------------------------------------------------------- 1 | import { ITopic } from "./topic"; 2 | 3 | export interface IPost { 4 | id: number; 5 | 6 | topic: ITopic; 7 | 8 | datetime: Date; 9 | 10 | title: string; 11 | 12 | hash: number; 13 | 14 | postVersion: number; 15 | 16 | deleted: boolean; 17 | 18 | wip: boolean; 19 | 20 | isIndex: boolean; 21 | 22 | isExample: boolean; 23 | 24 | htmlContent?: string; 25 | } 26 | -------------------------------------------------------------------------------- /cshub-shared/src/entities/question.ts: -------------------------------------------------------------------------------- 1 | // If: 2 | // - multiple choice: has a list of answers (multiple can be correct) 3 | // - open (number): has only a single answer, which will be checked 4 | // - open (string): has only a single answer, which won't be checked 5 | export enum QuestionType { 6 | SINGLECLOSED = "CLOSED", 7 | MULTICLOSED = "MULTICLOSED", 8 | OPENNUMBER = "OPENNUMBER", 9 | OPENTEXT = "OPENTEXT", 10 | DYNAMIC = "DYNAMIC", 11 | } 12 | -------------------------------------------------------------------------------- /cshub-shared/src/entities/study.ts: -------------------------------------------------------------------------------- 1 | import { ITopic } from "./topic"; 2 | 3 | export interface IStudy { 4 | id: number; 5 | 6 | name: string; 7 | 8 | topTopic: ITopic; 9 | 10 | hidden: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /cshub-shared/src/entities/topic.ts: -------------------------------------------------------------------------------- 1 | export interface ITopic { 2 | id: number; 3 | 4 | parent: ITopic | null; 5 | 6 | children: ITopic[]; 7 | 8 | name: string; 9 | 10 | hash: number; 11 | } 12 | -------------------------------------------------------------------------------- /cshub-shared/src/entities/user.ts: -------------------------------------------------------------------------------- 1 | import { IStudy } from "./study"; 2 | import { IEmailDomain } from "./emaildomains"; 3 | 4 | export interface IUser { 5 | id: number; 6 | 7 | email: string; 8 | 9 | avatar: string; 10 | 11 | admin: boolean; 12 | 13 | blocked: boolean; 14 | 15 | verified: boolean; 16 | 17 | firstname: string; 18 | 19 | lastname: string; 20 | 21 | studies: IStudy[]; 22 | 23 | domain: IEmailDomain; 24 | } 25 | -------------------------------------------------------------------------------- /cshub-shared/src/models/IApiRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IApiRequest { 2 | URL: string; 3 | headers?: { [key: string]: string }; 4 | params?: { [key: string]: string }; 5 | 6 | /** 7 | * Doesn't need to be set: see https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-type-inference-work-on-this-interface-interface-foot--- 8 | * Typescript limitation :( 9 | * This might fix it https://github.com/microsoft/TypeScript/issues/30134 10 | */ 11 | response?: RESPONSETYPE; 12 | } 13 | -------------------------------------------------------------------------------- /cshub-shared/src/models/IJWTToken.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from "../entities/user"; 2 | 3 | export interface IJWTToken { 4 | user: IUser; 5 | expirydate: number; 6 | } 7 | -------------------------------------------------------------------------------- /cshub-shared/src/models/ISocketRequest.ts: -------------------------------------------------------------------------------- 1 | import { IApiRequest } from "./IApiRequest"; 2 | 3 | export interface ISocketRequest extends IApiRequest { 4 | callback?: (...params: any[]) => void; 5 | } 6 | -------------------------------------------------------------------------------- /cshub-shared/src/models/ServerError.ts: -------------------------------------------------------------------------------- 1 | export const INPUTINVALID = 2 | "Your input was invalid. This shouldn't have happened as all validation is also done client side, you cheatin there ;)? If not, we're sorry, something is wrong"; 3 | 4 | export class ServerError { 5 | constructor(public message: string, public showRefresh = false) {} 6 | } 7 | -------------------------------------------------------------------------------- /cshub-shared/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./IApiRequest"; 2 | export * from "./IJWTToken"; 3 | export * from "./ISocketRequest"; 4 | -------------------------------------------------------------------------------- /cshub-shared/src/shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module "markdown-it-katex" { 2 | import MarkdownIt from "markdown-it"; 3 | function mk(md: MarkdownIt, ...params: any[]): void; 4 | export = mk; 5 | } 6 | -------------------------------------------------------------------------------- /cshub-shared/src/utilities/DeltaHandler.ts: -------------------------------------------------------------------------------- 1 | import { IRealtimeEdit } from "../api-calls/realtime-edit"; 2 | import Delta from "quill-delta"; 3 | 4 | /** 5 | * 6 | * @param inputEdits the array where the last few edits are stored 7 | * @param newEdit the new edit 8 | * @param countUserDeltas whether to include the user's edits in the edit 9 | */ 10 | export const transformFromArray = ( 11 | inputEdits: IRealtimeEdit[], 12 | newEdit: IRealtimeEdit, 13 | countUserDeltas: boolean, 14 | ): Delta | undefined => { 15 | const toBeTransformed: IRealtimeEdit[] = []; 16 | for (let i = inputEdits.length - 1; i >= 0; i--) { 17 | const iRealtimeEdit = inputEdits[i]; 18 | if (iRealtimeEdit.serverGeneratedId === newEdit.prevServerGeneratedId) { 19 | break; 20 | } else if (countUserDeltas || iRealtimeEdit.userId !== newEdit.userId) { 21 | toBeTransformed.push(iRealtimeEdit); 22 | } 23 | } 24 | 25 | toBeTransformed.reverse(); 26 | 27 | let editDelta: Delta | undefined = undefined; 28 | 29 | for (const transformable of toBeTransformed) { 30 | if (!editDelta) { 31 | editDelta = new Delta(transformable.delta); 32 | } else { 33 | editDelta = editDelta.compose(new Delta(transformable.delta)); 34 | } 35 | } 36 | 37 | if (editDelta && newEdit.delta) { 38 | // Quill priority works the other way around 39 | return editDelta.transform(newEdit.delta, true); 40 | } else { 41 | return newEdit.delta; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /cshub-shared/src/utilities/MarkdownLatexQuill.ts: -------------------------------------------------------------------------------- 1 | import { BlotConstructor } from "parchment/dist/src/registry"; 2 | 3 | import mk from "markdown-it-katex"; 4 | import MarkdownIt from "markdown-it"; 5 | 6 | export class MarkdownLatexQuill { 7 | public static blotName = "mklqx"; 8 | 9 | private MarkdownLatexQuillExt: any; 10 | 11 | constructor(private quillObj: any) {} 12 | 13 | public registerQuill = (): void => { 14 | const Block: BlotConstructor = this.quillObj.import("blots/block"); 15 | 16 | this.MarkdownLatexQuillExt = class extends Block {}; 17 | 18 | this.MarkdownLatexQuillExt.blotName = MarkdownLatexQuill.blotName; 19 | 20 | this.MarkdownLatexQuillExt.className = MarkdownLatexQuill.blotName; 21 | 22 | this.MarkdownLatexQuillExt.tagName = "PRE"; 23 | 24 | this.MarkdownLatexQuillExt.allowedChildren = (Block as any).allowedChildren; 25 | 26 | this.quillObj.register(this.MarkdownLatexQuillExt); 27 | }; 28 | } 29 | 30 | export const getMarkdownParser = (): MarkdownIt => { 31 | return new MarkdownIt({ 32 | highlight: (str: string, lang: string) => { 33 | if (lang.length === 0) { 34 | lang = "null"; 35 | } 36 | return `
${str}
`; 37 | }, 38 | }).use(mk); 39 | }; 40 | -------------------------------------------------------------------------------- /cshub-shared/src/utilities/QuillDefaultOptions.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | theme: "snow", 3 | placeholder: "Type here ...", 4 | modules: { 5 | cursors: true, 6 | resize: {}, 7 | keyboard: { 8 | bindings: { 9 | "list autofill": { 10 | prefix: /^\s*?(\d+\.|-|\[ ?]|\[x])$/, 11 | }, 12 | }, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /cshub-shared/src/utilities/Random.ts: -------------------------------------------------------------------------------- 1 | export const getRandomNumberLarge = (): number => { 2 | return Math.floor(Math.random() * (999999999 - 100000001)) + 100000000; 3 | }; 4 | -------------------------------------------------------------------------------- /cshub-shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "experimentalDecorators": true, 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "outDir": "./dist", 15 | "baseUrl": ".", 16 | "lib": [ 17 | "esnext", 18 | "dom", 19 | "dom.iterable", 20 | "scripthost" 21 | ] 22 | }, 23 | "include": [ 24 | "src/**/*.ts", 25 | "src/**/*.tsx", 26 | "tests/**/*.ts", 27 | "tests/**/*.tsx" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /deployment/application/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | client: 4 | container_name: ${CLIENT_CONTAINER_NAME} 5 | image: cshubnl/client:${BRANCH} 6 | networks: 7 | - web 8 | labels: 9 | - traefik.frontend.rule=Host:${CLIENT_HOSTNAME},www.${CLIENT_HOSTNAME} 10 | - traefik.frontend.redirect.regex=http(s*)://www.(.+) 11 | - traefik.frontend.redirect.replacement=http$$1://$$2 12 | - traefik.frontend.redirect.permanent=true 13 | - traefik.frontend.port=${CLIENT_PORT_INTERNAL} 14 | environment: 15 | - VUE_APP_API_URL=${CLIENT_ENV_API_URL} 16 | ports: 17 | - ${CLIENT_PORT_EXTERNAL}:${CLIENT_PORT_INTERNAL} 18 | volumes: 19 | - ./nginx.conf:/etc/nginx/nginx.conf 20 | server: 21 | container_name: ${SERVER_CONTAINER_NAME} 22 | image: cshubnl/server:${BRANCH} 23 | env_file: ${SERVER_ENV_PATH} 24 | networks: 25 | - web 26 | labels: 27 | - traefik.frontend.rule=Host:${SERVER_HOSTNAME} 28 | - traefik.frontend.port=${SERVER_PORT_INTERNAL} 29 | ports: 30 | - ${SERVER_PORT_EXTERNAL}:${SERVER_PORT_INTERNAL} 31 | restart: unless-stopped 32 | 33 | networks: 34 | web: 35 | external: true 36 | -------------------------------------------------------------------------------- /deployment/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker build -t cshub-shared -t cshubnl/shared:dev ../cshub-shared 3 | docker build -t cshubnl/client:dev ../cshub-client 4 | docker build -t cshubnl/server:dev ../cshub-server 5 | 6 | docker push cshubnl/shared:dev 7 | docker push cshubnl/client:dev 8 | docker push cshubnl/server:dev 9 | -------------------------------------------------------------------------------- /deployment/dev/.env_example: -------------------------------------------------------------------------------- 1 | BRANCH=dev 2 | 3 | CLIENT_CONTAINER_NAME=cshub-dev-client 4 | CLIENT_HOSTNAME=dev.cshub.nl 5 | CLIENT_PORT_INTERNAL=80 6 | CLIENT_PORT_EXTERNAL=8080 7 | CLIENT_ENV_API_URL=https://api-dev.cshub.nl 8 | 9 | SERVER_CONTAINER_NAME=cshub-dev-server 10 | SERVER_HOSTNAME=api-dev.cshub.nl 11 | SERVER_PORT_INTERNAL=3001 12 | SERVER_PORT_EXTERNAL=3001 13 | SERVER_ENV_PATH=./settings.env 14 | -------------------------------------------------------------------------------- /deployment/dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | ../application/docker-compose.yml -------------------------------------------------------------------------------- /deployment/dev/nginx_example.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main escape=json 18 | '{' 19 | '"time_local":"$time_local",' 20 | '"remote_addr":"$remote_addr",' 21 | '"remote_user":"$remote_user",' 22 | '"request":"$request",' 23 | '"status": "$status",' 24 | '"body_bytes_sent":"$body_bytes_sent",' 25 | '"request_time":"$request_time",' 26 | '"http_referrer":"$http_referer",' 27 | '"http_user_agent":"$http_user_agent"' 28 | '}'; 29 | 30 | access_log /var/log/nginx/access.log main; 31 | 32 | sendfile on; 33 | #tcp_nopush on; 34 | 35 | keepalive_timeout 65; 36 | 37 | #gzip on; 38 | 39 | server { 40 | listen 80 default_server; 41 | server_name localhost; 42 | 43 | root /usr/share/nginx/html; 44 | index index.html; 45 | 46 | location / { 47 | try_files $uri @prerender; 48 | } 49 | 50 | location @prerender { 51 | 52 | set $prerender 0; 53 | if ($http_user_agent ~* "bot|Bot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|redditbot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|Discordbot|curl|Whatsapp") { 54 | set $prerender 1; 55 | } 56 | if ($args ~ "_escaped_fragment_") { 57 | set $prerender 1; 58 | } 59 | if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)") { 60 | set $prerender 0; 61 | } 62 | 63 | resolver DNS; 64 | 65 | if ($prerender = 1) { 66 | rewrite .* /prerender/$request_uri? break; 67 | proxy_pass https://api-dev.cshub.nl; 68 | } 69 | if ($prerender = 0) { 70 | rewrite .* /index.html break; 71 | } 72 | } 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /deployment/dev/settings_example.env: -------------------------------------------------------------------------------- 1 | LIVE=false 2 | PORT=3000 3 | LOGLEVEL=info 4 | 5 | DATABASE_HOST=localhost 6 | DATABASE_USER=xxx 7 | DATABASE_PASSWORD=xxx 8 | DATABASE_NAME=CSHubTest 9 | DATABASE_PORT=3306 10 | 11 | USESSH=true 12 | SSH_HOST=cshub 13 | SSH_USER=xxx 14 | SSH_PORT=22 15 | SSH_PRIVATEKEYLOCATION=xxx 16 | 17 | DOMAIN=192.168.x.x 18 | SITEADDRESS=192.168.xxx 19 | SITEPROTOCOL=http 20 | TOKENAGEMILLISECONDS=7200000 21 | PASSWORDITERATIONS=42424 22 | JWTHASH=xxxxx 23 | PASSWORDSALT=xxxxx 24 | 25 | MAIL_USEGMAIL=false 26 | MAIL_GMAILSETTINGS_PASSWORD=xxx 27 | MAIL_GMAILSETTINGS_MAILADDRESS=xxx@gmail.com 28 | MAIL_APIKEY=xxxxx 29 | MAIL_NOREPLYADDRESS=no-reply@xxx.nl 30 | MAIL_DEBUGMAILADDRESS=xxxx 31 | 32 | APIADDRESS=xxxx 33 | -------------------------------------------------------------------------------- /deployment/logging/.env_example: -------------------------------------------------------------------------------- 1 | # Make sure to make a volume called "esdata1" 2 | 3 | ELASTIC_CONTAINER_NAME=cshub-elasticsearch 4 | ELASTIC_PORT_INTERNAL_API=9200 5 | ELASTIC_PORT_EXTERNAL_API=9200 6 | ELASTIC_PORT_INTERNAL_CLUSTER=9300 7 | ELASTIC_PORT_EXTERNAL_CLUSTER=9300 8 | 9 | KIBANA_CONTAINER_NAME=cshub-kibana 10 | KIBANA_HOSTNAME=kibana.cshub.nl 11 | KIBANA_PORT_INTERNAL=5601 12 | KIBANA_PORT_EXTERNAL=5601 13 | KIBANA_ELASTIC_HOST=cshub 14 | 15 | FLUENTD_CONTAINER_NAME=cshub-fluentd 16 | FLUENTD_PORT_INTERNAL=24224 17 | FLUENTD_PORT_EXTERNAL=24224 18 | -------------------------------------------------------------------------------- /deployment/logging/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | elasticsearch: 4 | image: 'docker.elastic.co/elasticsearch/elasticsearch:6.5.4' 5 | container_name: ${ELASTIC_CONTAINER_NAME} 6 | environment: 7 | - cluster.name=docker-cluster 8 | - bootstrap.memory_lock=true 9 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 10 | ulimits: 11 | memlock: 12 | soft: -1 13 | hard: -1 14 | volumes: 15 | - esdata1:/usr/share/elasticsearch/data 16 | ports: 17 | - ${ELASTIC_PORT_EXTERNAL_API}:${ELASTIC_PORT_INTERNAL_API} 18 | - ${ELASTIC_PORT_EXTERNAL_CLUSTER}:${ELASTIC_PORT_INTERNAL_CLUSTER} 19 | kibana: 20 | image: docker.elastic.co/kibana/kibana:6.5.4 21 | container_name: ${KIBANA_CONTAINER_NAME} 22 | networks: 23 | - web 24 | volumes: 25 | - ./kibana.yml:/usr/share/kibana/config/kibana.yml 26 | ports: 27 | - ${KIBANA_PORT_EXTERNAL}:${KIBANA_PORT_INTERNAL} 28 | labels: 29 | - traefik.frontend.rule=Host:${KIBANA_HOSTNAME} 30 | - traefik.frontend.auth.basic.usersFile=/kibanapass 31 | - traefik.frontend.port=${KIBANA_PORT_INTERNAL} 32 | environment: 33 | - ELASTICSEARCH_URL=http://${KIBANA_ELASTIC_HOST}:${ELASTIC_PORT_EXTERNAL_API} 34 | - SERVER_HOST=0.0.0.0 35 | - SERVER_NAME=${KIBANA_HOSTNAME} 36 | fluentd: 37 | image: harbor.xirion.net/library/cshub-fluentd 38 | container_name: ${FLUENTD_CONTAINER_NAME} 39 | logging: 40 | driver: "json-file" 41 | ports: 42 | - ${FLUENTD_PORT_EXTERNAL}:${FLUENTD_PORT_INTERNAL} 43 | - ${FLUENTD_PORT_EXTERNAL}:${FLUENTD_PORT_INTERNAL}/udp 44 | volumes: 45 | - ./fluentd/conf:/fluentd/etc 46 | volumes: 47 | esdata1: 48 | 49 | networks: 50 | web: 51 | external: true 52 | -------------------------------------------------------------------------------- /deployment/logging/fluentd/conf/fluent.conf: -------------------------------------------------------------------------------- 1 | 2 | @type forward 3 | port 24224 4 | bind 0.0.0.0 5 | 6 | 7 | 8 | @type parser 9 | key_name log 10 | reserve_data true 11 | 12 | @type json 13 | 14 | 15 | 16 | 17 | @type record_transformer 18 | remove_keys log 19 | enable_ruby 20 | 21 | @timestamp ${ require 'time'; Time.now.utc.iso8601(3) } 22 | 23 | 24 | 25 | 26 | @type geoip 27 | 28 | # Specify the field which has the ip to geoip-lookup 29 | geoip_lookup_keys ClientHost 30 | 31 | # Specify the database to user 32 | geoip2_database /geo2.mmdb 33 | 34 | # Specify backend to use 35 | backend_library geoip2_c 36 | 37 | 38 | city ${city.names.en["ClientHost"]} 39 | lat ${location.latitude["ClientHost"]} 40 | lon ${location.longitude["ClientHost"]} 41 | country ${country.iso_code["ClientHost"]} 42 | #country_name ${country_names.en["ClientHost"]} 43 | postal_code ${postal.code["ClientHost"]} 44 | geoip '{"location":[${location.longitude["ClientHost"]},${location.latitude["ClientHost"]}]}' 45 | 46 | 47 | @log_level debug 48 | skip_adding_null_record true 49 | 50 | 51 | 52 | @type elasticsearch 53 | host cshub 54 | port 9200 55 | index_name fluentd 56 | type_name fluentd 57 | flush_thread_count 2 58 | flush_interval 5 59 | 60 | -------------------------------------------------------------------------------- /deployment/logging/kibana.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /deployment/prod/.env_example: -------------------------------------------------------------------------------- 1 | BRANCH=master 2 | 3 | CLIENT_CONTAINER_NAME=cshub-client 4 | CLIENT_HOSTNAME=cshub.nl 5 | CLIENT_PORT_INTERNAL=80 6 | CLIENT_PORT_EXTERNAL=8181 7 | CLIENT_ENV_API_URL=https://api.cshub.nl 8 | 9 | SERVER_CONTAINER_NAME=cshub-server 10 | SERVER_HOSTNAME=api.cshub.nl 11 | SERVER_PORT_INTERNAL=3000 12 | SERVER_PORT_EXTERNAL=3000 13 | SERVER_ENV_PATH=./settings.env 14 | -------------------------------------------------------------------------------- /deployment/prod/docker-compose.yml: -------------------------------------------------------------------------------- 1 | ../application/docker-compose.yml -------------------------------------------------------------------------------- /deployment/prod/nginx_example.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main escape=json 18 | '{' 19 | '"time_local":"$time_local",' 20 | '"remote_addr":"$remote_addr",' 21 | '"remote_user":"$remote_user",' 22 | '"request":"$request",' 23 | '"status": "$status",' 24 | '"body_bytes_sent":"$body_bytes_sent",' 25 | '"request_time":"$request_time",' 26 | '"http_referrer":"$http_referer",' 27 | '"http_user_agent":"$http_user_agent"' 28 | '}'; 29 | 30 | access_log /var/log/nginx/access.log main; 31 | 32 | sendfile on; 33 | #tcp_nopush on; 34 | 35 | keepalive_timeout 65; 36 | 37 | #gzip on; 38 | 39 | server { 40 | listen 80 default_server; 41 | server_name localhost; 42 | 43 | root /usr/share/nginx/html; 44 | index index.html; 45 | 46 | location / { 47 | try_files $uri @prerender; 48 | } 49 | 50 | location @prerender { 51 | 52 | set $prerender 0; 53 | if ($http_user_agent ~* "bot|Bot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|redditbot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|Discordbot|curl|Whatsapp") { 54 | set $prerender 1; 55 | } 56 | if ($args ~ "_escaped_fragment_") { 57 | set $prerender 1; 58 | } 59 | if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)") { 60 | set $prerender 0; 61 | } 62 | 63 | resolver DNS; 64 | 65 | if ($prerender = 1) { 66 | rewrite .* /prerender/$request_uri? break; 67 | proxy_pass https://api.cshub.nl; 68 | } 69 | if ($prerender = 0) { 70 | rewrite .* /index.html break; 71 | } 72 | } 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /deployment/prod/settings_example.env: -------------------------------------------------------------------------------- 1 | LIVE=true 2 | PORT=3000 3 | LOGLEVEL=info 4 | 5 | DATABASE_HOST=localhost 6 | DATABASE_USER=xxx 7 | DATABASE_PASSWORD=xxx 8 | DATABASE_NAME=CSHubTest 9 | DATABASE_PORT=3306 10 | 11 | USESSH=false 12 | SSH_HOST=cshub 13 | SSH_USER=xxx 14 | SSH_PORT=22 15 | SSH_PRIVATEKEYLOCATION=xxx 16 | 17 | DOMAIN=192.168.x.x 18 | SITEADDRESS=cshub.nl 19 | SITEPROTOCOL=https 20 | TOKENAGEMILLISECONDS=7200000 21 | PASSWORDITERATIONS=42424 22 | JWTHASH=xxxxx 23 | PASSWORDSALT=xxxxx 24 | 25 | MAIL_USEGMAIL=false 26 | MAIL_GMAILSETTINGS_PASSWORD=xxx 27 | MAIL_GMAILSETTINGS_MAILADDRESS=xxx@gmail.com 28 | MAIL_APIKEY=xxxxx 29 | MAIL_NOREPLYADDRESS=no-reply@xxx.nl 30 | MAIL_DEBUGMAILADDRESS=xxxx 31 | 32 | APIADDRESS=xxxx 33 | -------------------------------------------------------------------------------- /deployment/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | run() { 4 | echo "Starting $1" 5 | cd ./$1 6 | docker-compose pull 7 | docker-compose down 8 | docker-compose up -d 9 | cd ../ 10 | } 11 | 12 | 13 | if [[ $1 ]]; then 14 | run $1 15 | else 16 | run "logging" 17 | run "traefik" 18 | run "prod" 19 | run "dev" 20 | run "watchtower" 21 | fi 22 | -------------------------------------------------------------------------------- /deployment/traefik/.env_example: -------------------------------------------------------------------------------- 1 | CONTAINER_NAME=cshub-traefik 2 | HOSTNAME=traefik.cshub.nl 3 | PORT_INTERNAL_HTTP=80 4 | PORT_EXTERNAL_HTTP=80 5 | PORT_INTERNAL_UI=8080 6 | PORT_EXTERNAL_UI=8484 -------------------------------------------------------------------------------- /deployment/traefik/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | traefik: 4 | image: traefik:1.7 # The official Traefik docker image 5 | container_name: ${CONTAINER_NAME} 6 | command: --api --docker 7 | networks: 8 | - web 9 | ports: 10 | - ${PORT_EXTERNAL_HTTP}:${PORT_INTERNAL_HTTP} # The HTTP port 11 | - ${PORT_EXTERNAL_UI}:${PORT_INTERNAL_UI} # The Web UI (enabled by --api) 12 | labels: 13 | - traefik.enable=true 14 | - traefik.frontend.rule=Host:${HOSTNAME} 15 | - traefik.frontend.auth.basic.usersFile=/traefikpass 16 | - traefik.port=${PORT_EXTERNAL_UI} 17 | volumes: 18 | - ./traefik.toml:/etc/traefik/traefik.toml 19 | - /var/run/docker.sock:/var/run/docker.sock 20 | - ../logging/.htpasswd:/kibanapass 21 | - ./.htpasswd:/traefikpass 22 | logging: 23 | driver: "json-file" 24 | networks: 25 | web: 26 | external: true 27 | -------------------------------------------------------------------------------- /deployment/traefik/traefik.toml: -------------------------------------------------------------------------------- 1 | [traefikLog] 2 | filePath = "/dev/stdout" 3 | format = "json" 4 | 5 | [accessLog] 6 | filePath = "/dev/stdout" 7 | format = "json" 8 | -------------------------------------------------------------------------------- /deployment/watchtower/.env_example: -------------------------------------------------------------------------------- 1 | CONTAINER_NAME=cshub-watchtower 2 | -------------------------------------------------------------------------------- /deployment/watchtower/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | watchtower: 4 | image: v2tec/watchtower 5 | container_name: ${CONTAINER_NAME} 6 | volumes: 7 | - /var/run/docker.sock:/var/run/docker.sock 8 | command: --interval 18000 --cleanup 9 | -------------------------------------------------------------------------------- /utilities/fixpermissions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | chown pm2 ./cshub-server -R 3 | chown pm2 ./cshub-shared -R 4 | chown root:root ./cshub-client -R 5 | 6 | chmod 770 ./cshub-server -R 7 | chmod 770 ./cshub-shared -R 8 | chmod 771 ./cshub-client -R 9 | 10 | chmod 700 ./cshub-server/src/settings.* 11 | -------------------------------------------------------------------------------- /utilities/intellij-runconfigs/NodeJS_Debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /utilities/intellij-runconfigs/Vue_CLI_Debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /utilities/intellij-runconfigs/Vue_CLI_Server.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |