├── .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 [](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 |
2 |
3 |
4 | {{ icon }}
5 |
6 |
7 |
8 | {{ text }}
9 |
10 |
11 |
12 |
13 |
14 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/cshub-client/src/components/global/NotificationDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ notificationDialogComputed.header }}
5 | {{
6 | text
7 | }}
8 |
9 |
10 | {{
11 | notificationDialogComputed.button.text
12 | }}
13 | Close
19 |
20 |
21 |
22 |
23 |
24 |
70 |
71 |
77 |
--------------------------------------------------------------------------------
/cshub-client/src/components/posts/Examples.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
27 |
28 |
38 |
--------------------------------------------------------------------------------
/cshub-client/src/components/posts/PostPagination.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
2 |
3 |
4 |
5 |
6 | {{ title }}
7 | {{ subtitle }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/cshub-client/src/components/practice/editors/Editors.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
10 |
14 |
16 |
20 |
22 |
23 |
25 |
26 |
27 |
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 |
2 |
3 |
Question:
4 |
5 |
Explanation:
6 |
7 |
Answer:
8 |
{{ answer }}
9 |
10 |
11 |
12 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/cshub-client/src/components/practice/viewers/MultipleChoiceViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Question:
4 |
5 |
Explanation:
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/cshub-client/src/components/practice/viewers/OpenNumberViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Question:
4 |
5 |
Explanation:
6 |
7 |
Answer:
8 |
{{ answer }}
9 |
Precision:
10 |
{{ precision }}
11 |
12 |
13 |
14 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/cshub-client/src/components/practice/viewers/OpenTextViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Question:
4 |
5 |
Explanation:
6 |
7 |
Answer:
8 |
{{ answer }}
9 |
10 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 | Unsaved posts
5 |
6 |
7 |
No posts found!
8 |
9 |
10 |
11 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/cshub-client/src/views/user/WIPPostsView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WIP posts
5 |
6 |
7 |
No posts found!
8 |
9 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/utilities/queries/QUERIES.md:
--------------------------------------------------------------------------------
1 | To view executed queries, execute the following query:
2 | ```sql
3 | SELECT CONVERT(argument USING utf8), event_time
4 | FROM mysql.general_log
5 | WHERE event_time > NOW() - INTERVAL 10 MINUTE
6 | ORDER BY event_time DESC
7 | ```
8 |
9 | Cleanup edits:
10 | ```sql
11 | UPDATE edits
12 | SET htmlContent = NULL
13 | WHERE id NOT IN (
14 | SELECT edits.id
15 | FROM edits
16 | INNER JOIN (
17 | SELECT id, post, MAX(datetime) AS datetime
18 | FROM edits
19 | WHERE approved = 1
20 | GROUP BY post
21 | ) editsDate ON edits.datetime = editsDate.datetime
22 | ORDER BY edits.id DESC
23 | )
24 | ```
25 |
--------------------------------------------------------------------------------
/utilities/version.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const { readFileSync, writeFileSync } = require('fs');
4 | const { execSync } = require('child_process');
5 |
6 | const pkgFiles = ["cshub-client/package.json", "cshub-server/package.json", "cshub-shared/package.json"]
7 |
8 | const gitSHA = execSync("git rev-parse HEAD").toString().replace(/\s/g, '');
9 | console.log("Git SHA: " + gitSHA);
10 |
11 | for (const pkgFile of pkgFiles) {
12 | const pkg = JSON.parse(readFileSync(pkgFile));
13 | pkg["gitSHA"] = gitSHA;
14 | writeFileSync(pkgFile, JSON.stringify(pkg));
15 | }
16 |
--------------------------------------------------------------------------------