├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .vscode └── extensions.json ├── Dockerfile ├── README.md ├── apps ├── .gitkeep ├── mobile │ ├── .build.env │ ├── .eslintrc.json │ ├── android │ │ ├── .gitignore │ │ ├── .idea │ │ │ ├── compiler.xml │ │ │ ├── jarRepositories.xml │ │ │ └── misc.xml │ │ ├── app │ │ │ ├── .gitignore │ │ │ ├── build.gradle │ │ │ ├── capacitor.build.gradle │ │ │ ├── proguard-rules.pro │ │ │ └── src │ │ │ │ ├── androidTest │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── getcapacitor │ │ │ │ │ └── myapp │ │ │ │ │ └── ExampleInstrumentedTest.java │ │ │ │ ├── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── assets │ │ │ │ │ ├── capacitor.config.json │ │ │ │ │ └── capacitor.plugins.json │ │ │ │ ├── java │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── vnts │ │ │ │ │ │ └── MainActivity.java │ │ │ │ └── res │ │ │ │ │ ├── drawable-land-hdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-mdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-xhdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-xxhdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-xxxhdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-hdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-mdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-xhdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-xxhdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-xxxhdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-v24 │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ │ ├── drawable │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── splash.png │ │ │ │ │ ├── layout │ │ │ │ │ └── activity_main.xml │ │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── values │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ │ └── xml │ │ │ │ │ ├── config.xml │ │ │ │ │ └── file_paths.xml │ │ │ │ └── test │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── getcapacitor │ │ │ │ └── myapp │ │ │ │ └── ExampleUnitTest.java │ │ ├── build.gradle │ │ ├── capacitor.settings.gradle │ │ ├── gradle.properties │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── settings.gradle │ │ └── variables.gradle │ ├── capacitor.config.ts │ ├── index.html │ ├── package.json │ ├── project.json │ ├── src │ │ ├── app │ │ │ ├── App.vue │ │ │ ├── components │ │ │ │ ├── AppAccountMenu.vue │ │ │ │ ├── AppCard.vue │ │ │ │ ├── AppDialog.vue │ │ │ │ ├── AppDrawer.vue │ │ │ │ ├── AppNetworkError.vue │ │ │ │ ├── AppPage.vue │ │ │ │ ├── AppPageHeader.vue │ │ │ │ ├── AvatarChip.vue │ │ │ │ ├── headers │ │ │ │ │ └── DashboardHeader.vue │ │ │ │ └── inputs │ │ │ │ │ ├── VInput.vue │ │ │ │ │ └── VSelect.vue │ │ │ ├── layouts │ │ │ │ ├── app.vue │ │ │ │ └── default.vue │ │ │ └── pages │ │ │ │ ├── dashboard.vue │ │ │ │ ├── login.vue │ │ │ │ └── settings.vue │ │ ├── assets │ │ │ ├── main.scss │ │ │ └── quasar.scss │ │ ├── common │ │ │ ├── api │ │ │ │ ├── client.ts │ │ │ │ ├── index.ts │ │ │ │ ├── interceptors.ts │ │ │ │ ├── modules │ │ │ │ │ ├── auth.ts │ │ │ │ │ └── users.ts │ │ │ │ └── response-error.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── use-logout-action.ts │ │ │ │ └── use-promise-state.ts │ │ │ └── index.ts │ │ ├── config.ts │ │ ├── locales │ │ │ ├── en.yml │ │ │ └── pl.yml │ │ ├── main.ts │ │ ├── public │ │ │ └── favicon.ico │ │ ├── router │ │ │ ├── index.ts │ │ │ ├── middlewares │ │ │ │ ├── index.ts │ │ │ │ ├── use-auth-guard.ts │ │ │ │ └── use-loading-indicator.ts │ │ │ └── routes.ts │ │ ├── shims-vue.d.ts │ │ └── stores │ │ │ ├── account.ts │ │ │ ├── app.ts │ │ │ └── index.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── vite.config.ts ├── server │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── project.json │ ├── src │ │ ├── app │ │ │ ├── app.module.ts │ │ │ ├── auth │ │ │ │ ├── auth.controller.ts │ │ │ │ ├── auth.module.ts │ │ │ │ ├── auth.service.ts │ │ │ │ ├── decorators │ │ │ │ │ ├── auth-session.decorator.ts │ │ │ │ │ ├── auth-user.decorator.ts │ │ │ │ │ ├── public.decorator.ts │ │ │ │ │ └── roles.decorator.ts │ │ │ │ ├── guards │ │ │ │ │ ├── jwt-auth.guard.ts │ │ │ │ │ └── roles.guard.ts │ │ │ │ ├── strategies │ │ │ │ │ └── jwt.strategy.ts │ │ │ │ └── utils │ │ │ │ │ └── hash-password.ts │ │ │ ├── cli │ │ │ │ ├── cli.module.ts │ │ │ │ ├── seed.command.ts │ │ │ │ └── seed │ │ │ │ │ └── seed-data.ts │ │ │ ├── cron │ │ │ │ ├── cron.module.ts │ │ │ │ └── cron.service.ts │ │ │ ├── db │ │ │ │ ├── database.module.ts │ │ │ │ ├── session.entity.ts │ │ │ │ └── user.entity.ts │ │ │ └── users │ │ │ │ ├── users.controller.ts │ │ │ │ ├── users.module.ts │ │ │ │ └── users.service.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── cli.ts │ │ ├── common │ │ │ ├── config.ts │ │ │ ├── filesystem.ts │ │ │ ├── index.ts │ │ │ └── pipes │ │ │ │ └── yup-validation.pipe.ts │ │ ├── main.ts │ │ └── static │ │ │ └── index.html │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── webpack.config.js └── web │ ├── .build.env │ ├── .electron.env │ ├── .eslintrc.json │ ├── electron │ ├── electron-env.d.ts │ ├── main │ │ └── index.ts │ └── preload │ │ └── index.ts │ ├── index.html │ ├── project.json │ ├── src │ ├── app │ │ ├── App.vue │ │ ├── components │ │ │ ├── AppAccountMenu.vue │ │ │ ├── AppCard.vue │ │ │ ├── AppDialog.vue │ │ │ ├── AppDrawer.vue │ │ │ ├── AppNetworkError.vue │ │ │ ├── AppPage.vue │ │ │ ├── AppPageHeader.vue │ │ │ ├── AvatarChip.vue │ │ │ ├── dialogs │ │ │ │ └── UserCreateDialog.vue │ │ │ ├── headers │ │ │ │ ├── DashboardHeader.vue │ │ │ │ └── UserHeader.vue │ │ │ └── inputs │ │ │ │ ├── VInput.vue │ │ │ │ └── VSelect.vue │ │ ├── layouts │ │ │ ├── app.vue │ │ │ └── default.vue │ │ └── pages │ │ │ ├── dashboard.vue │ │ │ ├── login.vue │ │ │ ├── settings.vue │ │ │ └── users │ │ │ ├── index.vue │ │ │ ├── list.vue │ │ │ └── user.vue │ ├── assets │ │ ├── main.scss │ │ └── quasar.scss │ ├── common │ │ ├── api │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── interceptors.ts │ │ │ ├── modules │ │ │ │ ├── auth.ts │ │ │ │ └── users.ts │ │ │ └── response-error.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── use-logout-action.ts │ │ │ └── use-promise-state.ts │ │ └── index.ts │ ├── config.ts │ ├── locales │ │ ├── en.yml │ │ └── pl.yml │ ├── main.ts │ ├── public │ │ └── favicon.ico │ ├── router │ │ ├── index.ts │ │ ├── middlewares │ │ │ ├── index.ts │ │ │ ├── use-auth-guard.ts │ │ │ └── use-loading-indicator.ts │ │ └── routes.ts │ ├── shims-vue.d.ts │ └── stores │ │ ├── account.ts │ │ ├── app.ts │ │ └── index.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── vite.config.ts ├── docker-compose.yml ├── electron-builder.yml ├── jest.config.ts ├── jest.preset.js ├── libs ├── .gitkeep └── shared │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.ts │ ├── project.json │ ├── src │ ├── auth │ │ ├── index.ts │ │ ├── inputs │ │ │ └── auth-login.input.ts │ │ └── responses │ │ │ └── auth-login.response.ts │ ├── common │ │ ├── index.ts │ │ ├── inputs │ │ │ └── pagination.input.ts │ │ ├── responses │ │ │ └── pagination.response.ts │ │ └── role.enum.ts │ ├── index.ts │ ├── users │ │ ├── index.ts │ │ ├── inputs │ │ │ ├── user-create.input.ts │ │ │ ├── user-update-self-password.input.ts │ │ │ ├── user-update-self.input.ts │ │ │ └── user-update.input.ts │ │ └── responses │ │ │ └── user-profile.response.ts │ └── utils │ │ ├── compare-roles.ts │ │ ├── index.ts │ │ ├── timeout.ts │ │ ├── use-schema.ts │ │ └── yup-locale.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── nx.json ├── package-lock.json ├── package.json ├── tools ├── generators │ └── .gitkeep ├── scripts │ └── patch-graph.js └── tsconfig.tools.json └── tsconfig.base.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*.{ts,js,vue}] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | end_of_line = lf 16 | 17 | [*.json] 18 | charset = utf-8 19 | indent_style = space 20 | indent_size = 2 21 | end_of_line = lf 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Front-end: API server connection configuration 2 | VITE_WEB_DEFAULT_LOCALE="en" 3 | VITE_WEB_API_URL="http://localhost" 4 | VITE_WEB_API_PORT=3000 5 | 6 | # HTTP / HTTPS server config 7 | NEST_API_HTTP_PORT=3000 8 | NEST_API_HTTP_SSL=false 9 | NEST_API_HTTP_KEY="" 10 | NEST_API_HTTP_CERT="" 11 | 12 | # Cross-Origin Resource Sharing domain origins 13 | # More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS 14 | NEST_API_HTTP_CORS=["http://localhost", "http://localhost:8080", "http://localhost:8090", "app://localhost", "capacitor://localhost"] 15 | 16 | # Keys required for hashing passwords and tokens 17 | # They should be filled with random, unique strings 18 | NEST_API_SECRETS_PWDSALT="" 19 | NEST_API_SECRETS_JWT="" 20 | 21 | # Database type: postgres, mysql, sqlite etc. 22 | # More info: https://typeorm.io 23 | DATABASE_TYPE="postgres" 24 | 25 | # Database connection config 26 | DATABASE_HOST="localhost" 27 | DATABASE_PORT=5432 28 | 29 | # Database name and user credentials 30 | DATABASE_NAME="" 31 | DATABASE_USER="" 32 | DATABASE_PASSWORD="" 33 | 34 | # Disable this in the production version of the application 35 | # More info: https://typeorm.io/faq#how-do-i-update-a-database-schema 36 | DATABASE_ENABLE_SYNC=true -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | 4 | "ignorePatterns": ["node_modules", "dist", "build", "components.d.ts"], 5 | 6 | "overrides": [ 7 | { 8 | "files": ["*.ts", ".tsx"], 9 | 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:prettier/recommended" 14 | ], 15 | 16 | "env": { 17 | "es6": true, 18 | "browser": true, 19 | "node": true 20 | }, 21 | 22 | "plugins": [ 23 | "@typescript-eslint", 24 | "prettier" 25 | ], 26 | 27 | "rules": { 28 | "prettier/prettier": "warn", 29 | 30 | "@typescript-eslint/no-floating-promises": "off", 31 | "@typescript-eslint/require-await": "off", 32 | "@typescript-eslint/no-explicit-any": "off", 33 | "@typescript-eslint/no-unsafe-call": "off", 34 | "@typescript-eslint/no-unsafe-member-access": "off", 35 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], 36 | 37 | "semi": ["error", "always"], 38 | "quotes": ["warn", "single", { "allowTemplateLiterals": true }] 39 | } 40 | }, 41 | 42 | { 43 | "files": ["*.vue"], 44 | 45 | "extends": [ 46 | "plugin:vue/vue3-essential", 47 | "@vue/eslint-config-typescript/recommended", 48 | "@vue/eslint-config-prettier" 49 | ], 50 | 51 | "plugins": [ 52 | "prettier" 53 | ], 54 | 55 | "env": { 56 | "vue/setup-compiler-macros": true 57 | }, 58 | 59 | "rules": { 60 | "prettier/prettier": "warn", 61 | 62 | "@typescript-eslint/no-floating-promises": "off", 63 | "@typescript-eslint/require-await": "off", 64 | "@typescript-eslint/no-explicit-any": "off", 65 | "@typescript-eslint/no-unsafe-call": "off", 66 | "@typescript-eslint/no-unsafe-member-access": "off", 67 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], 68 | 69 | //nuxt standard 70 | "vue/multi-word-component-names": "off", 71 | 72 | "semi": ["error", "always"], 73 | "quotes": ["warn", "single", { "allowTemplateLiterals": true }] 74 | } 75 | }, 76 | 77 | { 78 | "files": ["*.js"], 79 | 80 | "plugins": [ 81 | "prettier" 82 | ], 83 | 84 | "parserOptions": { 85 | "ecmaVersion": 2018 86 | }, 87 | 88 | "env": { 89 | "es6": true 90 | }, 91 | 92 | "rules": { 93 | "prettier/prettier": "warn", 94 | 95 | "semi": ["error", "always"], 96 | "quotes": ["warn", "single", { "allowTemplateLiterals": true }] 97 | } 98 | } 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .env 42 | apps/**/components.d.ts 43 | apps/**/auto-imports.d.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true, 4 | "endOfLine": "auto", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "vue.volar", 5 | "dbaeumer.vscode-eslint", 6 | "editorconfig.editorconfig", 7 | "syler.sass-indented", 8 | "eamodio.gitlens", 9 | "aaron-bond.better-comments", 10 | "visualstudioexptteam.vscodeintellicode", 11 | "pkief.material-icon-theme", 12 | "mikestead.dotenv", 13 | "firsttris.vscode-jest-runner" 14 | ], 15 | "unwantedRecommendations": ["octref.vetur"] 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.13-alpine 2 | WORKDIR /usr 3 | 4 | COPY package.json ./ 5 | COPY package-lock.json ./ 6 | COPY dist/apps/server ./server 7 | 8 | RUN ls -a 9 | RUN npm install 10 | 11 | CMD node ./server/main.js -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/.gitkeep -------------------------------------------------------------------------------- /apps/mobile/.build.env: -------------------------------------------------------------------------------- 1 | VITE_DISABLE_VUE_TSC=true -------------------------------------------------------------------------------- /apps/mobile/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../../.eslintrc.json" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /apps/mobile/android/.gitignore: -------------------------------------------------------------------------------- 1 | # Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore 2 | 3 | # Built application files 4 | *.apk 5 | *.aar 6 | *.ap_ 7 | *.aab 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | # Uncomment the following line in case you need and you don't have the release build type files in your app 20 | # release/ 21 | 22 | # Gradle files 23 | .gradle/ 24 | build/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/workspace.xml 44 | .idea/tasks.xml 45 | .idea/gradle.xml 46 | .idea/assetWizardSettings.xml 47 | .idea/dictionaries 48 | .idea/libraries 49 | # Android Studio 3 in .gitignore file. 50 | .idea/caches 51 | .idea/modules.xml 52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 53 | .idea/navEditor.xml 54 | 55 | # Keystore files 56 | # Uncomment the following lines if you do not want to check your keystore files in. 57 | #*.jks 58 | #*.keystore 59 | 60 | # External native build folder generated in Android Studio 2.2 and later 61 | .externalNativeBuild 62 | .cxx/ 63 | 64 | # Google Services (e.g. APIs or Firebase) 65 | # google-services.json 66 | 67 | # Freeline 68 | freeline.py 69 | freeline/ 70 | freeline_project_description.json 71 | 72 | # fastlane 73 | fastlane/report.xml 74 | fastlane/Preview.html 75 | fastlane/screenshots 76 | fastlane/test_output 77 | fastlane/readme.md 78 | 79 | # Version control 80 | vcs.xml 81 | 82 | # lint 83 | lint/intermediates/ 84 | lint/generated/ 85 | lint/outputs/ 86 | lint/tmp/ 87 | # lint/reports/ 88 | 89 | # Android Profiling 90 | *.hprof 91 | 92 | # Cordova plugins for Capacitor 93 | capacitor-cordova-android-plugins 94 | 95 | # Copied web assets 96 | app/src/main/assets/public 97 | 98 | # Generated Config files 99 | app/src/main/assets/capacitor.config.json 100 | app/src/main/assets/capacitor.plugins.json 101 | app/src/main/res/xml/config.xml 102 | -------------------------------------------------------------------------------- /apps/mobile/android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/mobile/android/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /apps/mobile/android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /apps/mobile/android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | !/build/.npmkeep 3 | -------------------------------------------------------------------------------- /apps/mobile/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | namespace "com.example.vnts" 5 | compileSdkVersion rootProject.ext.compileSdkVersion 6 | defaultConfig { 7 | applicationId "com.example.vnts" 8 | minSdkVersion rootProject.ext.minSdkVersion 9 | targetSdkVersion rootProject.ext.targetSdkVersion 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | aaptOptions { 14 | // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. 15 | // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 16 | ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' 17 | } 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | repositories { 28 | flatDir{ 29 | dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation fileTree(include: ['*.jar'], dir: 'libs') 35 | implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" 36 | implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" 37 | implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" 38 | implementation project(':capacitor-android') 39 | testImplementation "junit:junit:$junitVersion" 40 | androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" 41 | androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" 42 | implementation project(':capacitor-cordova-android-plugins') 43 | } 44 | 45 | apply from: 'capacitor.build.gradle' 46 | 47 | try { 48 | def servicesJSON = file('google-services.json') 49 | if (servicesJSON.text) { 50 | apply plugin: 'com.google.gms.google-services' 51 | } 52 | } catch(Exception e) { 53 | logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") 54 | } 55 | -------------------------------------------------------------------------------- /apps/mobile/android/app/capacitor.build.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | 3 | android { 4 | compileOptions { 5 | sourceCompatibility JavaVersion.VERSION_17 6 | targetCompatibility JavaVersion.VERSION_17 7 | } 8 | } 9 | 10 | apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" 11 | dependencies { 12 | 13 | 14 | } 15 | 16 | 17 | if (hasProperty('postBuildExtras')) { 18 | postBuildExtras() 19 | } 20 | -------------------------------------------------------------------------------- /apps/mobile/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.myapp; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import android.content.Context; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | import androidx.test.platform.app.InstrumentationRegistry; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * @see Testing documentation 15 | */ 16 | @RunWith(AndroidJUnit4.class) 17 | public class ExampleInstrumentedTest { 18 | 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 23 | 24 | assertEquals("com.getcapacitor.app", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/assets/capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.example.vnts", 3 | "appName": "nx-nestjs-vue", 4 | "webDir": "../../dist/apps/mobile" 5 | } 6 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/assets/capacitor.plugins.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/java/com/example/vnts/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.vnts; 2 | 3 | import com.getcapacitor.BridgeActivity; 4 | 5 | public class MainActivity extends BridgeActivity {} 6 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-land-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-land-hdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-land-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-land-mdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-land-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-land-xhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-land-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-land-xxhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-land-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-land-xxxhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-port-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-port-hdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-port-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-port-mdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-port-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-port-xhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-port-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-port-xxhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-port-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable-port-xxxhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/drawable/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/drawable/splash.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | nx-nestjs-vue 4 | nx-nestjs-vue 5 | com.example.vnts 6 | com.example.vnts 7 | 8 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 17 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/xml/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/mobile/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.myapp; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | 14 | @Test 15 | public void addition_isCorrect() throws Exception { 16 | assertEquals(4, 2 + 2); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/mobile/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:7.4.0' 11 | classpath 'com.google.gms:google-services:4.3.15' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | apply from: "variables.gradle" 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | mavenCentral() 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /apps/mobile/android/capacitor.settings.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | include ':capacitor-android' 3 | project(':capacitor-android').projectDir = new File('../../../node_modules/@capacitor/android/capacitor') 4 | -------------------------------------------------------------------------------- /apps/mobile/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | -------------------------------------------------------------------------------- /apps/mobile/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /apps/mobile/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /apps/mobile/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /apps/mobile/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':capacitor-cordova-android-plugins' 3 | project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') 4 | 5 | apply from: 'capacitor.settings.gradle' -------------------------------------------------------------------------------- /apps/mobile/android/variables.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | minSdkVersion = 22 3 | compileSdkVersion = 33 4 | targetSdkVersion = 33 5 | androidxActivityVersion = '1.7.0' 6 | androidxAppCompatVersion = '1.6.1' 7 | androidxCoordinatorLayoutVersion = '1.2.0' 8 | androidxCoreVersion = '1.10.0' 9 | androidxFragmentVersion = '1.5.6' 10 | coreSplashScreenVersion = '1.0.0' 11 | androidxWebkitVersion = '1.6.1' 12 | junitVersion = '4.13.2' 13 | androidxJunitVersion = '1.1.5' 14 | androidxEspressoCoreVersion = '3.5.1' 15 | cordovaAndroidVersion = '10.1.1' 16 | } -------------------------------------------------------------------------------- /apps/mobile/capacitor.config.ts: -------------------------------------------------------------------------------- 1 | import { CapacitorConfig } from '@capacitor/cli'; 2 | 3 | const config: CapacitorConfig = { 4 | appId: 'com.example.vnts', 5 | appName: 'nx-nestjs-vue', 6 | webDir: '../../dist/apps/mobile', 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /apps/mobile/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nx-nestjs-vue-mobile 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile" 3 | } 4 | -------------------------------------------------------------------------------- /apps/mobile/src/app/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 49 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/AppAccountMenu.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 61 | 62 | 71 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/AppCard.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/AppDialog.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 46 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/AppNetworkError.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 17 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/AppPage.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/AppPageHeader.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/AvatarChip.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 41 | 42 | 52 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/headers/DashboardHeader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/inputs/VInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 46 | -------------------------------------------------------------------------------- /apps/mobile/src/app/components/inputs/VSelect.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 49 | -------------------------------------------------------------------------------- /apps/mobile/src/app/layouts/app.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 38 | -------------------------------------------------------------------------------- /apps/mobile/src/app/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /apps/mobile/src/app/pages/dashboard.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /apps/mobile/src/assets/main.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap'); 2 | 3 | body:not(i) { 4 | font-family: 'Poppins', sans-serif; 5 | } 6 | 7 | .app-header { 8 | box-shadow: 0 12px 12px -12px rgb(0 0 0 / 20%); 9 | } 10 | 11 | .app-drawer { 12 | background-color: #111827; 13 | color: #ffffff; 14 | box-shadow: 0px 0px 12px rgb(0 0 0 / 20%); 15 | } 16 | 17 | .app-page-container { 18 | background-color: #f1f5f9; 19 | min-height: 100px; 20 | } 21 | 22 | .container { 23 | margin: auto; 24 | padding: 38px; 25 | max-width: 1600px; 26 | 27 | @media (max-width: 600px){ 28 | padding: 18px; 29 | } 30 | } 31 | 32 | .border-bottom-accent { 33 | border-bottom: 1px solid #e2e8f0; 34 | } 35 | 36 | .q-card { 37 | border-radius: 20px; 38 | box-shadow: 0px 2px 3px -3px rgba(66, 68, 90, 0.72); 39 | display: flex; 40 | flex-direction: column; 41 | 42 | .card-title { 43 | font-weight: 500; 44 | font-size: 24px; 45 | margin-bottom: 5px; 46 | } 47 | 48 | >form{ 49 | height: 100%; 50 | display: flex; 51 | flex-direction: column; 52 | } 53 | 54 | .q-card__section { 55 | padding: 10px 30px; 56 | 57 | &:first-child { 58 | padding-top: 30px; 59 | } 60 | 61 | &:last-child { 62 | padding-bottom: 30px; 63 | } 64 | } 65 | 66 | .q-card__actions { 67 | padding: 30px; 68 | display: flex; 69 | align-items: flex-end; 70 | flex-grow: 1; 71 | 72 | .q-btn { 73 | font-size: 14px; 74 | padding: 8px 15px 8px 15px; 75 | } 76 | } 77 | } 78 | 79 | .q-pagination { 80 | .q-btn { 81 | font-size: 14px; 82 | line-height: 13px; 83 | padding: 8px !important; 84 | } 85 | } 86 | 87 | .q-dialog-max-width { 88 | width: 100%; 89 | max-width: 500px; 90 | } 91 | -------------------------------------------------------------------------------- /apps/mobile/src/assets/quasar.scss: -------------------------------------------------------------------------------- 1 | $primary : #4F46E5; 2 | $secondary : #111827; 3 | $accent : #9C27B0; 4 | 5 | $dark : #1D1D1D; 6 | 7 | $positive : #21BA45; 8 | $negative : #EF5350; 9 | $info : #31CCEC; 10 | $warning : #F2C037; 11 | -------------------------------------------------------------------------------- /apps/mobile/src/common/api/client.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@/config'; 2 | import axios from 'axios'; 3 | 4 | export const $axios = axios.create({ 5 | baseURL: `${config.api.host}:${config.api.port}/api`, 6 | withCredentials: true, 7 | }); 8 | 9 | export function setAuthorizationToken(token: string): void { 10 | $axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 11 | } 12 | 13 | export function clearAuthorizationToken(): void { 14 | $axios.defaults.headers.common['Authorization'] = undefined; 15 | } 16 | -------------------------------------------------------------------------------- /apps/mobile/src/common/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './interceptors'; 3 | export * from './response-error'; 4 | 5 | import * as auth from './modules/auth'; 6 | import * as users from './modules/users'; 7 | 8 | export const api = { 9 | auth, 10 | users, 11 | }; 12 | -------------------------------------------------------------------------------- /apps/mobile/src/common/api/interceptors.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import { timeout } from '@workspace/shared'; 3 | import { $axios } from './client'; 4 | import { ResponseError } from './response-error'; 5 | 6 | type ResponseSuccessCallback = (response: AxiosResponse) => void; 7 | type ResponseErrorCallback = (error: ResponseError) => void; 8 | 9 | interface CallbackTrigger { 10 | responseSuccess: ResponseSuccessCallback; 11 | responseError: ResponseErrorCallback; 12 | } 13 | 14 | const callbackTrigger: CallbackTrigger = { 15 | responseSuccess: null, 16 | responseError: null, 17 | }; 18 | 19 | $axios.interceptors.response.use( 20 | (response: AxiosResponse) => { 21 | if (callbackTrigger.responseSuccess) callbackTrigger.responseSuccess(response); 22 | return response; 23 | }, 24 | 25 | async (error: ResponseError) => { 26 | if (error.response && error.response.status !== 0) { 27 | error.isNetworkError = false; 28 | if (callbackTrigger.responseError) callbackTrigger.responseError(error); 29 | return Promise.reject(error); 30 | } else { 31 | error.isNetworkError = true; 32 | if (callbackTrigger.responseError) callbackTrigger.responseError(error); 33 | await timeout(5000); 34 | return await $axios.request(error.config); 35 | } 36 | } 37 | ); 38 | 39 | export function onResponseSuccess(callback: ResponseSuccessCallback): void { 40 | callbackTrigger.responseSuccess = callback; 41 | } 42 | 43 | export function onResponseError(callback: ResponseErrorCallback): void { 44 | callbackTrigger.responseError = callback; 45 | } 46 | -------------------------------------------------------------------------------- /apps/mobile/src/common/api/modules/auth.ts: -------------------------------------------------------------------------------- 1 | import { AuthLoginDto, AuthLoginResponse } from '@workspace/shared'; 2 | import { $axios } from '../client'; 3 | 4 | export async function login(authLoginDto: AuthLoginDto) { 5 | return await $axios.post('/auth/login', authLoginDto); 6 | } 7 | 8 | export async function logout() { 9 | return await $axios.post('/auth/logout'); 10 | } 11 | -------------------------------------------------------------------------------- /apps/mobile/src/common/api/modules/users.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PaginationDto, 3 | PaginationResponse, 4 | UserCreateDto, 5 | UserProfileResponse, 6 | UserUpdateDto, 7 | UserUpdateSelfDto, 8 | UserUpdateSelfPasswordDto, 9 | } from '@workspace/shared'; 10 | 11 | import { $axios } from '../client'; 12 | 13 | export async function getSelf() { 14 | return await $axios.get('/users/self'); 15 | } 16 | 17 | export async function updateSelf(data: UserUpdateSelfDto) { 18 | return await $axios.put('/users/self', data); 19 | } 20 | 21 | export async function updateSelfPassword(data: UserUpdateSelfPasswordDto) { 22 | return await $axios.put('/users/self/password', data); 23 | } 24 | 25 | export async function createOne(data: UserCreateDto) { 26 | return await $axios.post('/users/', data); 27 | } 28 | 29 | export async function getMany(data: PaginationDto) { 30 | return await $axios.get>(`/users/`, { 31 | params: data, 32 | }); 33 | } 34 | 35 | export async function getOne(id: string) { 36 | return await $axios.get(`/users/${id}`); 37 | } 38 | 39 | export async function updateOne(id: string, data: UserUpdateDto) { 40 | return await $axios.put(`/users/${id}`, data); 41 | } 42 | 43 | export async function deleteOne(id: string) { 44 | return await $axios.delete(`/users/${id}`); 45 | } 46 | -------------------------------------------------------------------------------- /apps/mobile/src/common/api/response-error.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | 3 | export interface ResponseError extends AxiosError { 4 | isNetworkError: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /apps/mobile/src/common/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-promise-state'; 2 | export * from './use-logout-action'; 3 | -------------------------------------------------------------------------------- /apps/mobile/src/common/hooks/use-logout-action.ts: -------------------------------------------------------------------------------- 1 | import { useAccountStore } from '@/stores/account'; 2 | import { useQuasar, DialogChainObject } from 'quasar'; 3 | import { reactive } from 'vue'; 4 | import { useI18n } from 'vue-i18n'; 5 | import { useRouter } from 'vue-router'; 6 | import { api } from '../api'; 7 | import { usePromiseState, UsePromiseStateRetrun } from './use-promise-state'; 8 | 9 | export interface UseLogoutActionReturn 10 | extends UsePromiseStateRetrun { 11 | logout: () => void; 12 | } 13 | 14 | export function useLogoutAction(): UseLogoutActionReturn { 15 | const router = useRouter(); 16 | const $q = useQuasar(); 17 | const { t } = useI18n(); 18 | 19 | const accountStore = useAccountStore(); 20 | 21 | let dialog: DialogChainObject | undefined = undefined; 22 | 23 | function showLogoutDialog(): void { 24 | dialog = $q.dialog({ 25 | message: t('account_logout_progress'), 26 | progress: true, 27 | persistent: true, 28 | ok: false, 29 | color: 'primary', 30 | }); 31 | } 32 | 33 | function hideLogoutDialog(): void { 34 | if (dialog) dialog.hide(); 35 | } 36 | 37 | const logoutAction = usePromiseState( 38 | async () => { 39 | await api.auth.logout(); 40 | accountStore.setAuthenticated(false); 41 | hideLogoutDialog(); 42 | 43 | $q.notify({ 44 | icon: 'mdi-check', 45 | color: 'positive', 46 | message: t('account_logout_success'), 47 | timeout: 1000, 48 | }); 49 | 50 | router.push({ name: 'login' }); 51 | }, 52 | () => { 53 | hideLogoutDialog(); 54 | 55 | $q.notify({ 56 | icon: 'mdi-close', 57 | color: 'negative', 58 | message: t('account_logout_failed'), 59 | timeout: 1000, 60 | }); 61 | } 62 | ); 63 | 64 | function logout(): void { 65 | showLogoutDialog(); 66 | logoutAction.execute(500); 67 | } 68 | 69 | return reactive({ logout, ...logoutAction }); 70 | } 71 | -------------------------------------------------------------------------------- /apps/mobile/src/common/hooks/use-promise-state.ts: -------------------------------------------------------------------------------- 1 | import { timeout } from '@workspace/shared'; 2 | import { reactive, ref, shallowRef } from 'vue'; 3 | import { createEventHook, EventHookOn } from '@vueuse/core'; 4 | 5 | export interface UsePromiseStateRetrun { 6 | isReady: boolean; 7 | isLoading: boolean; 8 | counter: number; 9 | startTime: number; 10 | endTime: number; 11 | state: TResult; 12 | error: TError; 13 | onError: EventHookOn; 14 | execute: (delay?: number, payload?: TPayload) => Promise; 15 | } 16 | 17 | export function usePromiseState( 18 | promise: (payload?: any) => Promise, 19 | onError?: (e: TError) => void 20 | ): UsePromiseStateRetrun { 21 | const isReady = ref(false); 22 | const isLoading = ref(false); 23 | const counter = ref(0); 24 | const startTime = ref(undefined); 25 | const endTime = ref(undefined); 26 | 27 | const state = shallowRef(undefined); 28 | const error = shallowRef(undefined); 29 | 30 | const errorEvent = createEventHook(); 31 | if (onError) errorEvent.on(onError); 32 | 33 | async function execute(delay?: number, payload?: TPayload): Promise { 34 | isReady.value = false; 35 | isLoading.value = true; 36 | startTime.value = Date.now(); 37 | endTime.value = undefined; 38 | error.value = undefined; 39 | 40 | try { 41 | if (delay > 0) await timeout(delay); 42 | 43 | const data = await promise(payload); 44 | state.value = data; 45 | isReady.value = true; 46 | counter.value++; 47 | } catch (e) { 48 | error.value = e as TError; 49 | errorEvent.trigger(error.value); 50 | } finally { 51 | isLoading.value = false; 52 | endTime.value = Date.now(); 53 | } 54 | 55 | return state.value; 56 | } 57 | 58 | return reactive({ 59 | isReady, 60 | isLoading, 61 | counter, 62 | startTime, 63 | endTime, 64 | state, 65 | error, 66 | onError: errorEvent.on, 67 | execute, 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /apps/mobile/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './hooks'; 3 | -------------------------------------------------------------------------------- /apps/mobile/src/config.ts: -------------------------------------------------------------------------------- 1 | interface Config { 2 | defaultLocale: string; 3 | useCookies: boolean; 4 | api: { 5 | host: string; 6 | port: number; 7 | }; 8 | } 9 | 10 | export const config: Config = { 11 | defaultLocale: import.meta.env.VITE_WEB_DEFAULT_LOCALE ?? 'en', 12 | useCookies: true, 13 | 14 | api: { 15 | host: import.meta.env.VITE_WEB_API_URL ?? 'http://localhost', 16 | port: import.meta.env.VITE_WEB_API_PORT ?? 3000, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /apps/mobile/src/locales/en.yml: -------------------------------------------------------------------------------- 1 | app_name: 'nx-nestjs-vue' 2 | app_connection_lost: 'Connection to the server has been lost. Connecting...' 3 | 4 | yes: 'Yes' 5 | no: 'No' 6 | save: 'Save' 7 | add: 'Add' 8 | cancel: 'Cancel' 9 | search: 'Search' 10 | edit: 'Edit' 11 | saved_successfully: 'The changes made have been saved.' 12 | 13 | routes_login: 'Sign in' 14 | routes_dashboard: 'Dashboard' 15 | routes_users: 'Users' 16 | routes_user: 'User' 17 | routes_settings: 'Settings' 18 | routes_logout: 'Sign out' 19 | 20 | validation_field_required: 'This field is required!' 21 | validation_string_min: 'This field must contain at least {min} characters!' 22 | validation_string_max: 'This field must contain a maximum of {max} characters!' 23 | validation_string_email: 'This is not a valid e-mail address!' 24 | validation_string_password_repeat: 'Passwords must match!' 25 | validation_string_matches: 'Incorrect format!' 26 | 27 | roles_user: 'User' 28 | roles_admin: 'Administartor' 29 | 30 | menu_main_title: 'Main menu' 31 | menu_main_caption: 'Basic functions of the application' 32 | menu_admin_title: 'Administration' 33 | menu_admin_caption: 'Functions for administrators' 34 | menu_account_title: 'Account' 35 | menu_account_caption: 'User account options' 36 | 37 | account_session_exp: 'Your session has expired!' 38 | account_logout_success: 'Logged out successfully!' 39 | account_logout_failed: 'There was a problem logging out!' 40 | account_logout_progress: 'Signing out ...' 41 | account_menu_settings: 'Settings' 42 | account_menu_logout: 'Sign out' 43 | 44 | login_welcome_title: 'Control panel' 45 | login_welcome_description: 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quaerat quas officiis nisi consequatur optio repudiandae corporis. Quos, iste labore? Laboriosam odio enim deserunt sunt illo commodi nostrum in praesentium suscipit autem, ea deleniti consectetur aspernatur fuga minima obcaecati corrupti iusto aliquid voluptatem! Repellat itaque voluptas facilis dolore expedita? Consectetur, facere!' 46 | login_form_email: 'E-mail adress' 47 | login_form_password: 'Password' 48 | login_form_remember: 'Remember me' 49 | login_form_signin: 'Sign in' 50 | login_form_errors_no_authorization: 'Wrong e-mail or password!' 51 | 52 | dashboard_welcome: 'Hello again, {name}!' 53 | 54 | users_title: 'User settings' 55 | users_email: 'E-mail' 56 | users_form_first_name: 'Name' 57 | users_form_last_name: 'Surname' 58 | users_form_position: 'Position' 59 | users_form_role: 'Role' 60 | users_form_isActive: 'Active' 61 | users_form_password: 'Password' 62 | users_form_errors_default: 'Error occured!' 63 | users_form_errors_samelogin: 'The given username already exists in the database!' 64 | users_create_title: 'New user' 65 | users_create_email: 'E-mail' 66 | users_create_first_name: 'Name' 67 | users_create_last_name: 'Surname' 68 | users_create_position: 'Position' 69 | users_create_role: 'Role' 70 | users_create_password: 'Password' 71 | users_create_errors_default: 'Error occured!' 72 | users_create_errors_user_exists: 'A user with such a login already exists!' 73 | users_list_first_and_last_name: 'Name and surname' 74 | users_list_email: 'E-mail' 75 | users_list_role: 'Role' 76 | users_list_created_at: 'Created at' 77 | users_select_search: 'Search for a user' 78 | 79 | settings_general_title: 'General' 80 | settings_general_role: 'Role' 81 | settings_general_email: 'E-mail' 82 | settings_general_form_first_name: 'Name' 83 | settings_general_form_last_name: 'Surname' 84 | settings_general_form_position: 'Position' 85 | settings_general_form_errors_default: 'Error occured!' 86 | settings_password_title: 'Account' 87 | settings_password_form_password: 'Current password' 88 | settings_password_form_new_password: 'New password' 89 | settings_password_form_repeat_password: 'Repeat new password' 90 | settings_password_form_errors_default: 'Error occured!' 91 | settings_password_form_errors_wrong_password: 'Wrong password!' 92 | -------------------------------------------------------------------------------- /apps/mobile/src/locales/pl.yml: -------------------------------------------------------------------------------- 1 | app_name: 'nx-nestjs-vue' 2 | app_connection_lost: 'Połączenie z serwerem zostało zerwane. Łączenie...' 3 | 4 | yes: 'Tak' 5 | no: 'Nie' 6 | save: 'Zapisz' 7 | add: 'Dodaj' 8 | cancel: 'Anuluj' 9 | search: 'Wyszukaj' 10 | edit: 'Edycja' 11 | saved_successfully: 'Wprowadzone zmiany zostały zapisane.' 12 | 13 | routes_login: 'Logowanie' 14 | routes_dashboard: 'Dashboard' 15 | routes_users: 'Użytkownicy' 16 | routes_user: 'Użytkownik' 17 | routes_settings: 'Ustawienia' 18 | routes_logout: 'Wyloguj' 19 | 20 | validation_field_required: 'To pole jest wymagane!' 21 | validation_string_min: 'To pole musi zawierać minumum {min} znaków!' 22 | validation_string_max: 'To pole musi zawierać maksymalnie {max} znaków!' 23 | validation_string_email: 'To nie jest poprawny adres e-mail!' 24 | validation_string_password_repeat: 'Hasła muszą być identyczne!' 25 | validation_string_matches: 'Niepoprawny format!' 26 | 27 | roles_user: 'Użytkownik' 28 | roles_admin: 'Administartor' 29 | 30 | menu_main_title: 'Menu główne' 31 | menu_main_caption: 'Podstawowe funkcje aplikacji' 32 | menu_admin_title: 'Administracja' 33 | menu_admin_caption: 'Funkcje dla administratorów' 34 | menu_account_title: 'Konto' 35 | menu_account_caption: 'Opcje konta użytkownika' 36 | account_session_exp: 'Sesja wygasła!' 37 | account_logout_success: 'Wylogowano poprawnie!' 38 | account_logout_failed: 'Wystąpił problem podczas wylogowywania!' 39 | account_logout_progress: 'Wylogowywanie...' 40 | account_menu_settings: 'Ustawienia' 41 | account_menu_logout: 'Wyloguj' 42 | 43 | login_welcome_title: 'Panel kontrolny' 44 | login_welcome_description: 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quaerat quas officiis nisi consequatur optio repudiandae corporis. Quos, iste labore? Laboriosam odio enim deserunt sunt illo commodi nostrum in praesentium suscipit autem, ea deleniti consectetur aspernatur fuga minima obcaecati corrupti iusto aliquid voluptatem! Repellat itaque voluptas facilis dolore expedita? Consectetur, facere!' 45 | login_form_login: 'Nazwa użytkownika' 46 | login_form_password: 'Hasło' 47 | login_form_remember: 'Zapamiętaj mnie' 48 | login_form_signin: 'Zaloguj się' 49 | login_form_errors_no_authorization: 'Błędny e-mail lub hasło!' 50 | 51 | dashboard_welcome: 'Witaj ponownie, {name}!' 52 | 53 | users_title: 'Ustawienia użytkownika' 54 | users_email: 'E-mail' 55 | users_form_first_name: 'Imię' 56 | users_form_last_name: 'Nazwisko' 57 | users_form_position: 'Stanowisko' 58 | users_form_role: 'Rola' 59 | users_form_isActive: 'Aktywny' 60 | users_form_password: 'Hasło' 61 | users_form_errors_default: 'Wystąpił błąd!' 62 | users_form_errors_samelogin: 'Podana nazwa użytkownika już istnieje w bazie!' 63 | users_create_title: 'Nowy użytkownik' 64 | users_create_email: 'E-mail' 65 | users_create_first_name: 'Imię' 66 | users_create_last_name: 'Nazwisko' 67 | users_create_position: 'Stanowisko' 68 | users_create_role: 'Rola' 69 | users_create_password: 'Hasło' 70 | users_create_errors_default: 'Wystąpił błąd!' 71 | users_create_errors_user_exists: 'Użytkownik o takim loginie już istnieje!' 72 | users_list_first_and_last_name: 'Imię i nazwisko' 73 | users_list_email: 'E-mail' 74 | users_list_role: 'Rola' 75 | users_list_created_at: 'Utworzony' 76 | users_select_search: 'Wyszukaj użytkownika' 77 | 78 | settings_general_title: 'Ogólne' 79 | settings_general_role: 'Rola' 80 | settings_general_email: 'E-mail' 81 | settings_general_form_first_name: 'Imię' 82 | settings_general_form_last_name: 'Nazwisko' 83 | settings_general_form_position: 'Stanowisko' 84 | settings_general_form_errors_default: 'Wystąpił błąd!' 85 | settings_password_title: 'Konto' 86 | settings_password_form_password: 'Aktualne hasło' 87 | settings_password_form_new_password: 'Nowe hasło' 88 | settings_password_form_repeat_password: 'Powtórz nowe hasło' 89 | settings_password_form_errors_default: 'Wystąpił błąd!' 90 | settings_password_form_errors_wrong_password: 'Błędne hasło!' -------------------------------------------------------------------------------- /apps/mobile/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { pinia } from './stores'; 3 | import router from './router'; 4 | import { Quasar, Notify, Dialog } from 'quasar'; 5 | import { createI18n } from 'vue-i18n'; 6 | import messages from '@intlify/unplugin-vue-i18n/messages'; 7 | import { configure as veeConfigure } from 'vee-validate'; 8 | 9 | import '@quasar/extras/material-icons/material-icons.css'; 10 | import '@quasar/extras/mdi-v6/mdi-v6.css'; 11 | import 'quasar/src/css/index.sass'; 12 | import './assets/main.scss'; 13 | 14 | import App from './app/App.vue'; 15 | import { config } from './config'; 16 | 17 | veeConfigure({ 18 | validateOnInput: true, 19 | }); 20 | 21 | const i18n = createI18n({ 22 | locale: config.defaultLocale, 23 | legacy: false, 24 | globalInjection: true, 25 | messages, 26 | }); 27 | 28 | const app = createApp(App); 29 | 30 | app.use(i18n); 31 | app.use(pinia); 32 | app.use(router); 33 | 34 | app.use(Quasar, { 35 | plugins: { 36 | Notify, 37 | Dialog, 38 | }, 39 | }); 40 | 41 | app.mount('#app'); 42 | -------------------------------------------------------------------------------- /apps/mobile/src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/mobile/src/public/favicon.ico -------------------------------------------------------------------------------- /apps/mobile/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@workspace/shared'; 2 | import { createRouter, createWebHashHistory } from 'vue-router'; 3 | import { AuthMeta, useAuthGuard, useLoadingIndicator } from './middlewares'; 4 | import { routes } from './routes'; 5 | 6 | declare module 'vue-router' { 7 | export interface RouteMeta { 8 | auth: AuthMeta; 9 | roles?: Role[]; 10 | } 11 | } 12 | 13 | const router = createRouter({ 14 | history: createWebHashHistory(import.meta.env.BASE_URL), 15 | routes, 16 | }); 17 | 18 | useAuthGuard(router); 19 | useLoadingIndicator(router); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /apps/mobile/src/router/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-auth-guard'; 2 | export * from './use-loading-indicator'; 3 | -------------------------------------------------------------------------------- /apps/mobile/src/router/middlewares/use-auth-guard.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'vue-router'; 2 | import { useAccountStore } from '@/stores/account'; 3 | 4 | export enum AuthMeta { 5 | None, 6 | Required, 7 | Optional, 8 | } 9 | 10 | export function useAuthGuard(router: Router) { 11 | router.beforeEach(async (to) => { 12 | const accountStore = useAccountStore(); 13 | accountStore.refreshAuthState(); 14 | 15 | if (!accountStore.state.loaded && accountStore.state.authenticated) 16 | await accountStore.fetch(500); 17 | 18 | if (to.meta.auth === AuthMeta.Required && !accountStore.state.authenticated) { 19 | return { name: 'login' }; 20 | } 21 | 22 | if (to.meta.auth === AuthMeta.None && accountStore.state.authenticated) { 23 | return { name: 'dashboard' }; 24 | } 25 | 26 | if (to.meta.roles && !to.meta.roles.includes(accountStore.state.role)) { 27 | return { name: 'dashboard' }; 28 | } 29 | 30 | if (to.name === 'user' && to.params.id === accountStore.state.id) { 31 | return { name: 'dashboard' }; 32 | } 33 | 34 | return true; 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /apps/mobile/src/router/middlewares/use-loading-indicator.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'vue-router'; 2 | import { useAppStore } from '@/stores/app'; 3 | 4 | export function useLoadingIndicator(router: Router) { 5 | router.beforeEach(() => { 6 | const appStore = useAppStore(); 7 | appStore.state.isRouteLoading = true; 8 | }); 9 | 10 | router.afterEach(() => { 11 | const appStore = useAppStore(); 12 | appStore.state.isRouteLoading = false; 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /apps/mobile/src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | import { AuthMeta } from './middlewares'; 3 | 4 | import DefaultLayout from '@/app/layouts/default.vue'; 5 | import AppLayout from '@/app/layouts/app.vue'; 6 | import LoginPage from '@/app/pages/login.vue'; 7 | 8 | export const routes: RouteRecordRaw[] = [ 9 | { 10 | path: '/', 11 | component: DefaultLayout, 12 | children: [ 13 | { 14 | path: '/', 15 | name: 'login', 16 | component: LoginPage, 17 | meta: { 18 | auth: AuthMeta.None, 19 | }, 20 | }, 21 | ], 22 | }, 23 | { 24 | path: '/app', 25 | component: AppLayout, 26 | children: [ 27 | { 28 | path: '/dashboard', 29 | name: 'dashboard', 30 | component: () => import('@/app/pages/dashboard.vue'), 31 | meta: { 32 | auth: AuthMeta.Required, 33 | }, 34 | }, 35 | { 36 | path: '/settings', 37 | name: 'settings', 38 | component: () => import('@/app/pages/settings.vue'), 39 | meta: { 40 | auth: AuthMeta.Required, 41 | }, 42 | }, 43 | ], 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /apps/mobile/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | 10 | declare module '@intlify/unplugin-vue-i18n/messages' { 11 | import { LocaleMessage } from '@intlify/core-base'; 12 | import { VueMessageType } from 'vue-i18n'; 13 | const messages: { [x: string]: LocaleMessage }; 14 | export default messages; 15 | } 16 | -------------------------------------------------------------------------------- /apps/mobile/src/stores/app.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | import { useTitle } from '@vueuse/core'; 3 | import { defineStore } from 'pinia'; 4 | import { watch } from 'vue'; 5 | import { useI18n } from 'vue-i18n'; 6 | import { useRoute } from 'vue-router'; 7 | 8 | interface State { 9 | networkError: boolean; 10 | isRouteLoading: boolean; 11 | routeTitle: string; 12 | } 13 | 14 | export const useAppStore = defineStore('app', () => { 15 | const route = useRoute(); 16 | const { t } = useI18n(); 17 | const title = useTitle(); 18 | 19 | const state: State = reactive({ 20 | networkError: false, 21 | isRouteLoading: false, 22 | routeTitle: t(`app_name`), 23 | }); 24 | 25 | watch( 26 | () => route.name, 27 | () => { 28 | title.value = t(`routes_${route.name.toString()}`) + ' - ' + t(`app_name`); 29 | state.routeTitle = t(`routes_${route.name.toString()}`); 30 | } 31 | ); 32 | 33 | return { state }; 34 | }); 35 | -------------------------------------------------------------------------------- /apps/mobile/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | 3 | const pinia = createPinia(); 4 | 5 | export { pinia }; 6 | -------------------------------------------------------------------------------- /apps/mobile/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "types": ["node"], 7 | "composite": true 8 | }, 9 | 10 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "../../tsconfig.base.json"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/mobile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["esnext", "dom"], 5 | "target": "esnext", 6 | "jsx": "preserve", 7 | "skipLibCheck": true, 8 | "useDefineForClassFields": true, 9 | 10 | "types": [ 11 | "vite/client", 12 | "vitest", 13 | "@intlify/unplugin-vue-i18n/messages" 14 | ], 15 | 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./src/*"], 19 | "@workspace/shared": ["../../libs/shared/src/index.ts"] 20 | } 21 | }, 22 | 23 | "files": [], 24 | 25 | "include": [ 26 | "./src/components.d.ts", 27 | "**/*.js", 28 | "**/*.jsx", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | "**/*.vue", 32 | "**/*.json" 33 | ], 34 | 35 | "exclude": ["**/*.spec.js", "**/*.spec.jsx", "**/*.spec.ts", "**/*.spec.tsx"], 36 | 37 | "references": [ 38 | { 39 | "path": "./tsconfig.app.json" 40 | }, 41 | { 42 | "path": "./tsconfig.spec.json" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /apps/mobile/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "types": ["vitest/globals", "node"], 7 | "composite": true 8 | }, 9 | 10 | "include": [ 11 | "vite.config.ts", 12 | "**/*.test.ts", 13 | "**/*.spec.ts", 14 | "**/*.test.tsx", 15 | "**/*.spec.tsx", 16 | "**/*.test.js", 17 | "**/*.spec.js", 18 | "**/*.test.jsx", 19 | "**/*.spec.jsx", 20 | "**/*.d.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /apps/mobile/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import path from 'path'; 4 | import { defineConfig } from 'vite'; 5 | import Vue from '@vitejs/plugin-vue'; 6 | import Components from 'unplugin-vue-components/vite'; 7 | import AutoImport from 'unplugin-auto-import/vite'; 8 | import { quasar, transformAssetUrls } from '@quasar/vite-plugin'; 9 | import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; 10 | import checker from 'vite-plugin-checker'; 11 | import viteTsConfigPaths from 'vite-tsconfig-paths'; 12 | 13 | const resolve = (p: string) => path.resolve(__dirname, p); 14 | 15 | export default defineConfig({ 16 | server: { 17 | host: true, 18 | port: 8090, 19 | 20 | fs: { 21 | allow: ['../..'], 22 | }, 23 | }, 24 | 25 | base: './', 26 | cacheDir: '../../node_modules/.vite-mobile', 27 | clearScreen: true, 28 | assetsInclude: /\.(pdf|jpg|png|svg)$/, 29 | 30 | resolve: { 31 | alias: { 32 | '@/': `${resolve('./src')}/`, 33 | }, 34 | }, 35 | 36 | publicDir: resolve('./src/public'), 37 | 38 | plugins: [ 39 | viteTsConfigPaths({ 40 | projects: [resolve('../../tsconfig.base.json')], 41 | }), 42 | 43 | Vue({ 44 | template: { 45 | transformAssetUrls, 46 | }, 47 | }), 48 | 49 | process.env.VITE_DISABLE_VUE_TSC 50 | ? null 51 | : checker({ 52 | vueTsc: true, 53 | }), 54 | 55 | VueI18nPlugin({ 56 | defaultSFCLang: 'yml', 57 | include: resolve('./src/locales/**'), 58 | }), 59 | 60 | quasar({ 61 | sassVariables: resolve('./src/assets/quasar.scss'), 62 | }), 63 | 64 | AutoImport({ 65 | dts: resolve('./src/auto-imports.d.ts'), 66 | imports: ['vue', 'vue-router'], 67 | }), 68 | 69 | Components({ 70 | dts: resolve('./src/components.d.ts'), 71 | dirs: ['src/app/components'], 72 | }), 73 | ], 74 | 75 | test: { 76 | globals: true, 77 | cache: { 78 | dir: '../../node_modules/.vitest', 79 | }, 80 | environment: 'jsdom', 81 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /apps/server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/server/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'server', 4 | preset: '../../jest.preset.js', 5 | 6 | globals: { 7 | 'ts-jest': { 8 | tsconfig: '/tsconfig.spec.json', 9 | }, 10 | }, 11 | 12 | testEnvironment: 'node', 13 | 14 | transform: { 15 | '^.+\\.[tj]s$': 'ts-jest', 16 | }, 17 | 18 | moduleFileExtensions: ['ts', 'js', 'html'], 19 | coverageDirectory: '../../coverage/apps/server', 20 | }; 21 | -------------------------------------------------------------------------------- /apps/server/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/server/src", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/webpack:webpack", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "target": "node", 12 | "compiler": "tsc", 13 | "isolatedConfig": true, 14 | "webpackConfig": "apps/server/webpack.config.js", 15 | "outputPath": "dist/apps/server", 16 | "main": "apps/server/src/main.ts", 17 | "tsConfig": "apps/server/tsconfig.app.json", 18 | "assets": ["apps/server/src/assets", "apps/server/src/static"], 19 | "additionalEntryPoints": [ 20 | { 21 | "entryName": "cli", 22 | "entryPath": "src/cli.ts" 23 | } 24 | ] 25 | }, 26 | "configurations": { 27 | "production": { 28 | "optimization": true, 29 | "extractLicenses": true, 30 | "inspect": false 31 | } 32 | } 33 | }, 34 | "build-cli": { 35 | "executor": "@nx/webpack:webpack", 36 | "outputs": ["{options.outputPath}"], 37 | "options": { 38 | "target": "node", 39 | "compiler": "tsc", 40 | "isolatedConfig": true, 41 | "webpackConfig": "apps/server/webpack.config.js", 42 | "outputPath": "dist/apps/server-cli", 43 | "main": "apps/server/src/cli.ts", 44 | "tsConfig": "apps/server/tsconfig.app.json", 45 | "assets": ["apps/server/src/assets"] 46 | }, 47 | "configurations": { 48 | "production": { 49 | "optimization": true, 50 | "extractLicenses": true, 51 | "inspect": false 52 | } 53 | } 54 | }, 55 | "serve": { 56 | "executor": "@nx/js:node", 57 | "options": { 58 | "buildTarget": "server:build" 59 | } 60 | }, 61 | "lint": { 62 | "executor": "@nx/linter:eslint", 63 | "outputs": ["{options.outputFile}"], 64 | "options": { 65 | "lintFilePatterns": ["apps/server/**/*.ts"], 66 | "fix": true 67 | } 68 | }, 69 | "test": { 70 | "executor": "@nx/jest:jest", 71 | "outputs": ["coverage/apps/server"], 72 | "options": { 73 | "jestConfig": "apps/server/jest.config.ts", 74 | "passWithNoTests": true 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /apps/server/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { staticDir } from '@/common/filesystem'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigModule } from '@nestjs/config'; 4 | import { ServeStaticModule } from '@nestjs/serve-static'; 5 | import { config } from '@/common'; 6 | import { DatabaseModule } from './db/database.module'; 7 | import { CliModule } from './cli/cli.module'; 8 | import { CronModule } from './cron/cron.module'; 9 | import { UsersModule } from './users/users.module'; 10 | import { AuthModule } from './auth/auth.module'; 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forRoot({ 15 | load: [config], 16 | isGlobal: true, 17 | }), 18 | 19 | ServeStaticModule.forRoot({ 20 | rootPath: staticDir(), 21 | exclude: ['/api*'], 22 | }), 23 | 24 | DatabaseModule, 25 | CliModule, 26 | CronModule, 27 | AuthModule, 28 | UsersModule, 29 | ], 30 | 31 | controllers: [], 32 | }) 33 | export class AppModule {} 34 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { AuthLoginDto } from '@workspace/shared'; 2 | import { Body, Controller, Post, Req, Res } from '@nestjs/common'; 3 | import { AuthService } from './auth.service'; 4 | import { Response, Request } from 'express'; 5 | import { AuthSession } from './decorators/auth-session.decorator'; 6 | import { AuthLoginResponse } from '@workspace/shared'; 7 | import { Session } from '@/app/db/session.entity'; 8 | import { Public } from './decorators/public.decorator'; 9 | 10 | @Controller('/auth') 11 | export class AuthController { 12 | constructor(private readonly authService: AuthService) {} 13 | 14 | @Public() 15 | @Post('/login') 16 | async login( 17 | @Req() request: Request, 18 | @Res({ passthrough: true }) response: Response, 19 | @AuthSession() session: Session, 20 | @Body() authLoginDto: AuthLoginDto 21 | ): Promise { 22 | return this.authService.login(request, response, session, authLoginDto); 23 | } 24 | 25 | @Post('/logout') 26 | async logout( 27 | @AuthSession() session: Session, 28 | @Req() request: Request, 29 | @Res({ passthrough: true }) response: Response 30 | ): Promise { 31 | return this.authService.logout(request, response, session); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_GUARD } from '@nestjs/core'; 3 | import { UsersModule } from '@/app/users/users.module'; 4 | import { AuthController } from './auth.controller'; 5 | import { AuthService } from './auth.service'; 6 | import { JwtAuthGuard } from './guards/jwt-auth.guard'; 7 | import { JwtStrategy } from './strategies/jwt.strategy'; 8 | import { RolesGuard } from './guards/roles.guard'; 9 | 10 | @Module({ 11 | imports: [UsersModule], 12 | controllers: [AuthController], 13 | providers: [ 14 | AuthService, 15 | JwtStrategy, 16 | { 17 | provide: APP_GUARD, 18 | useClass: JwtAuthGuard, 19 | }, 20 | { 21 | provide: APP_GUARD, 22 | useClass: RolesGuard, 23 | }, 24 | ], 25 | exports: [AuthService, JwtStrategy], 26 | }) 27 | export class AuthModule {} 28 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/decorators/auth-session.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Session } from '@/app/db/session.entity'; 2 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 3 | 4 | export const AuthSession = createParamDecorator((_data, context: ExecutionContext): Session => { 5 | return context.switchToHttp().getRequest().userSession; 6 | }); 7 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/decorators/auth-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@/app/db/user.entity'; 2 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 3 | 4 | export const AuthUser = createParamDecorator((_data, context: ExecutionContext): User => { 5 | const session = context.switchToHttp().getRequest().userSession; 6 | if (!session) return undefined; 7 | return session.user; 8 | }); 9 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/decorators/public.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const IS_PUBLIC_KEY = 'isPublic'; 4 | export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); 5 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/decorators/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@workspace/shared'; 2 | import { SetMetadata } from '@nestjs/common'; 3 | 4 | export const ROLES_KEY = 'roles'; 5 | export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); 6 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; 5 | 6 | @Injectable() 7 | export class JwtAuthGuard extends AuthGuard('jwt') { 8 | constructor(private reflector: Reflector) { 9 | super({ 10 | property: 'userSession', 11 | }); 12 | } 13 | 14 | handleRequest(err, userSession, _info, context) { 15 | const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ 16 | context.getHandler(), 17 | context.getClass(), 18 | ]); 19 | 20 | if ((err || !userSession) && !isPublic) { 21 | throw err || new UnauthorizedException(); 22 | } 23 | 24 | return userSession; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/guards/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@workspace/shared'; 2 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 3 | import { Reflector } from '@nestjs/core'; 4 | import { ROLES_KEY } from '../decorators/roles.decorator'; 5 | 6 | @Injectable() 7 | export class RolesGuard implements CanActivate { 8 | constructor(private reflector: Reflector) {} 9 | 10 | canActivate(context: ExecutionContext): boolean { 11 | const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ 12 | context.getHandler(), 13 | context.getClass(), 14 | ]); 15 | 16 | if (!requiredRoles) return true; 17 | 18 | const user = context.switchToHttp().getRequest().userSession.user; 19 | return requiredRoles.some((role) => user.role === role); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-jwt'; 2 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { Session } from '@/app/db/session.entity'; 6 | 7 | export interface JwtPayload { 8 | id: string; 9 | } 10 | 11 | function tokenExtractor(req: any): null | string { 12 | const bearerToken = 13 | req.headers.authorization && req.headers.authorization.includes('Bearer ') 14 | ? req.headers.authorization.replace('Bearer ', '') 15 | : null; 16 | 17 | if (bearerToken) return bearerToken; 18 | else return req && req.cookies ? (req.cookies?.jwt as string) ?? null : null; 19 | } 20 | 21 | @Injectable() 22 | export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { 23 | constructor(configService: ConfigService) { 24 | super({ 25 | jwtFromRequest: tokenExtractor, 26 | secretOrKey: configService.get('secrets.jwt'), 27 | }); 28 | } 29 | 30 | async validate(payload: JwtPayload) { 31 | if (!payload || !payload.id) { 32 | throw new UnauthorizedException(); 33 | } 34 | 35 | const userSession = await Session.findOne({ 36 | relations: ['user'], 37 | 38 | where: { 39 | token: payload.id, 40 | }, 41 | }); 42 | 43 | if (!userSession) { 44 | throw new UnauthorizedException(); 45 | } 46 | 47 | userSession.lastSeen = new Date(); 48 | userSession.save(); 49 | 50 | return userSession; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/server/src/app/auth/utils/hash-password.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export function hashPassword(plain: string, key: string): string { 4 | const hmac = crypto.createHmac('sha512', key); 5 | hmac.update(plain); 6 | return hmac.digest('hex'); 7 | } 8 | -------------------------------------------------------------------------------- /apps/server/src/app/cli/cli.module.ts: -------------------------------------------------------------------------------- 1 | import { UsersModule } from '@/app/users/users.module'; 2 | import { Module } from '@nestjs/common'; 3 | import { SeedCommand } from './seed.command'; 4 | 5 | @Module({ 6 | imports: [UsersModule], 7 | providers: [SeedCommand], 8 | }) 9 | export class CliModule {} 10 | -------------------------------------------------------------------------------- /apps/server/src/app/cli/seed.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | import { UsersService } from '@/app/users/users.service'; 3 | import { ConflictException, Logger } from '@nestjs/common'; 4 | import { usersSeedData } from './seed/seed-data'; 5 | import { timeout, UserCreateDto } from '@workspace/shared'; 6 | 7 | @Command({ 8 | name: 'seed', 9 | description: 'Seed initial data to database', 10 | }) 11 | export class SeedCommand extends CommandRunner { 12 | private readonly logger = new Logger(SeedCommand.name); 13 | 14 | constructor(private readonly usersService: UsersService) { 15 | super(); 16 | } 17 | 18 | async run(): Promise { 19 | await this.createTestUsers(usersSeedData); 20 | } 21 | 22 | async createTestUsers(users: UserCreateDto[]) { 23 | for (const _user of users) { 24 | this.logger.verbose(`Creating account: ${_user.email}`); 25 | await timeout(200); 26 | 27 | try { 28 | const user = await this.usersService.createOne(_user); 29 | this.logger.verbose(`Account has been created! ID: ${user.id}`); 30 | } catch (e) { 31 | if (e instanceof ConflictException) { 32 | this.logger.error('The account already exists'); 33 | } else { 34 | this.logger.error(e); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/app/cli/seed/seed-data.ts: -------------------------------------------------------------------------------- 1 | import { Role, UserCreateDto } from '@workspace/shared'; 2 | 3 | export const usersSeedData: UserCreateDto[] = [ 4 | { 5 | email: 'admin@admin.com', 6 | password: '123456', 7 | role: Role.Admin, 8 | firstName: 'John', 9 | lastName: 'Doe', 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /apps/server/src/app/cron/cron.module.ts: -------------------------------------------------------------------------------- 1 | import { ScheduleModule } from '@nestjs/schedule'; 2 | import { Module } from '@nestjs/common'; 3 | import { CronService } from './cron.service'; 4 | import { AuthModule } from '@/app/auth/auth.module'; 5 | 6 | @Module({ 7 | imports: [ScheduleModule.forRoot(), AuthModule], 8 | providers: [CronService], 9 | }) 10 | export class CronModule {} 11 | -------------------------------------------------------------------------------- /apps/server/src/app/cron/cron.service.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from '@/app/auth/auth.service'; 2 | import { Injectable, Logger } from '@nestjs/common'; 3 | import { Cron, CronExpression } from '@nestjs/schedule'; 4 | 5 | @Injectable() 6 | export class CronService { 7 | private readonly logger = new Logger(CronService.name); 8 | 9 | constructor(private readonly authService: AuthService) {} 10 | 11 | @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) 12 | async cleanExpiredSessions() { 13 | const affected = await this.authService.cleanExpiredSessions(); 14 | this.logger.log(`Cleaned up ${affected} expired user sessions`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/server/src/app/db/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; 4 | import { Session } from './session.entity'; 5 | import { User } from './user.entity'; 6 | 7 | const entities: unknown[] = [User, Session]; 8 | 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forRootAsync({ 12 | imports: [ConfigModule], 13 | inject: [ConfigService], 14 | 15 | useFactory: async (configService: ConfigService) => { 16 | return { 17 | type: configService.get('db.type'), 18 | host: configService.get('db.host'), 19 | port: configService.get('db.port'), 20 | database: configService.get('db.name'), 21 | username: configService.get('db.user'), 22 | password: configService.get('db.password'), 23 | synchronize: configService.get('db.synchronize'), 24 | bigNumberStrings: false, 25 | entities: entities, 26 | } as TypeOrmModuleOptions; 27 | }, 28 | }), 29 | ], 30 | 31 | controllers: [], 32 | providers: [], 33 | }) 34 | export class DatabaseModule {} 35 | -------------------------------------------------------------------------------- /apps/server/src/app/db/session.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | Index, 6 | JoinColumn, 7 | ManyToOne, 8 | PrimaryGeneratedColumn, 9 | } from 'typeorm'; 10 | import { User } from './user.entity'; 11 | 12 | @Entity({ name: 'sessions' }) 13 | export class Session extends BaseEntity { 14 | @PrimaryGeneratedColumn('uuid') 15 | id: string; 16 | 17 | @ManyToOne(() => User, (user) => user.sessions, { nullable: false }) 18 | @JoinColumn() 19 | @Index() 20 | user: User; 21 | 22 | @Column({ 23 | nullable: true, 24 | default: null, 25 | }) 26 | token: string; 27 | 28 | @Column({ 29 | type: 'bigint', 30 | nullable: true, 31 | default: null, 32 | }) 33 | exp: number; 34 | 35 | @Column({ 36 | default: false, 37 | }) 38 | cookies: boolean; 39 | 40 | @Column({ 41 | default: () => 'CURRENT_TIMESTAMP', 42 | }) 43 | lastSeen: Date; 44 | 45 | @Column({ 46 | default: () => 'CURRENT_TIMESTAMP', 47 | }) 48 | createdAt: Date; 49 | } 50 | -------------------------------------------------------------------------------- /apps/server/src/app/db/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@workspace/shared'; 2 | import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; 3 | import { Session } from './session.entity'; 4 | 5 | @Entity({ name: 'users' }) 6 | export class User extends BaseEntity { 7 | @PrimaryGeneratedColumn('uuid') 8 | id: string; 9 | 10 | @OneToMany(() => Session, (session) => session.user) 11 | sessions: Session[]; 12 | 13 | @Column({ 14 | type: 'enum', 15 | enum: Role, 16 | default: Role.User, 17 | }) 18 | role: Role; 19 | 20 | @Column({ 21 | length: 255, 22 | }) 23 | email: string; 24 | 25 | @Column() 26 | hash: string; 27 | 28 | @Column() 29 | firstName: string; 30 | 31 | @Column() 32 | lastName: string; 33 | 34 | @Column({ 35 | nullable: true, 36 | }) 37 | position: string; 38 | 39 | @Column({ 40 | default: true, 41 | }) 42 | isActive: boolean; 43 | 44 | @Column({ 45 | default: () => 'CURRENT_TIMESTAMP', 46 | }) 47 | createdAt: Date; 48 | } 49 | -------------------------------------------------------------------------------- /apps/server/src/app/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | ForbiddenException, 6 | Get, 7 | Param, 8 | ParseUUIDPipe, 9 | Post, 10 | Put, 11 | Query, 12 | } from '@nestjs/common'; 13 | import { UsersService } from './users.service'; 14 | import { AuthUser } from '@/app/auth/decorators/auth-user.decorator'; 15 | import { User } from '@/app/db/user.entity'; 16 | import { 17 | UserCreateDto, 18 | UserProfileResponse, 19 | UserUpdateDto, 20 | UserUpdateSelfDto, 21 | UserUpdateSelfPasswordDto, 22 | PaginationResponse, 23 | PaginationDto, 24 | } from '@workspace/shared'; 25 | 26 | @Controller('/users') 27 | export class UsersController { 28 | constructor(private readonly usersService: UsersService) {} 29 | 30 | @Get('/self') 31 | async getSelf(@AuthUser() user: User) { 32 | return this.usersService.getProfile(user); 33 | } 34 | 35 | @Put('/self') 36 | async updateSelf( 37 | @AuthUser() user: User, 38 | @Body() userUpdateSelfDto: UserUpdateSelfDto 39 | ): Promise { 40 | return await this.usersService.updateSelf(user, userUpdateSelfDto); 41 | } 42 | 43 | @Put('/self/password') 44 | async updateSelfPassword( 45 | @AuthUser() user: User, 46 | @Body() userUpdateSelfPasswordDto: UserUpdateSelfPasswordDto 47 | ): Promise { 48 | return await this.usersService.updateSelfPassword(user, userUpdateSelfPasswordDto); 49 | } 50 | 51 | @Post('/') 52 | async createOne(@Body() userCreateDto: UserCreateDto): Promise { 53 | return await this.usersService.createOne(userCreateDto); 54 | } 55 | 56 | @Get('/') 57 | async getMany( 58 | @Query() paginationDto: PaginationDto 59 | ): Promise> { 60 | return await this.usersService.getMany(paginationDto); 61 | } 62 | 63 | @Get('/:id') 64 | async getOne(@Param('id', ParseUUIDPipe) id: string): Promise { 65 | return await this.usersService.getOne(id); 66 | } 67 | 68 | @Put('/:id') 69 | async updateOne( 70 | @AuthUser() user: User, 71 | @Param('id', ParseUUIDPipe) id: string, 72 | @Body() userUpdateDto: UserUpdateDto 73 | ): Promise { 74 | if (id === user.id) throw new ForbiddenException(); 75 | return await this.usersService.updateOne(id, userUpdateDto); 76 | } 77 | 78 | @Delete('/:id') 79 | async deleteOne(@Param('id', ParseUUIDPipe) id: string): Promise { 80 | return await this.usersService.deleteOne(id); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /apps/server/src/app/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersController } from './users.controller'; 3 | import { UsersService } from './users.service'; 4 | 5 | @Module({ 6 | controllers: [UsersController], 7 | providers: [UsersService], 8 | exports: [UsersService], 9 | }) 10 | export class UsersModule {} 11 | -------------------------------------------------------------------------------- /apps/server/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/server/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/server/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { CommandFactory } from 'nest-commander'; 2 | import { AppModule } from './app/app.module'; 3 | import 'multer'; 4 | 5 | async function bootstrap() { 6 | await CommandFactory.run(AppModule, ['verbose', 'error', 'warn']); 7 | } 8 | 9 | bootstrap(); 10 | -------------------------------------------------------------------------------- /apps/server/src/common/config.ts: -------------------------------------------------------------------------------- 1 | import { object, number, string, boolean, ValidationError, array } from 'yup'; 2 | 3 | const configSchema = object().shape({ 4 | http: object({ 5 | port: number().nullable().optional().default(3000), 6 | ssl: boolean().nullable().optional().default(false), 7 | key: string().nullable().optional(), 8 | cert: string().nullable().optional(), 9 | cors: array() 10 | .nullable() 11 | .optional() 12 | .default(['http://localhost']) 13 | .transform((_, v) => { 14 | return JSON.parse(v); 15 | }) 16 | .of(string()), 17 | }), 18 | 19 | db: object({ 20 | type: string().nullable().optional().default('postgres'), 21 | host: string().nullable().optional().default('localhost'), 22 | port: number().nullable().optional().default(3000), 23 | name: string().required(), 24 | user: string().required(), 25 | password: string().required(), 26 | synchronize: boolean().nullable().optional().default(true), 27 | }), 28 | 29 | secrets: object({ 30 | pwdsalt: string().required(), 31 | jwt: string().required(), 32 | }), 33 | }); 34 | 35 | export const config = () => { 36 | try { 37 | const _config = { 38 | http: { 39 | port: process.env.NEST_API_HTTP_PORT, 40 | ssl: process.env.NEST_API_HTTP_SSL, 41 | key: process.env.NEST_API_HTTP_KEY, 42 | cert: process.env.NEST_API_HTTP_CERT, 43 | cors: process.env.NEST_API_HTTP_CORS, 44 | }, 45 | 46 | db: { 47 | type: process.env.DATABASE_TYPE, 48 | host: process.env.DATABASE_HOST, 49 | port: process.env.DATABASE_PORT, 50 | 51 | name: process.env.DATABASE_NAME, 52 | user: process.env.DATABASE_USER, 53 | password: process.env.DATABASE_PASSWORD, 54 | synchronize: process.env.DATABASE_ENABLE_SYNC, 55 | }, 56 | 57 | secrets: { 58 | pwdsalt: process.env.NEST_API_SECRETS_PWDSALT, 59 | jwt: process.env.NEST_API_SECRETS_JWT, 60 | }, 61 | }; 62 | 63 | return configSchema.validateSync(_config); 64 | } catch (err) { 65 | throw new Error((err as ValidationError).message); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /apps/server/src/common/filesystem.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | interface NodeProcess extends NodeJS.Process { 4 | pkg?: { 5 | entrypoint: string; 6 | defaultEntrypoint: string; 7 | }; 8 | } 9 | 10 | export function realDirname(): string { 11 | if ((process as NodeProcess).pkg) return process.cwd(); 12 | return __dirname; 13 | } 14 | 15 | export function assetsDir(): string { 16 | return path.join(realDirname(), 'assets'); 17 | } 18 | 19 | export function staticDir(): string { 20 | return path.join(realDirname(), 'static'); 21 | } 22 | -------------------------------------------------------------------------------- /apps/server/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './pipes/yup-validation.pipe'; 3 | -------------------------------------------------------------------------------- /apps/server/src/common/pipes/yup-validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, Injectable, PipeTransform, BadRequestException } from '@nestjs/common'; 2 | import { ValidationError } from 'yup'; 3 | 4 | interface Error { 5 | path: string; 6 | message: string; 7 | } 8 | 9 | const serializeValidationError = (err: ValidationError) => { 10 | const invalid: Error[] = err.inner.map(({ path, message }) => ({ 11 | path, 12 | message, 13 | })); 14 | 15 | return invalid; 16 | }; 17 | 18 | @Injectable() 19 | export class YupValidationPipe implements PipeTransform { 20 | async transform(value: any, { metatype }: ArgumentMetadata) { 21 | const { schema } = metatype.prototype; 22 | if (!schema) return value; 23 | 24 | try { 25 | return await schema.validate(value, { abortEarly: false }); 26 | } catch (err) { 27 | throw new BadRequestException(serializeValidationError(err as ValidationError)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/server/src/main.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as http from 'http'; 3 | import * as https from 'https'; 4 | import fs from 'fs'; 5 | import { NestFactory } from '@nestjs/core'; 6 | import { ConfigService } from '@nestjs/config'; 7 | import cookieParser from 'cookie-parser'; 8 | import helmet from 'helmet'; 9 | import { AppModule } from './app/app.module'; 10 | import { YupValidationPipe } from './common'; 11 | import { ExpressAdapter } from '@nestjs/platform-express'; 12 | import { Logger } from '@nestjs/common'; 13 | 14 | async function bootstrap() { 15 | const server = express(); 16 | const app = await NestFactory.create(AppModule, new ExpressAdapter(server)); 17 | 18 | const configService = app.get(ConfigService); 19 | const logger = new Logger('APP'); 20 | 21 | const port = configService.get('http.port'); 22 | const origins = configService.get('http.cors'); 23 | const secure = configService.get('http.secure'); 24 | const keyPath = configService.get('http.key'); 25 | const certPath = configService.get('http.cert'); 26 | 27 | app.setGlobalPrefix('/api'); 28 | 29 | app.enableCors({ 30 | origin: origins, 31 | credentials: true, 32 | }); 33 | 34 | app.use( 35 | helmet.contentSecurityPolicy({ 36 | useDefaults: true, 37 | directives: { 38 | 'img-src': [`'self'`, 'https: data:'], 39 | }, 40 | }) 41 | ); 42 | 43 | app.use(cookieParser()); 44 | app.useGlobalPipes(new YupValidationPipe()); 45 | 46 | await app.init(); 47 | 48 | if (secure) { 49 | try { 50 | const httpsOptions = { 51 | key: fs.readFileSync(keyPath), 52 | cert: fs.readFileSync(certPath), 53 | }; 54 | 55 | https.createServer(httpsOptions, server).listen(port); 56 | 57 | logger.log( 58 | `🚀 The server was started in \x1b[36mHTTPS\x1b[32m mode on port \x1b[36m${port}` 59 | ); 60 | } catch (e) { 61 | logger.error(`The HTTPS server cannot be started`); 62 | logger.error(e); 63 | } 64 | } else { 65 | http.createServer(server).listen(configService.get('http.port')); 66 | 67 | logger.log( 68 | `🚀 The server was started in \x1b[35mHTTP\x1b[32m mode on port \x1b[36m${port}` 69 | ); 70 | } 71 | } 72 | 73 | bootstrap(); 74 | -------------------------------------------------------------------------------- /apps/server/src/static/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/server/src/static/index.html -------------------------------------------------------------------------------- /apps/server/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": ["node"] 8 | }, 9 | 10 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | 7 | "paths": { 8 | "@/*": ["./src/*"], 9 | "@workspace/shared": ["../../libs/shared/src/index.ts"] 10 | } 11 | }, 12 | 13 | "files": [], 14 | "include": [], 15 | "references": [ 16 | { 17 | "path": "./tsconfig.app.json" 18 | }, 19 | { 20 | "path": "./tsconfig.spec.json" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /apps/server/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { composePlugins, withNx } = require('@nx/webpack'); 2 | 3 | // Nx plugins for webpack. 4 | module.exports = composePlugins(withNx(), (config) => { 5 | // Update the webpack config as needed here. 6 | // e.g. `config.plugins.push(new MyPlugin())` 7 | return config; 8 | }); 9 | -------------------------------------------------------------------------------- /apps/web/.build.env: -------------------------------------------------------------------------------- 1 | VITE_DISABLE_VUE_TSC=true -------------------------------------------------------------------------------- /apps/web/.electron.env: -------------------------------------------------------------------------------- 1 | VITE_IS_ELECTRON_APP=true -------------------------------------------------------------------------------- /apps/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../../.eslintrc.json" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | DIST: string; 6 | /** /dist/ or /public/ */ 7 | PUBLIC: string; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/electron/main/index.ts: -------------------------------------------------------------------------------- 1 | process.env.DIST = join(__dirname, '../..'); 2 | console.log(process.env.DIST); 3 | process.env.PUBLIC = app.isPackaged 4 | ? process.env.DIST 5 | : join(__dirname, '../../../../../apps/web/src/public'); 6 | 7 | import { app, BrowserWindow, shell, ipcMain } from 'electron'; 8 | import { release } from 'os'; 9 | import { join } from 'path'; 10 | 11 | // Disable GPU Acceleration for Windows 7 12 | if (release().startsWith('6.1')) app.disableHardwareAcceleration(); 13 | 14 | // Set application name for Windows 10+ notifications 15 | if (process.platform === 'win32') app.setAppUserModelId(app.getName()); 16 | 17 | if (!app.requestSingleInstanceLock()) { 18 | app.quit(); 19 | process.exit(0); 20 | } 21 | 22 | // Remove electron security warnings 23 | // This warning only shows in development mode 24 | // Read more on https://www.electronjs.org/docs/latest/tutorial/security 25 | process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'; 26 | 27 | let win: BrowserWindow | null = null; 28 | // Here, you can also use other preload 29 | const preload = join(__dirname, '../preload/index.js'); 30 | const url = process.env.VITE_DEV_SERVER_URL as string; 31 | const indexHtml = join(process.env.DIST, 'index.html'); 32 | 33 | async function createWindow() { 34 | win = new BrowserWindow({ 35 | width: 1280, 36 | height: 800, 37 | autoHideMenuBar: true, 38 | icon: join(process.env.PUBLIC, 'favicon.ico'), 39 | webPreferences: { 40 | preload, 41 | // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production 42 | // Consider using contextBridge.exposeInMainWorld 43 | // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation 44 | nodeIntegration: true, 45 | contextIsolation: false, 46 | }, 47 | }); 48 | 49 | if (app.isPackaged) { 50 | win.loadFile(indexHtml); 51 | } else { 52 | win.loadURL(url); 53 | // Open devTool if the app is not packaged 54 | win.webContents.openDevTools(); 55 | } 56 | 57 | // Test actively push message to the Electron-Renderer 58 | win.webContents.on('did-finish-load', () => { 59 | win?.webContents.send('main-process-message', new Date().toLocaleString()); 60 | }); 61 | 62 | // Make all links open with the browser, not with the application 63 | win.webContents.setWindowOpenHandler(({ url }) => { 64 | if (url.startsWith('https:')) shell.openExternal(url); 65 | return { action: 'deny' }; 66 | }); 67 | } 68 | 69 | app.whenReady().then(createWindow); 70 | 71 | app.on('window-all-closed', () => { 72 | win = null; 73 | if (process.platform !== 'darwin') app.quit(); 74 | }); 75 | 76 | app.on('second-instance', () => { 77 | if (win) { 78 | // Focus on the main window if the user tried to open another 79 | if (win.isMinimized()) win.restore(); 80 | win.focus(); 81 | } 82 | }); 83 | 84 | app.on('activate', () => { 85 | const allWindows = BrowserWindow.getAllWindows(); 86 | if (allWindows.length) { 87 | allWindows[0].focus(); 88 | } else { 89 | createWindow(); 90 | } 91 | }); 92 | 93 | // new window example arg: new windows url 94 | ipcMain.handle('open-win', (_event, arg) => { 95 | const childWindow = new BrowserWindow({ 96 | webPreferences: { 97 | preload, 98 | }, 99 | }); 100 | 101 | if (app.isPackaged) { 102 | childWindow.loadFile(indexHtml, { hash: arg }); 103 | } else { 104 | childWindow.loadURL(`${url}/#${arg}`); 105 | // childWindow.webContents.openDevTools({ mode: "undocked", activate: true }) 106 | } 107 | }); 108 | -------------------------------------------------------------------------------- /apps/web/electron/preload/index.ts: -------------------------------------------------------------------------------- 1 | function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { 2 | return new Promise((resolve) => { 3 | if (condition.includes(document.readyState)) { 4 | resolve(true); 5 | } else { 6 | document.addEventListener('readystatechange', () => { 7 | if (condition.includes(document.readyState)) { 8 | resolve(true); 9 | } 10 | }); 11 | } 12 | }); 13 | } 14 | 15 | const safeDOM = { 16 | append(parent: HTMLElement, child: HTMLElement) { 17 | if (!Array.from(parent.children).find((e) => e === child)) { 18 | return parent.appendChild(child); 19 | } 20 | 21 | return null; 22 | }, 23 | remove(parent: HTMLElement, child: HTMLElement) { 24 | if (Array.from(parent.children).find((e) => e === child)) { 25 | return parent.removeChild(child); 26 | } 27 | 28 | return null; 29 | }, 30 | }; 31 | 32 | /** 33 | * https://tobiasahlin.com/spinkit 34 | * https://connoratherton.com/loaders 35 | * https://projects.lukehaas.me/css-loaders 36 | * https://matejkustec.github.io/SpinThatShit 37 | */ 38 | function useLoading() { 39 | const className = `lds-ring`; 40 | const styleContent = ` 41 | .app-loading-wrap { 42 | position: fixed; 43 | top: 0; 44 | left: 0; 45 | width: 100vw; 46 | height: 100vh; 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | background: #111827; 51 | z-index: 9; 52 | } 53 | 54 | .${className} { 55 | display: inline-block; 56 | position: relative; 57 | width: 64px; 58 | height: 64px; 59 | } 60 | 61 | .${className} div { 62 | box-sizing: border-box; 63 | display: block; 64 | position: absolute; 65 | width: 48px; 66 | height: 48px; 67 | margin: 8px; 68 | border: 3px solid #4F46E5; 69 | border-radius: 50%; 70 | animation: ${className} 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 71 | border-color: #4F46E5 transparent transparent transparent; 72 | } 73 | 74 | .${className} div:nth-child(1) { 75 | animation-delay: -0.45s; 76 | } 77 | 78 | .${className} div:nth-child(2) { 79 | animation-delay: -0.3s; 80 | } 81 | 82 | .${className} div:nth-child(3) { 83 | animation-delay: -0.15s; 84 | } 85 | 86 | @keyframes ${className} { 87 | 0% { 88 | transform: rotate(0deg); 89 | } 90 | 100% { 91 | transform: rotate(360deg); 92 | } 93 | } 94 | `; 95 | 96 | const oStyle = document.createElement('style'); 97 | oStyle.innerHTML = styleContent; 98 | 99 | const oDiv = document.createElement('div'); 100 | oDiv.className = 'app-loading-wrap'; 101 | oDiv.innerHTML = `
`; 102 | 103 | return { 104 | appendLoading() { 105 | safeDOM.append(document.head, oStyle); 106 | safeDOM.append(document.body, oDiv); 107 | }, 108 | removeLoading() { 109 | safeDOM.remove(document.head, oStyle); 110 | safeDOM.remove(document.body, oDiv); 111 | }, 112 | }; 113 | } 114 | 115 | // ---------------------------------------------------------------------- 116 | 117 | const { appendLoading, removeLoading } = useLoading(); 118 | domReady().then(appendLoading); 119 | 120 | window.onmessage = (ev) => { 121 | ev.data.payload === 'removeLoading' && removeLoading(); 122 | }; 123 | 124 | setTimeout(removeLoading, 30000); 125 | -------------------------------------------------------------------------------- /apps/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nx-nestjs-vue 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/web/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/web/src", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/vite:build", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/apps/web", 12 | "configFile": "apps/web/vite.config.ts" 13 | }, 14 | "configurations": { 15 | "production": {} 16 | } 17 | }, 18 | "serve": { 19 | "executor": "@nx/vite:dev-server", 20 | "options": { 21 | "buildTarget": "web:build", 22 | "open": true 23 | } 24 | }, 25 | "electron": { 26 | "executor": "nx:run-commands", 27 | "options": { 28 | "commands": [ 29 | "nx serve web" 30 | ], 31 | "parallel": false 32 | }, 33 | "configurations": { 34 | "build": { 35 | "commands": [ 36 | "nx build web", 37 | "electron-builder" 38 | ] 39 | } 40 | } 41 | }, 42 | "lint": { 43 | "executor": "@nx/linter:eslint", 44 | "outputs": ["{options.outputFile}"], 45 | "options": { 46 | "lintFilePatterns": ["apps/web/**/*.{ts,js,tsx,jsx,vue}"], 47 | "fix": true 48 | } 49 | }, 50 | "test": { 51 | "executor": "@nx/vite:test", 52 | "outputs": ["coverage/apps/web"], 53 | "options": { 54 | "passWithNoTests": true, 55 | "reportsDirectory": "../../coverage/apps/web" 56 | } 57 | }, 58 | "typecheck": { 59 | "executor": "nx:run-commands", 60 | "options": { 61 | "commands": [ 62 | { 63 | "command": "vue-tsc --noEmit -p tsconfig.json" 64 | } 65 | ], 66 | "cwd": "apps/web" 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apps/web/src/app/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 49 | -------------------------------------------------------------------------------- /apps/web/src/app/components/AppAccountMenu.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 61 | 62 | 71 | -------------------------------------------------------------------------------- /apps/web/src/app/components/AppCard.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /apps/web/src/app/components/AppDialog.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 91 | -------------------------------------------------------------------------------- /apps/web/src/app/components/AppNetworkError.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 17 | -------------------------------------------------------------------------------- /apps/web/src/app/components/AppPage.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /apps/web/src/app/components/AppPageHeader.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /apps/web/src/app/components/AvatarChip.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 41 | 42 | 52 | -------------------------------------------------------------------------------- /apps/web/src/app/components/headers/DashboardHeader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | -------------------------------------------------------------------------------- /apps/web/src/app/components/headers/UserHeader.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /apps/web/src/app/components/inputs/VInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 46 | -------------------------------------------------------------------------------- /apps/web/src/app/components/inputs/VSelect.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 49 | -------------------------------------------------------------------------------- /apps/web/src/app/layouts/app.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 38 | -------------------------------------------------------------------------------- /apps/web/src/app/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /apps/web/src/app/pages/dashboard.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /apps/web/src/app/pages/users/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /apps/web/src/assets/main.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap'); 2 | 3 | body:not(i) { 4 | font-family: 'Poppins', sans-serif; 5 | } 6 | 7 | .app-header { 8 | box-shadow: 0 12px 12px -12px rgb(0 0 0 / 20%); 9 | } 10 | 11 | .app-drawer { 12 | background-color: #111827; 13 | color: #ffffff; 14 | box-shadow: 0px 0px 12px rgb(0 0 0 / 20%); 15 | } 16 | 17 | .app-page-container { 18 | background-color: #f1f5f9; 19 | min-height: 100px; 20 | } 21 | 22 | .container { 23 | margin: auto; 24 | padding: 38px; 25 | max-width: 1600px; 26 | 27 | @media (max-width: 600px){ 28 | padding: 18px; 29 | } 30 | } 31 | 32 | .border-bottom-accent { 33 | border-bottom: 1px solid #e2e8f0; 34 | } 35 | 36 | .q-card { 37 | border-radius: 5px; 38 | box-shadow: 0px 2px 3px -3px rgba(66, 68, 90, 0.72); 39 | display: flex; 40 | flex-direction: column; 41 | border-radius: 25px !important; 42 | 43 | .title-bar { 44 | display: flex; 45 | align-items: center; 46 | min-height: 80px; 47 | padding-left: 30px; 48 | padding-right: 30px; 49 | font-weight: 600; 50 | border-bottom: 1px solid #E6E6E6; 51 | 52 | &:empty { 53 | min-height: 0; 54 | border-bottom: none; 55 | } 56 | 57 | h5 { 58 | margin: 0; 59 | font-weight: 500; 60 | font-size: 20px; 61 | } 62 | } 63 | 64 | >form, >fragment { 65 | height: 100%; 66 | display: flex; 67 | flex-direction: column; 68 | } 69 | 70 | .q-card__section { 71 | padding: 10px 30px; 72 | 73 | &:first-child { 74 | padding-top: 30px; 75 | } 76 | 77 | &:last-child { 78 | padding-bottom: 30px; 79 | } 80 | } 81 | 82 | .q-card__actions { 83 | display: flex; 84 | padding: 0; 85 | padding-left: 30px; 86 | padding-right: 30px; 87 | align-items: center; 88 | min-height: 80px !important; 89 | border-top: 1px solid #E6E6E6; 90 | 91 | .q-btn { 92 | font-size: 14px; 93 | padding: 6px 15px 6px 15px; 94 | } 95 | } 96 | } 97 | 98 | body.platform-android:not(.native-mobile) .q-dialog__inner--minimized > div { 99 | max-height: 100%; 100 | } 101 | 102 | .q-dialog-max-width { 103 | width: 100%; 104 | max-width: 500px; 105 | } 106 | 107 | .q-dialog__inner { 108 | >div { 109 | overflow: hidden; 110 | } 111 | } 112 | 113 | .q-pagination { 114 | .q-btn { 115 | font-size: 14px; 116 | line-height: 13px; 117 | padding: 8px !important; 118 | } 119 | } 120 | 121 | .q-dialog-max-width { 122 | width: 100%; 123 | max-width: 500px; 124 | } 125 | -------------------------------------------------------------------------------- /apps/web/src/assets/quasar.scss: -------------------------------------------------------------------------------- 1 | $primary : #4F46E5; 2 | $secondary : #111827; 3 | $accent : #9C27B0; 4 | 5 | $dark : #1D1D1D; 6 | 7 | $positive : #21BA45; 8 | $negative : #EF5350; 9 | $info : #31CCEC; 10 | $warning : #F2C037; 11 | -------------------------------------------------------------------------------- /apps/web/src/common/api/client.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@/config'; 2 | import axios from 'axios'; 3 | 4 | export const $axios = axios.create({ 5 | baseURL: `${config.api.host}:${config.api.port}/api`, 6 | withCredentials: true, 7 | }); 8 | 9 | export function setAuthorizationToken(token: string): void { 10 | $axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 11 | } 12 | 13 | export function clearAuthorizationToken(): void { 14 | $axios.defaults.headers.common['Authorization'] = undefined; 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/common/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './interceptors'; 3 | export * from './response-error'; 4 | 5 | import * as auth from './modules/auth'; 6 | import * as users from './modules/users'; 7 | 8 | export const api = { 9 | auth, 10 | users, 11 | }; 12 | -------------------------------------------------------------------------------- /apps/web/src/common/api/interceptors.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import { timeout } from '@workspace/shared'; 3 | import { $axios } from './client'; 4 | import { ResponseError } from './response-error'; 5 | 6 | type ResponseSuccessCallback = (response: AxiosResponse) => void; 7 | type ResponseErrorCallback = (error: ResponseError) => void; 8 | 9 | interface CallbackTrigger { 10 | responseSuccess: ResponseSuccessCallback; 11 | responseError: ResponseErrorCallback; 12 | } 13 | 14 | const callbackTrigger: CallbackTrigger = { 15 | responseSuccess: null, 16 | responseError: null, 17 | }; 18 | 19 | $axios.interceptors.response.use( 20 | (response: AxiosResponse) => { 21 | if (callbackTrigger.responseSuccess) callbackTrigger.responseSuccess(response); 22 | return response; 23 | }, 24 | 25 | async (error: ResponseError) => { 26 | if (error.response && error.response.status !== 0) { 27 | error.isNetworkError = false; 28 | if (callbackTrigger.responseError) callbackTrigger.responseError(error); 29 | return Promise.reject(error); 30 | } else { 31 | error.isNetworkError = true; 32 | if (callbackTrigger.responseError) callbackTrigger.responseError(error); 33 | await timeout(5000); 34 | return await $axios.request(error.config); 35 | } 36 | } 37 | ); 38 | 39 | export function onResponseSuccess(callback: ResponseSuccessCallback): void { 40 | callbackTrigger.responseSuccess = callback; 41 | } 42 | 43 | export function onResponseError(callback: ResponseErrorCallback): void { 44 | callbackTrigger.responseError = callback; 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/src/common/api/modules/auth.ts: -------------------------------------------------------------------------------- 1 | import { AuthLoginDto, AuthLoginResponse } from '@workspace/shared'; 2 | import { $axios } from '../client'; 3 | 4 | export async function login(authLoginDto: AuthLoginDto) { 5 | return await $axios.post('/auth/login', authLoginDto); 6 | } 7 | 8 | export async function logout() { 9 | return await $axios.post('/auth/logout'); 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/common/api/modules/users.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PaginationDto, 3 | PaginationResponse, 4 | UserCreateDto, 5 | UserProfileResponse, 6 | UserUpdateDto, 7 | UserUpdateSelfDto, 8 | UserUpdateSelfPasswordDto, 9 | } from '@workspace/shared'; 10 | 11 | import { $axios } from '../client'; 12 | 13 | export async function getSelf() { 14 | return await $axios.get('/users/self'); 15 | } 16 | 17 | export async function updateSelf(data: UserUpdateSelfDto) { 18 | return await $axios.put('/users/self', data); 19 | } 20 | 21 | export async function updateSelfPassword(data: UserUpdateSelfPasswordDto) { 22 | return await $axios.put('/users/self/password', data); 23 | } 24 | 25 | export async function createOne(data: UserCreateDto) { 26 | return await $axios.post('/users/', data); 27 | } 28 | 29 | export async function getMany(data: PaginationDto) { 30 | return await $axios.get>(`/users/`, { 31 | params: data, 32 | }); 33 | } 34 | 35 | export async function getOne(id: string) { 36 | return await $axios.get(`/users/${id}`); 37 | } 38 | 39 | export async function updateOne(id: string, data: UserUpdateDto) { 40 | return await $axios.put(`/users/${id}`, data); 41 | } 42 | 43 | export async function deleteOne(id: string) { 44 | return await $axios.delete(`/users/${id}`); 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/src/common/api/response-error.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | 3 | export interface ResponseError extends AxiosError { 4 | isNetworkError: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/common/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-promise-state'; 2 | export * from './use-logout-action'; 3 | -------------------------------------------------------------------------------- /apps/web/src/common/hooks/use-logout-action.ts: -------------------------------------------------------------------------------- 1 | import { useAccountStore } from '@/stores/account'; 2 | import { useQuasar, DialogChainObject } from 'quasar'; 3 | import { reactive } from 'vue'; 4 | import { useI18n } from 'vue-i18n'; 5 | import { useRouter } from 'vue-router'; 6 | import { api } from '../api'; 7 | import { usePromiseState, UsePromiseStateRetrun } from './use-promise-state'; 8 | 9 | export interface UseLogoutActionReturn 10 | extends UsePromiseStateRetrun { 11 | logout: () => void; 12 | } 13 | 14 | export function useLogoutAction(): UseLogoutActionReturn { 15 | const router = useRouter(); 16 | const $q = useQuasar(); 17 | const { t } = useI18n(); 18 | 19 | const accountStore = useAccountStore(); 20 | 21 | let dialog: DialogChainObject | undefined = undefined; 22 | 23 | function showLogoutDialog(): void { 24 | dialog = $q.dialog({ 25 | message: t('account_logout_progress'), 26 | progress: true, 27 | persistent: true, 28 | ok: false, 29 | color: 'primary', 30 | }); 31 | } 32 | 33 | function hideLogoutDialog(): void { 34 | if (dialog) dialog.hide(); 35 | } 36 | 37 | const logoutAction = usePromiseState( 38 | async () => { 39 | await api.auth.logout(); 40 | accountStore.setAuthenticated(false); 41 | hideLogoutDialog(); 42 | 43 | $q.notify({ 44 | icon: 'mdi-check', 45 | color: 'positive', 46 | message: t('account_logout_success'), 47 | timeout: 1000, 48 | }); 49 | 50 | router.push({ name: 'login' }); 51 | }, 52 | () => { 53 | hideLogoutDialog(); 54 | 55 | $q.notify({ 56 | icon: 'mdi-close', 57 | color: 'negative', 58 | message: t('account_logout_failed'), 59 | timeout: 1000, 60 | }); 61 | } 62 | ); 63 | 64 | function logout(): void { 65 | showLogoutDialog(); 66 | logoutAction.execute(500); 67 | } 68 | 69 | return reactive({ logout, ...logoutAction }); 70 | } 71 | -------------------------------------------------------------------------------- /apps/web/src/common/hooks/use-promise-state.ts: -------------------------------------------------------------------------------- 1 | import { timeout } from '@workspace/shared'; 2 | import { reactive, ref, shallowRef } from 'vue'; 3 | import { createEventHook, EventHookOn } from '@vueuse/core'; 4 | 5 | export interface UsePromiseStateRetrun { 6 | isReady: boolean; 7 | isLoading: boolean; 8 | counter: number; 9 | startTime: number; 10 | endTime: number; 11 | state: TResult; 12 | error: TError; 13 | onError: EventHookOn; 14 | execute: (delay?: number, payload?: TPayload) => Promise; 15 | } 16 | 17 | export function usePromiseState( 18 | promise: (payload?: any) => Promise, 19 | onError?: (e: TError) => void 20 | ): UsePromiseStateRetrun { 21 | const isReady = ref(false); 22 | const isLoading = ref(false); 23 | const counter = ref(0); 24 | const startTime = ref(undefined); 25 | const endTime = ref(undefined); 26 | 27 | const state = shallowRef(undefined); 28 | const error = shallowRef(undefined); 29 | 30 | const errorEvent = createEventHook(); 31 | if (onError) errorEvent.on(onError); 32 | 33 | async function execute(delay?: number, payload?: TPayload): Promise { 34 | isReady.value = false; 35 | isLoading.value = true; 36 | startTime.value = Date.now(); 37 | endTime.value = undefined; 38 | error.value = undefined; 39 | 40 | try { 41 | if (delay > 0) await timeout(delay); 42 | 43 | const data = await promise(payload); 44 | state.value = data; 45 | isReady.value = true; 46 | counter.value++; 47 | } catch (e) { 48 | error.value = e as TError; 49 | errorEvent.trigger(error.value); 50 | } finally { 51 | isLoading.value = false; 52 | endTime.value = Date.now(); 53 | } 54 | 55 | return state.value; 56 | } 57 | 58 | return reactive({ 59 | isReady, 60 | isLoading, 61 | counter, 62 | startTime, 63 | endTime, 64 | state, 65 | error, 66 | onError: errorEvent.on, 67 | execute, 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /apps/web/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './hooks'; 3 | -------------------------------------------------------------------------------- /apps/web/src/config.ts: -------------------------------------------------------------------------------- 1 | interface Config { 2 | defaultLocale: string; 3 | useCookies: boolean; 4 | api: { 5 | host: string; 6 | port: number; 7 | }; 8 | } 9 | 10 | export const config: Config = { 11 | defaultLocale: import.meta.env.VITE_WEB_DEFAULT_LOCALE ?? 'en', 12 | useCookies: !import.meta.env.VITE_IS_ELECTRON_APP, 13 | 14 | api: { 15 | host: import.meta.env.VITE_WEB_API_URL ?? 'http://localhost', 16 | port: import.meta.env.VITE_WEB_API_PORT ?? 3000, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /apps/web/src/locales/en.yml: -------------------------------------------------------------------------------- 1 | app_name: 'nx-nestjs-vue' 2 | app_connection_lost: 'Connection to the server has been lost. Connecting...' 3 | 4 | yes: 'Yes' 5 | no: 'No' 6 | save: 'Save' 7 | add: 'Add' 8 | close: 'Close' 9 | cancel: 'Cancel' 10 | search: 'Search' 11 | edit: 'Edit' 12 | saved_successfully: 'The changes made have been saved.' 13 | 14 | routes_login: 'Sign in' 15 | routes_dashboard: 'Dashboard' 16 | routes_users: 'Users' 17 | routes_user: 'User' 18 | routes_settings: 'Settings' 19 | routes_logout: 'Sign out' 20 | 21 | validation_field_required: 'This field is required!' 22 | validation_string_min: 'This field must contain at least {min} characters!' 23 | validation_string_max: 'This field must contain a maximum of {max} characters!' 24 | validation_string_email: 'This is not a valid e-mail address!' 25 | validation_string_password_repeat: 'Passwords must match!' 26 | validation_string_matches: 'Incorrect format!' 27 | 28 | roles_user: 'User' 29 | roles_admin: 'Administartor' 30 | 31 | menu_main_title: 'Main menu' 32 | menu_main_caption: 'Basic functions of the application' 33 | menu_admin_title: 'Administration' 34 | menu_admin_caption: 'Functions for administrators' 35 | menu_account_title: 'Account' 36 | menu_account_caption: 'User account options' 37 | 38 | account_session_exp: 'Your session has expired!' 39 | account_logout_success: 'Logged out successfully!' 40 | account_logout_failed: 'There was a problem logging out!' 41 | account_logout_progress: 'Signing out ...' 42 | account_menu_settings: 'Settings' 43 | account_menu_logout: 'Sign out' 44 | 45 | login_welcome_title: 'Control panel' 46 | login_welcome_description: 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quaerat quas officiis nisi consequatur optio repudiandae corporis. Quos, iste labore? Laboriosam odio enim deserunt sunt illo commodi nostrum in praesentium suscipit autem, ea deleniti consectetur aspernatur fuga minima obcaecati corrupti iusto aliquid voluptatem! Repellat itaque voluptas facilis dolore expedita? Consectetur, facere!' 47 | login_form_email: 'E-mail adress' 48 | login_form_password: 'Password' 49 | login_form_remember: 'Remember me' 50 | login_form_signin: 'Sign in' 51 | login_form_errors_no_authorization: 'Wrong e-mail or password!' 52 | 53 | dashboard_welcome: 'Hello again, {name}!' 54 | 55 | users_title: 'User settings' 56 | users_email: 'E-mail' 57 | users_form_first_name: 'Name' 58 | users_form_last_name: 'Surname' 59 | users_form_position: 'Position' 60 | users_form_role: 'Role' 61 | users_form_isActive: 'Active' 62 | users_form_password: 'Password' 63 | users_form_errors_default: 'Error occured!' 64 | users_form_errors_samelogin: 'The given username already exists in the database!' 65 | users_create_title: 'New user' 66 | users_create_email: 'E-mail' 67 | users_create_first_name: 'Name' 68 | users_create_last_name: 'Surname' 69 | users_create_position: 'Position' 70 | users_create_role: 'Role' 71 | users_create_password: 'Password' 72 | users_create_errors_default: 'Error occured!' 73 | users_create_errors_user_exists: 'A user with such a login already exists!' 74 | users_list_first_and_last_name: 'Name and surname' 75 | users_list_email: 'E-mail' 76 | users_list_role: 'Role' 77 | users_list_created_at: 'Created at' 78 | users_select_search: 'Search for a user' 79 | 80 | settings_general_title: 'General' 81 | settings_general_role: 'Role' 82 | settings_general_email: 'E-mail' 83 | settings_general_form_first_name: 'Name' 84 | settings_general_form_last_name: 'Surname' 85 | settings_general_form_position: 'Position' 86 | settings_general_form_errors_default: 'Error occured!' 87 | settings_password_title: 'Account' 88 | settings_password_form_password: 'Current password' 89 | settings_password_form_new_password: 'New password' 90 | settings_password_form_repeat_password: 'Repeat new password' 91 | settings_password_form_errors_default: 'Error occured!' 92 | settings_password_form_errors_wrong_password: 'Wrong password!' 93 | -------------------------------------------------------------------------------- /apps/web/src/locales/pl.yml: -------------------------------------------------------------------------------- 1 | app_name: 'nx-nestjs-vue' 2 | app_connection_lost: 'Połączenie z serwerem zostało zerwane. Łączenie...' 3 | 4 | yes: 'Tak' 5 | no: 'Nie' 6 | save: 'Zapisz' 7 | add: 'Dodaj' 8 | close: 'Zamknij' 9 | cancel: 'Anuluj' 10 | search: 'Wyszukaj' 11 | edit: 'Edycja' 12 | saved_successfully: 'Wprowadzone zmiany zostały zapisane.' 13 | 14 | routes_login: 'Logowanie' 15 | routes_dashboard: 'Dashboard' 16 | routes_users: 'Użytkownicy' 17 | routes_user: 'Użytkownik' 18 | routes_settings: 'Ustawienia' 19 | routes_logout: 'Wyloguj' 20 | 21 | validation_field_required: 'To pole jest wymagane!' 22 | validation_string_min: 'To pole musi zawierać minumum {min} znaków!' 23 | validation_string_max: 'To pole musi zawierać maksymalnie {max} znaków!' 24 | validation_string_email: 'To nie jest poprawny adres e-mail!' 25 | validation_string_password_repeat: 'Hasła muszą być identyczne!' 26 | validation_string_matches: 'Niepoprawny format!' 27 | 28 | roles_user: 'Użytkownik' 29 | roles_admin: 'Administartor' 30 | 31 | menu_main_title: 'Menu główne' 32 | menu_main_caption: 'Podstawowe funkcje aplikacji' 33 | menu_admin_title: 'Administracja' 34 | menu_admin_caption: 'Funkcje dla administratorów' 35 | menu_account_title: 'Konto' 36 | menu_account_caption: 'Opcje konta użytkownika' 37 | account_session_exp: 'Sesja wygasła!' 38 | account_logout_success: 'Wylogowano poprawnie!' 39 | account_logout_failed: 'Wystąpił problem podczas wylogowywania!' 40 | account_logout_progress: 'Wylogowywanie...' 41 | account_menu_settings: 'Ustawienia' 42 | account_menu_logout: 'Wyloguj' 43 | 44 | login_welcome_title: 'Panel kontrolny' 45 | login_welcome_description: 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quaerat quas officiis nisi consequatur optio repudiandae corporis. Quos, iste labore? Laboriosam odio enim deserunt sunt illo commodi nostrum in praesentium suscipit autem, ea deleniti consectetur aspernatur fuga minima obcaecati corrupti iusto aliquid voluptatem! Repellat itaque voluptas facilis dolore expedita? Consectetur, facere!' 46 | login_form_login: 'Nazwa użytkownika' 47 | login_form_password: 'Hasło' 48 | login_form_remember: 'Zapamiętaj mnie' 49 | login_form_signin: 'Zaloguj się' 50 | login_form_errors_no_authorization: 'Błędny e-mail lub hasło!' 51 | 52 | dashboard_welcome: 'Witaj ponownie, {name}!' 53 | 54 | users_title: 'Ustawienia użytkownika' 55 | users_email: 'E-mail' 56 | users_form_first_name: 'Imię' 57 | users_form_last_name: 'Nazwisko' 58 | users_form_position: 'Stanowisko' 59 | users_form_role: 'Rola' 60 | users_form_isActive: 'Aktywny' 61 | users_form_password: 'Hasło' 62 | users_form_errors_default: 'Wystąpił błąd!' 63 | users_form_errors_samelogin: 'Podana nazwa użytkownika już istnieje w bazie!' 64 | users_create_title: 'Nowy użytkownik' 65 | users_create_email: 'E-mail' 66 | users_create_first_name: 'Imię' 67 | users_create_last_name: 'Nazwisko' 68 | users_create_position: 'Stanowisko' 69 | users_create_role: 'Rola' 70 | users_create_password: 'Hasło' 71 | users_create_errors_default: 'Wystąpił błąd!' 72 | users_create_errors_user_exists: 'Użytkownik o takim loginie już istnieje!' 73 | users_list_first_and_last_name: 'Imię i nazwisko' 74 | users_list_email: 'E-mail' 75 | users_list_role: 'Rola' 76 | users_list_created_at: 'Utworzony' 77 | users_select_search: 'Wyszukaj użytkownika' 78 | 79 | settings_general_title: 'Ogólne' 80 | settings_general_role: 'Rola' 81 | settings_general_email: 'E-mail' 82 | settings_general_form_first_name: 'Imię' 83 | settings_general_form_last_name: 'Nazwisko' 84 | settings_general_form_position: 'Stanowisko' 85 | settings_general_form_errors_default: 'Wystąpił błąd!' 86 | settings_password_title: 'Konto' 87 | settings_password_form_password: 'Aktualne hasło' 88 | settings_password_form_new_password: 'Nowe hasło' 89 | settings_password_form_repeat_password: 'Powtórz nowe hasło' 90 | settings_password_form_errors_default: 'Wystąpił błąd!' 91 | settings_password_form_errors_wrong_password: 'Błędne hasło!' -------------------------------------------------------------------------------- /apps/web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { pinia } from './stores'; 3 | import router from './router'; 4 | import { Quasar, Notify, Dialog } from 'quasar'; 5 | import { createI18n } from 'vue-i18n'; 6 | import messages from '@intlify/unplugin-vue-i18n/messages'; 7 | import { configure as veeConfigure } from 'vee-validate'; 8 | 9 | import '@quasar/extras/material-icons/material-icons.css'; 10 | import '@quasar/extras/mdi-v6/mdi-v6.css'; 11 | import 'quasar/src/css/index.sass'; 12 | import './assets/main.scss'; 13 | 14 | import App from './app/App.vue'; 15 | import { config } from './config'; 16 | 17 | veeConfigure({ 18 | validateOnInput: true, 19 | }); 20 | 21 | const i18n = createI18n({ 22 | locale: config.defaultLocale, 23 | legacy: false, 24 | globalInjection: true, 25 | messages, 26 | }); 27 | 28 | const app = createApp(App); 29 | 30 | app.use(i18n); 31 | app.use(pinia); 32 | app.use(router); 33 | 34 | app.use(Quasar, { 35 | plugins: { 36 | Notify, 37 | Dialog, 38 | }, 39 | }); 40 | 41 | app.mount('#app').$nextTick(() => { 42 | postMessage({ payload: 'removeLoading' }, '*'); 43 | }); 44 | -------------------------------------------------------------------------------- /apps/web/src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/apps/web/src/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@workspace/shared'; 2 | import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'; 3 | import { AuthMeta, useAuthGuard, useLoadingIndicator } from './middlewares'; 4 | import { routes } from './routes'; 5 | 6 | declare module 'vue-router' { 7 | export interface RouteMeta { 8 | auth: AuthMeta; 9 | roles?: Role[]; 10 | } 11 | } 12 | 13 | const router = createRouter({ 14 | history: import.meta.env.VITE_IS_ELECTRON_APP 15 | ? createWebHashHistory(import.meta.env.BASE_URL) 16 | : createWebHistory(import.meta.env.BASE_URL), 17 | routes, 18 | }); 19 | 20 | useLoadingIndicator(router); 21 | useAuthGuard(router); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /apps/web/src/router/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-auth-guard'; 2 | export * from './use-loading-indicator'; 3 | -------------------------------------------------------------------------------- /apps/web/src/router/middlewares/use-auth-guard.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'vue-router'; 2 | import { useAccountStore } from '@/stores/account'; 3 | 4 | export enum AuthMeta { 5 | None, 6 | Required, 7 | Optional, 8 | } 9 | 10 | export function useAuthGuard(router: Router) { 11 | router.beforeEach(async (to) => { 12 | const accountStore = useAccountStore(); 13 | accountStore.refreshAuthState(); 14 | 15 | if (!accountStore.state.loaded && accountStore.state.authenticated) 16 | await accountStore.fetch(500); 17 | 18 | if (to.meta.auth === AuthMeta.Required && !accountStore.state.authenticated) { 19 | return { name: 'login' }; 20 | } 21 | 22 | if (to.meta.auth === AuthMeta.None && accountStore.state.authenticated) { 23 | return { name: 'dashboard' }; 24 | } 25 | 26 | if (to.meta.roles && !to.meta.roles.includes(accountStore.state.role)) { 27 | return { name: 'dashboard' }; 28 | } 29 | 30 | if (to.name === 'user' && to.params.id === accountStore.state.id) { 31 | return { name: 'dashboard' }; 32 | } 33 | 34 | return true; 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /apps/web/src/router/middlewares/use-loading-indicator.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'vue-router'; 2 | import { useAppStore } from '@/stores/app'; 3 | 4 | export function useLoadingIndicator(router: Router) { 5 | router.beforeEach(() => { 6 | const appStore = useAppStore(); 7 | appStore.state.isRouteLoading = true; 8 | }); 9 | 10 | router.afterEach(() => { 11 | const appStore = useAppStore(); 12 | appStore.state.isRouteLoading = false; 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | import { AuthMeta } from './middlewares'; 3 | import { Role } from '@workspace/shared'; 4 | 5 | import DefaultLayout from '@/app/layouts/default.vue'; 6 | import AppLayout from '@/app/layouts/app.vue'; 7 | import LoginPage from '@/app/pages/login.vue'; 8 | 9 | export const routes: RouteRecordRaw[] = [ 10 | { 11 | path: '/', 12 | component: DefaultLayout, 13 | children: [ 14 | { 15 | path: '/', 16 | name: 'login', 17 | component: LoginPage, 18 | meta: { 19 | auth: AuthMeta.None, 20 | }, 21 | }, 22 | ], 23 | }, 24 | { 25 | path: '/app', 26 | component: AppLayout, 27 | children: [ 28 | { 29 | path: '/dashboard', 30 | name: 'dashboard', 31 | component: () => import('@/app/pages/dashboard.vue'), 32 | meta: { 33 | auth: AuthMeta.Required, 34 | }, 35 | }, 36 | { 37 | path: '/users', 38 | component: () => import('@/app/pages/users/index.vue'), 39 | children: [ 40 | { 41 | path: 'list', 42 | name: 'users', 43 | component: () => import('@/app/pages/users/list.vue'), 44 | meta: { 45 | auth: AuthMeta.Required, 46 | roles: [Role.Admin], 47 | }, 48 | }, 49 | { 50 | path: ':id', 51 | name: 'user', 52 | component: () => import('@/app/pages/users/user.vue'), 53 | meta: { 54 | auth: AuthMeta.Required, 55 | roles: [Role.Admin], 56 | }, 57 | }, 58 | ], 59 | }, 60 | { 61 | path: '/settings', 62 | name: 'settings', 63 | component: () => import('@/app/pages/settings.vue'), 64 | meta: { 65 | auth: AuthMeta.Required, 66 | }, 67 | }, 68 | ], 69 | }, 70 | ]; 71 | -------------------------------------------------------------------------------- /apps/web/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | 10 | declare module '@intlify/unplugin-vue-i18n/messages' { 11 | import { LocaleMessage } from '@intlify/core-base'; 12 | import { VueMessageType } from 'vue-i18n'; 13 | const messages: { [x: string]: LocaleMessage }; 14 | export default messages; 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/stores/app.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | import { useTitle } from '@vueuse/core'; 3 | import { defineStore } from 'pinia'; 4 | import { watch } from 'vue'; 5 | import { useI18n } from 'vue-i18n'; 6 | import { useRoute } from 'vue-router'; 7 | 8 | interface State { 9 | networkError: boolean; 10 | isRouteLoading: boolean; 11 | routeTitle: string; 12 | } 13 | 14 | export const useAppStore = defineStore('app', () => { 15 | const route = useRoute(); 16 | const { t } = useI18n(); 17 | const title = useTitle(); 18 | 19 | const state: State = reactive({ 20 | networkError: false, 21 | isRouteLoading: false, 22 | routeTitle: t(`app_name`), 23 | }); 24 | 25 | watch( 26 | () => route.name, 27 | () => { 28 | title.value = t(`routes_${route.name.toString()}`) + ' - ' + t(`app_name`); 29 | state.routeTitle = t(`routes_${route.name.toString()}`); 30 | } 31 | ); 32 | 33 | return { state }; 34 | }); 35 | -------------------------------------------------------------------------------- /apps/web/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | 3 | const pinia = createPinia(); 4 | 5 | export { pinia }; 6 | -------------------------------------------------------------------------------- /apps/web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "types": ["node"], 7 | "composite": true 8 | }, 9 | 10 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "../../tsconfig.base.json"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["esnext", "dom"], 5 | "target": "esnext", 6 | "jsx": "preserve", 7 | "skipLibCheck": true, 8 | "useDefineForClassFields": true, 9 | 10 | "types": [ 11 | "vite/client", 12 | "vitest", 13 | "@intlify/unplugin-vue-i18n/messages" 14 | ], 15 | 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./src/*"], 19 | "@workspace/shared": ["../../libs/shared/src/index.ts"] 20 | } 21 | }, 22 | 23 | "files": [], 24 | 25 | "include": [ 26 | "./src/components.d.ts", 27 | "**/*.js", 28 | "**/*.jsx", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | "**/*.vue", 32 | "**/*.json" 33 | ], 34 | 35 | "exclude": ["**/*.spec.js", "**/*.spec.jsx", "**/*.spec.ts", "**/*.spec.tsx"], 36 | 37 | "references": [ 38 | { 39 | "path": "./tsconfig.app.json" 40 | }, 41 | { 42 | "path": "./tsconfig.spec.json" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "types": ["vitest/globals", "node"], 7 | "composite": true 8 | }, 9 | 10 | "include": [ 11 | "vite.config.ts", 12 | "**/*.test.ts", 13 | "**/*.spec.ts", 14 | "**/*.test.tsx", 15 | "**/*.spec.tsx", 16 | "**/*.test.js", 17 | "**/*.spec.js", 18 | "**/*.test.jsx", 19 | "**/*.spec.jsx", 20 | "**/*.d.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import path from 'path'; 4 | import { defineConfig } from 'vite'; 5 | import Vue from '@vitejs/plugin-vue'; 6 | import Components from 'unplugin-vue-components/vite'; 7 | import AutoImport from 'unplugin-auto-import/vite'; 8 | import { quasar, transformAssetUrls } from '@quasar/vite-plugin'; 9 | import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; 10 | import checker from 'vite-plugin-checker'; 11 | import electron from 'vite-plugin-electron'; 12 | import viteTsConfigPaths from 'vite-tsconfig-paths'; 13 | 14 | const resolve = (p: string) => path.resolve(__dirname, p); 15 | 16 | export default defineConfig({ 17 | server: { 18 | host: true, 19 | port: 8080, 20 | 21 | fs: { 22 | allow: ['../..'], 23 | }, 24 | }, 25 | 26 | base: process.env.VITE_IS_ELECTRON_APP ? './' : '/', 27 | cacheDir: '../../node_modules/.vite-web', 28 | clearScreen: true, 29 | assetsInclude: /\.(pdf|jpg|png|svg)$/, 30 | 31 | resolve: { 32 | alias: { 33 | '@/': `${resolve('./src')}/`, 34 | }, 35 | }, 36 | 37 | publicDir: resolve('./src/public'), 38 | 39 | plugins: [ 40 | viteTsConfigPaths({ 41 | projects: [resolve('../../tsconfig.base.json')], 42 | }), 43 | 44 | Vue({ 45 | template: { 46 | transformAssetUrls, 47 | }, 48 | }), 49 | 50 | process.env.VITE_DISABLE_VUE_TSC 51 | ? null 52 | : checker({ 53 | vueTsc: true, 54 | }), 55 | 56 | VueI18nPlugin({ 57 | defaultSFCLang: 'yml', 58 | include: resolve('./src/locales/**'), 59 | }), 60 | 61 | quasar({ 62 | sassVariables: resolve('./src/assets/quasar.scss'), 63 | }), 64 | 65 | AutoImport({ 66 | dts: resolve('./src/auto-imports.d.ts'), 67 | imports: ['vue', 'vue-router'], 68 | }), 69 | 70 | Components({ 71 | dts: resolve('./src/components.d.ts'), 72 | dirs: ['src/app/components'], 73 | }), 74 | 75 | process.env.VITE_IS_ELECTRON_APP 76 | ? electron([ 77 | { 78 | entry: path.join(__dirname, 'electron/main/index.ts'), 79 | vite: { 80 | build: { 81 | sourcemap: true, 82 | outDir: 'dist/apps/web/electron/main', 83 | }, 84 | }, 85 | }, 86 | 87 | { 88 | entry: path.join(__dirname, 'electron/preload/index.ts'), 89 | vite: { 90 | build: { 91 | sourcemap: 'inline', 92 | outDir: 'dist/apps/web/electron/preload', 93 | }, 94 | }, 95 | onstart(options) { 96 | options.reload(); 97 | }, 98 | }, 99 | ]) 100 | : null, 101 | ], 102 | 103 | test: { 104 | globals: true, 105 | cache: { 106 | dir: '../../node_modules/.vitest', 107 | }, 108 | environment: 'jsdom', 109 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 110 | }, 111 | }); 112 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | app: 4 | container_name: app 5 | restart: always 6 | build: . 7 | env_file: .env 8 | ports: 9 | - ${NEST_API_HTTP_PORT}:${NEST_API_HTTP_PORT} 10 | depends_on: 11 | - db 12 | environment: 13 | - DATABASE_HOST=db 14 | 15 | db: 16 | container_name: postgres 17 | image: postgres 18 | ports: 19 | - '5433:${DATABASE_PORT}' 20 | volumes: 21 | - data:/data/db 22 | environment: 23 | - POSTGRES_DB=${DATABASE_NAME} 24 | - POSTGRES_USER=${DATABASE_USER} 25 | - POSTGRES_PASSWORD=${DATABASE_PASSWORD} 26 | 27 | volumes: 28 | data: {} -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # @see https://www.electron.build/configuration/configuration 3 | ## 4 | 5 | appId: "nx-nestjs-vue" 6 | asar: true 7 | 8 | directories: 9 | output: "dist/release/${version}" 10 | 11 | files: 12 | - "dist" 13 | 14 | mac: 15 | artifactName: "${productName}_${version}.${ext}" 16 | target: 17 | - "dmg" 18 | 19 | win: 20 | artifactName: "${productName}_${version}.${ext}" 21 | target: 22 | - target: "nsis" 23 | arch: 24 | - "x64" 25 | 26 | nsis: 27 | oneClick: false 28 | perMachine: false 29 | allowToChangeInstallationDirectory: true 30 | deleteAppDataOnUninstall: false 31 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nrwl/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/libs/.gitkeep -------------------------------------------------------------------------------- /libs/shared/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/shared/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"] 3 | } -------------------------------------------------------------------------------- /libs/shared/README.md: -------------------------------------------------------------------------------- 1 | # shared 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test shared` to execute the unit tests via [Jest](https://jestjs.io). 8 | 9 | ## Running lint 10 | 11 | Run `nx lint shared` to execute the lint via [ESLint](https://eslint.org/). 12 | -------------------------------------------------------------------------------- /libs/shared/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'shared', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 14 | coverageDirectory: '../../coverage/libs/shared', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/shared/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/project-schema.json", 3 | "sourceRoot": "libs/shared/src", 4 | "projectType": "library", 5 | "targets": { 6 | "lint": { 7 | "executor": "@nrwl/linter:eslint", 8 | "outputs": ["{options.outputFile}"], 9 | "options": { 10 | "lintFilePatterns": ["libs/shared/**/*.ts"], 11 | "fix": true 12 | } 13 | }, 14 | "test": { 15 | "executor": "@nrwl/jest:jest", 16 | "outputs": ["coverage/libs/shared"], 17 | "options": { 18 | "jestConfig": "libs/shared/jest.config.ts", 19 | "passWithNoTests": true 20 | } 21 | } 22 | }, 23 | "tags": [] 24 | } 25 | -------------------------------------------------------------------------------- /libs/shared/src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './inputs/auth-login.input'; 2 | export * from './responses/auth-login.response'; 3 | -------------------------------------------------------------------------------- /libs/shared/src/auth/inputs/auth-login.input.ts: -------------------------------------------------------------------------------- 1 | import { bool, object, ObjectSchema, string } from 'yup'; 2 | import { setLocale } from 'yup'; 3 | import { UseSchema, yupLocale } from '@workspace/shared'; 4 | 5 | setLocale(yupLocale); 6 | 7 | export const authLoginSchema: ObjectSchema = object().shape({ 8 | email: string().required().email().lowercase().trim(), 9 | password: string().required().min(6), 10 | remember: bool(), 11 | cookies: bool(), 12 | }); 13 | 14 | @UseSchema(authLoginSchema) 15 | export class AuthLoginDto { 16 | email: string; 17 | password: string; 18 | remember: boolean; 19 | cookies: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /libs/shared/src/auth/responses/auth-login.response.ts: -------------------------------------------------------------------------------- 1 | import { UserProfileResponse } from '../../users/responses/user-profile.response'; 2 | 3 | export interface AuthLoginResponse { 4 | token?: string; 5 | expirationTime: number; 6 | account: UserProfileResponse; 7 | } 8 | -------------------------------------------------------------------------------- /libs/shared/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './inputs/pagination.input'; 2 | export * from './responses/pagination.response'; 3 | export * from './role.enum'; 4 | -------------------------------------------------------------------------------- /libs/shared/src/common/inputs/pagination.input.ts: -------------------------------------------------------------------------------- 1 | import { object, ObjectSchema, setLocale, number, string, bool } from 'yup'; 2 | import { UseSchema, yupLocale } from '@workspace/shared'; 3 | 4 | setLocale(yupLocale); 5 | 6 | export const paginationDtoSchema: ObjectSchema = object().shape({ 7 | page: number().optional().default(1).min(1), 8 | take: number().optional().default(10).min(1).max(50), 9 | sortBy: string().optional(), 10 | descending: bool().optional(), 11 | filter: string().optional(), 12 | }); 13 | 14 | @UseSchema(paginationDtoSchema) 15 | export class PaginationDto { 16 | page?: number; 17 | take?: number; 18 | sortBy?: string; 19 | descending?: boolean; 20 | filter?: string; 21 | } 22 | -------------------------------------------------------------------------------- /libs/shared/src/common/responses/pagination.response.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationResponse { 2 | page: number; 3 | pages: number; 4 | total: number; 5 | elements: T[]; 6 | } 7 | -------------------------------------------------------------------------------- /libs/shared/src/common/role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | User = 'user', 3 | Admin = 'admin', 4 | } 5 | 6 | export const rolesByImportance: Role[] = [Role.User, Role.Admin]; 7 | -------------------------------------------------------------------------------- /libs/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './common'; 3 | export * from './auth'; 4 | export * from './users'; 5 | -------------------------------------------------------------------------------- /libs/shared/src/users/index.ts: -------------------------------------------------------------------------------- 1 | export * from './inputs/user-create.input'; 2 | export * from './inputs/user-update-self-password.input'; 3 | export * from './inputs/user-update-self.input'; 4 | export * from './inputs/user-update.input'; 5 | export * from './responses/user-profile.response'; 6 | -------------------------------------------------------------------------------- /libs/shared/src/users/inputs/user-create.input.ts: -------------------------------------------------------------------------------- 1 | import { mixed, object, ObjectSchema, setLocale, string } from 'yup'; 2 | import { UseSchema, yupLocale } from '@workspace/shared'; 3 | import { Role } from '../../common'; 4 | 5 | setLocale(yupLocale); 6 | 7 | export const userCreateSchema: ObjectSchema = object().shape({ 8 | email: string().required().email().lowercase().trim(), 9 | password: string().required().min(6), 10 | firstName: string().required().trim(), 11 | lastName: string().required().trim(), 12 | role: mixed().optional().oneOf(Object.values(Role)), 13 | }); 14 | 15 | @UseSchema(userCreateSchema) 16 | export class UserCreateDto { 17 | email: string; 18 | password: string; 19 | firstName: string; 20 | lastName: string; 21 | role?: Role; 22 | } 23 | -------------------------------------------------------------------------------- /libs/shared/src/users/inputs/user-update-self-password.input.ts: -------------------------------------------------------------------------------- 1 | import { object, ref, ObjectSchema, setLocale, string } from 'yup'; 2 | import { UseSchema, yupLocale } from '@workspace/shared'; 3 | 4 | setLocale(yupLocale); 5 | 6 | export const userUpdateSelfPasswordSchema: ObjectSchema = object().shape({ 7 | password: string().required().min(6), 8 | newPassword: string().required().min(6), 9 | repeatPassword: string() 10 | .required() 11 | .oneOf([ref('newPassword'), null], 'string_password_repeat'), 12 | }); 13 | 14 | @UseSchema(userUpdateSelfPasswordSchema) 15 | export class UserUpdateSelfPasswordDto { 16 | password: string; 17 | newPassword: string; 18 | repeatPassword: string; 19 | } 20 | -------------------------------------------------------------------------------- /libs/shared/src/users/inputs/user-update-self.input.ts: -------------------------------------------------------------------------------- 1 | import { object, ObjectSchema, setLocale, string } from 'yup'; 2 | import { UseSchema, yupLocale } from '@workspace/shared'; 3 | 4 | setLocale(yupLocale); 5 | 6 | export const userUpdateSelfSchema: ObjectSchema = object().shape({ 7 | firstName: string().required().trim(), 8 | lastName: string().required().trim(), 9 | position: string().defined().nullable().trim(), 10 | }); 11 | 12 | @UseSchema(userUpdateSelfSchema) 13 | export class UserUpdateSelfDto { 14 | firstName: string; 15 | lastName: string; 16 | position: string | null; 17 | } 18 | -------------------------------------------------------------------------------- /libs/shared/src/users/inputs/user-update.input.ts: -------------------------------------------------------------------------------- 1 | import { boolean, mixed, setLocale, string, ObjectSchema } from 'yup'; 2 | import { UseSchema, yupLocale } from '@workspace/shared'; 3 | import { UserUpdateSelfDto, userUpdateSelfSchema } from './user-update-self.input'; 4 | import { Role } from '../../common'; 5 | 6 | setLocale(yupLocale); 7 | 8 | export const userUpdateSchema: ObjectSchema = userUpdateSelfSchema.shape({ 9 | password: string().when({ 10 | is: (val: string | null) => val.length && val.length > 0, 11 | then: (schema) => schema.min(6), 12 | }), 13 | 14 | isActive: boolean().required(), 15 | role: mixed().oneOf(Object.values(Role)), 16 | }); 17 | 18 | @UseSchema(userUpdateSchema) 19 | export class UserUpdateDto extends UserUpdateSelfDto { 20 | password: string; 21 | isActive: boolean; 22 | role: Role; 23 | } 24 | -------------------------------------------------------------------------------- /libs/shared/src/users/responses/user-profile.response.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '../../common'; 2 | 3 | export interface UserProfileResponse { 4 | id: string; 5 | role: Role; 6 | email: string; 7 | firstName: string; 8 | lastName: string; 9 | position: string; 10 | avatar: string; 11 | isActive: boolean; 12 | createdAt: Date; 13 | } 14 | -------------------------------------------------------------------------------- /libs/shared/src/utils/compare-roles.ts: -------------------------------------------------------------------------------- 1 | import { Role, rolesByImportance } from '@workspace/shared'; 2 | 3 | type ConditionOperator = '<' | '>' | '<=' | '>=' | '='; 4 | 5 | export function rawCompareRoles(role1: Role, role2: Role): number { 6 | if (rolesByImportance.indexOf(role1) > rolesByImportance.indexOf(role2)) return 1; 7 | else if (rolesByImportance.indexOf(role1) === rolesByImportance.indexOf(role2)) return 0; 8 | else return -1; 9 | } 10 | 11 | export function compareRoles(role1: Role, operator: ConditionOperator, role2: Role): boolean { 12 | switch (operator) { 13 | case '>': 14 | return rawCompareRoles(role1, role2) == 1; 15 | case '=': 16 | return rawCompareRoles(role1, role2) == 0; 17 | case '<': 18 | return rawCompareRoles(role1, role2) == -1; 19 | case '>=': 20 | return rawCompareRoles(role1, role2) >= 0; 21 | case '<=': 22 | return rawCompareRoles(role1, role2) <= 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/shared/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './timeout'; 2 | export * from './use-schema'; 3 | export * from './yup-locale'; 4 | export * from './compare-roles'; 5 | -------------------------------------------------------------------------------- /libs/shared/src/utils/timeout.ts: -------------------------------------------------------------------------------- 1 | export function timeout(ms: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /libs/shared/src/utils/use-schema.ts: -------------------------------------------------------------------------------- 1 | import { ObjectSchema } from 'yup'; 2 | 3 | export function UseSchema(schema: ObjectSchema) { 4 | return function (target) { 5 | target.prototype.schema = schema; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /libs/shared/src/utils/yup-locale.ts: -------------------------------------------------------------------------------- 1 | import { LocaleObject } from 'yup'; 2 | 3 | function toObject(key: string, values?: any) { 4 | delete values?.originalValue; 5 | delete values?.value; 6 | 7 | return { 8 | k: key, 9 | v: values, 10 | }; 11 | } 12 | 13 | export const yupLocale: LocaleObject = { 14 | mixed: { 15 | default: (v) => toObject('field_default', v), 16 | required: (v) => toObject('field_required', v), 17 | oneOf: (v) => toObject('field_oneof', v), 18 | notOneOf: (v) => toObject('field_notoneof', v), 19 | notType: (v) => toObject('field_nottype', v), 20 | defined: (v) => toObject('field_defined', v), 21 | }, 22 | 23 | string: { 24 | length: (v) => toObject('string_length', v), 25 | min: (v) => toObject('string_min', v), 26 | max: (v) => toObject('string_max', v), 27 | matches: (v) => toObject('string_matches', v), 28 | email: (v) => toObject('string_email', v), 29 | url: (v) => toObject('string_url', v), 30 | uuid: (v) => toObject('string_uuid', v), 31 | trim: (v) => toObject('string_trim', v), 32 | lowercase: (v) => toObject('string_lowercase', v), 33 | uppercase: (v) => toObject('string_uppercase', v), 34 | }, 35 | 36 | number: { 37 | min: (v) => toObject('number_min', v), 38 | max: (v) => toObject('number_max', v), 39 | lessThan: (v) => toObject('number_lessthan', v), 40 | moreThan: (v) => toObject('number_morethan', v), 41 | positive: (v) => toObject('number_positive', v), 42 | negative: (v) => toObject('number_negative', v), 43 | integer: (v) => toObject('number_integer', v), 44 | }, 45 | 46 | date: { 47 | min: (v) => toObject('date_min', v), 48 | max: (v) => toObject('date_max', v), 49 | }, 50 | 51 | boolean: { 52 | isValue: (v) => toObject('boolean_isvalue', v), 53 | }, 54 | 55 | object: { 56 | noUnknown: (v) => toObject('object_nounknown', v), 57 | }, 58 | 59 | array: { 60 | length: (v) => toObject('array_length', v), 61 | min: (v) => toObject('array_min', v), 62 | max: (v) => toObject('array_max', v), 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /libs/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/shared/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["jest.config.ts", "**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/shared/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/nx-schema.json", 3 | "npmScope": "workspace", 4 | "affected": { 5 | "defaultBase": "master" 6 | }, 7 | "implicitDependencies": { 8 | "package.json": { 9 | "dependencies": "*", 10 | "devDependencies": "*" 11 | }, 12 | ".eslintrc.json": "*" 13 | }, 14 | "tasksRunnerOptions": { 15 | "default": { 16 | "runner": "nx/tasks-runners/default", 17 | "options": { 18 | "cacheableOperations": ["build", "lint", "test", "e2e"] 19 | } 20 | } 21 | }, 22 | "targetDependencies": { 23 | "build": [ 24 | { 25 | "target": "build", 26 | "projects": "dependencies" 27 | } 28 | ] 29 | }, 30 | "defaultProject": "server" 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-nestjs-vue", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "main": "dist/apps/web/electron/main/index.js", 7 | "scripts": { 8 | "apps:dev": "nx run-many --target=serve --projects=web,server", 9 | "web:dev": "nx serve web", 10 | "web:electron:dev": "nx electron web", 11 | "web:electron:build": "nx electron web --configuration=build", 12 | "mobile:dev": "nx serve mobile", 13 | "mobile:android": "nx build mobile && nx run mobile:sync:android && nx run mobile:open:android", 14 | "server:dev": "nx serve server", 15 | "server:seed": "node dist/apps/server/cli.js seed", 16 | "build": "npm run clean && nx run-many --target=lint,test,build --configuration=production --projects=web,server", 17 | "test": "nx run-many --target=test --verbose", 18 | "clean": "rimraf dist", 19 | "lint": "nx run-many --target=lint", 20 | "docker": "npm run build && docker-compose up --force-recreate --build -d && docker image prune -f", 21 | "dep-graph": "node tools/scripts/patch-graph.js && nx graph" 22 | }, 23 | "devDependencies": { 24 | "@capacitor/android": "^5.0.4", 25 | "@capacitor/cli": "^5.0.4", 26 | "@capacitor/ios": "^5.0.4", 27 | "@intlify/unplugin-vue-i18n": "^0.11.0", 28 | "@nestjs/schematics": "^9.2.0", 29 | "@nestjs/testing": "^9.4.2", 30 | "@nx/js": "16.2.2", 31 | "@nx/eslint-plugin": "16.2.2", 32 | "@nx/jest": "16.2.2", 33 | "@nx/linter": "16.2.2", 34 | "@nx/nest": "16.2.2", 35 | "@nx/node": "16.2.2", 36 | "@nx/vite": "^16.2.2", 37 | "@nx/webpack": "16.2.2", 38 | "@nx/workspace": "16.2.2", 39 | "@nxext/capacitor": "^16.2.0", 40 | "@quasar/vite-plugin": "^1.3.3", 41 | "@types/cron": "^2.0.1", 42 | "@types/express": "^4.17.17", 43 | "@types/jest": "29.5.1", 44 | "@types/node": "^20.2.5", 45 | "@types/passport-jwt": "^3.0.8", 46 | "@types/tough-cookie": "^4.0.2", 47 | "@types/uuid": "^9.0.1", 48 | "@typescript-eslint/eslint-plugin": "~5.59.8", 49 | "@typescript-eslint/parser": "~5.59.8", 50 | "@vitejs/plugin-vue": "^4.2.3", 51 | "@vue/eslint-config-prettier": "^7.1.0", 52 | "@vue/eslint-config-typescript": "^11.0.3", 53 | "electron": "^25.0.0", 54 | "electron-builder": "^23.6.0", 55 | "eslint": "~8.41.0", 56 | "eslint-config-prettier": "^8.8.0", 57 | "eslint-plugin-import": "2.27.5", 58 | "eslint-plugin-prettier": "^4.2.1", 59 | "eslint-plugin-vue": "^9.14.1", 60 | "jest": "29.5.0", 61 | "jest-environment-jsdom": "^29.5.0", 62 | "jsdom": "~22.1.0", 63 | "nx": "16.2.2", 64 | "prettier": "^2.8.8", 65 | "rimraf": "^5.0.1", 66 | "sass": "1.32.12", 67 | "ts-jest": "29.1.0", 68 | "typescript": "^5.0.4", 69 | "unplugin-auto-import": "^0.16.4", 70 | "unplugin-vue-components": "^0.25.0", 71 | "vite": "^4.3.9", 72 | "vite-plugin-checker": "^0.6.0", 73 | "vite-plugin-electron-renderer": "^0.14.5", 74 | "vite-plugin-eslint": "1.8.1", 75 | "vite-tsconfig-paths": "^4.2.0", 76 | "vue-eslint-parser": "^9.3.0", 77 | "vue-tsc": "^1.6.5" 78 | }, 79 | "dependencies": { 80 | "@capacitor/core": "^5.0.4", 81 | "@nestjs/common": "^9.4.2", 82 | "@nestjs/config": "^2.3.2", 83 | "@nestjs/core": "^9.4.2", 84 | "@nestjs/passport": "^9.0.3", 85 | "@nestjs/platform-express": "^9.4.2", 86 | "@nestjs/schedule": "^2.2.2", 87 | "@nestjs/serve-static": "^3.0.1", 88 | "@nestjs/typeorm": "^9.0.1", 89 | "@quasar/extras": "1.16.4", 90 | "@vueuse/core": "^10.1.2", 91 | "@vueuse/integrations": "^10.1.2", 92 | "axios": "^1.4.0", 93 | "cookie-parser": "^1.4.6", 94 | "helmet": "^7.0.0", 95 | "jsonwebtoken": "^9.0.0", 96 | "nest-commander": "^3.7.1", 97 | "passport": "^0.6.0", 98 | "passport-jwt": "^4.0.1", 99 | "pg": "^8.11.0", 100 | "pinia": "^2.1.3", 101 | "quasar": "^2.12.0", 102 | "rxjs": "^7.8.1", 103 | "shelljs": "^0.8.5", 104 | "tslib": "^2.5.2", 105 | "typeorm": "^0.3.16", 106 | "universal-cookie": "^4.0.4", 107 | "uuid": "^9.0.0", 108 | "vee-validate": "^4.9.5", 109 | "vite-plugin-electron": "^0.11.2", 110 | "vue": "^3.3.4", 111 | "vue-i18n": "^9.2.2", 112 | "vue-router": "^4.2.2", 113 | "yup": "^1.2.0" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhivinX/nx-nestjs-vue/9efaa801ec6b936aeeff49b7a120ad9b92bae51a/tools/generators/.gitkeep -------------------------------------------------------------------------------- /tools/scripts/patch-graph.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin node 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | /** 6 | * Patch dep-graph builder function to support Vue files. 7 | * @see https://github.com/nrwl/nx/issues/2960 8 | */ 9 | function patchNxDepGraph() { 10 | try { 11 | const filePath = getFilePath(); 12 | const fileContent = fs.readFileSync(filePath).toString('utf-8'); 13 | const replacement = `extension !== \'.ts\' && extension !== \'.vue\'`; 14 | if (fileContent.includes(replacement)) { 15 | return; 16 | } 17 | fs.writeFileSync(filePath, fileContent.replace(`extension !== '.ts'`, replacement)); 18 | console.log('Successfully patched Nx dep-graph for Vue support.'); 19 | } catch (err) { 20 | console.error('Failed to patch Nx dep-graph for Vue support.', err); 21 | } 22 | } 23 | 24 | function getFilePath() { 25 | const possiblePaths = [ 26 | 'node_modules/nx/src/project-graph/build-dependencies/typescript-import-locator.js', 27 | 'node_modules/@nrwl/workspace/src/core/project-graph/build-dependencies/typescript-import-locator.js', 28 | ]; 29 | 30 | for (const p of possiblePaths) { 31 | const fullPath = path.join(process.env.INIT_CWD || '', p); 32 | if (fs.existsSync(fullPath)) { 33 | return fullPath; 34 | } 35 | } 36 | 37 | throw new Error(`Could not find Nx\'s dep-graph builder in node_modules`); 38 | } 39 | 40 | patchNxDepGraph(); 41 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "moduleResolution": "node", 7 | "target": "es2015", 8 | "module": "esnext", 9 | "lib": ["es2017", "dom"], 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "allowJs": true, 13 | "resolveJsonModule": true, 14 | "importHelpers": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": false, 17 | "skipDefaultLibCheck": false, 18 | 19 | "strict": true, 20 | "strictBindCallApply": false, 21 | "noImplicitReturns": true, 22 | "strictNullChecks": false, 23 | "noImplicitAny": false, 24 | 25 | "baseUrl": ".", 26 | "paths": { 27 | "@workspace/shared": ["libs/shared/src/index.ts"] 28 | } 29 | }, 30 | "exclude": ["node_modules", "tmp"] 31 | } 32 | --------------------------------------------------------------------------------