├── .env.prod ├── .github └── workflows │ └── release.yml ├── .gitignore ├── avantage ├── .example.env ├── .gitignore ├── Changelog.md ├── README.md ├── app.vue ├── assets │ └── css │ │ └── tailwind.css ├── components │ ├── User.vue │ ├── content │ │ ├── Alert.vue │ │ ├── BaseCard.vue │ │ ├── ContentHeader.vue │ │ ├── Hero.vue │ │ ├── ProseCode.vue │ │ └── TitleCard.vue │ ├── elements │ │ ├── Logos │ │ │ ├── FlameLogo.vue │ │ │ └── colorMode.vue │ │ ├── TestingPhaseDialog.vue │ │ ├── TheBillboard.vue │ │ └── TheFeatureMapGrid.vue │ └── layout │ │ ├── TheFooter.vue │ │ ├── TheMobileNav.vue │ │ └── TheNavBar.vue ├── composables │ ├── getParam.ts │ ├── useAuth.ts │ ├── useErrorMapper.ts │ ├── useOtp.ts │ ├── useSpark.ts │ └── useVisitCounter.ts ├── config │ └── fullstackjack.service ├── content │ ├── articles │ │ ├── deploy-nuxt3-github-actions.md │ │ └── state-in-nuxt3.md │ ├── index.md │ └── nuxt3-data-fetching.md ├── docker-compose.yml ├── hello.js ├── layouts │ ├── MobileOnly.vue │ └── default.vue ├── middleware │ ├── auth.ts │ └── guest.ts ├── nuxt.config.ts ├── package.json ├── pages │ ├── articles │ │ ├── [...slug].vue │ │ └── overview.vue │ ├── dashboard.vue │ ├── index.vue │ ├── info.vue │ ├── login.vue │ ├── register.vue │ ├── subscribe │ │ ├── index.vue │ │ └── success.vue │ └── verify.vue ├── public │ └── img │ │ ├── avantage-clear.svg │ │ ├── color-mode.svg │ │ ├── color-mode.webp │ │ └── logo-shadow.svg ├── server │ ├── api │ │ ├── auth │ │ │ ├── getByAuthToken.ts │ │ │ ├── login.ts │ │ │ ├── logout.ts │ │ │ ├── register.ts │ │ │ ├── update.ts │ │ │ └── verifyOtp.ts │ │ ├── counter.ts │ │ ├── stripe │ │ │ ├── createPortalSession.ts │ │ │ └── webhooks.post.ts │ │ └── subscribe.post.ts │ ├── app │ │ ├── email │ │ │ ├── emailSender.ts │ │ │ ├── templates │ │ │ │ └── verifyEmailTemplate.ts │ │ │ ├── types │ │ │ │ └── emailTypes.ts │ │ │ └── verifyEmail.ts │ │ ├── errors │ │ │ ├── errorMapper.ts │ │ │ └── responses │ │ │ │ ├── DefaultErrorsResponse.ts │ │ │ │ └── ZodErrorsResponse.ts │ │ ├── formRequests │ │ │ ├── LoginRequest.ts │ │ │ ├── RegisterRequest.ts │ │ │ ├── UpdateUserRequest.ts │ │ │ └── VerifyOtpRequest.ts │ │ └── services │ │ │ ├── otp.ts │ │ │ ├── sessionService.ts │ │ │ ├── stripeService.ts │ │ │ ├── userService.ts │ │ │ └── validator.ts │ ├── database │ │ ├── client.ts │ │ ├── dev.db │ │ ├── migrations │ │ │ ├── 20220928204235_init │ │ │ │ └── migration.sql │ │ │ ├── 20220929201021_add_test_table │ │ │ │ └── migration.sql │ │ │ ├── 20220930194208_add_test_another_migration │ │ │ │ └── migration.sql │ │ │ ├── 20221104204024_add_test_another_migration │ │ │ │ └── migration.sql │ │ │ ├── 20221215190709_dev │ │ │ │ └── migration.sql │ │ │ ├── 20221226101719_dev │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ ├── repositories │ │ │ ├── askJackRespository.ts │ │ │ ├── sessionRepository.ts │ │ │ ├── userRespository.ts │ │ │ └── videoRepository.ts │ │ └── schema.prisma │ └── middleware │ │ └── serverAuth.ts ├── tailwind.config.js ├── tests │ ├── feature │ │ └── register.test.ts │ └── unit │ │ └── register_validation.test.ts ├── tsconfig.json ├── types │ ├── FormValidation.ts │ ├── IAnswer.ts │ ├── IAnswerPost.ts │ ├── ICategory.ts │ ├── ILogin.ts │ ├── IQuestion.ts │ ├── IQuestionPost.ts │ ├── IRegistration.ts │ ├── ISession.ts │ ├── ISubscription.ts │ ├── ITag.ts │ ├── IUser.ts │ ├── InputValidation.ts │ ├── SubPostRes.ts │ ├── TopicData.ts │ └── theme.ts ├── vitest.config.js └── yarn.lock ├── bin ├── npm ├── npx └── prisma ├── config ├── avantage │ └── Dockerfile ├── deployment │ ├── fullstackjack.service │ ├── pre-release-int-config.json │ └── release-prod-config.json ├── nginx │ ├── auth │ │ └── .htpasswd │ ├── fastcgi_params │ ├── includes │ │ └── cors.conf │ ├── nginx.conf │ ├── sites-dev │ │ └── avantage.conf │ └── sites-prod │ │ └── fullstackjack.conf └── ssl │ └── avantage.dev │ ├── create-certificate.sh │ ├── fullchain.pem │ ├── openssl.cnf │ └── privkey.pem ├── docker-compose.dev.yml ├── docker-compose.prod.yml └── docker-compose.yml /.env.prod: -------------------------------------------------------------------------------- 1 | COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml 2 | 3 | COMPOSE_PROJECT_NAME=avantage 4 | 5 | DB_NAME=avantage 6 | DB_USERNAME=jack 7 | DB_PASSWORD=password 8 | DB_ROOT_PASSWORD=password 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy release candidate to int 2 | 3 | on: 4 | push: 5 | tags: 6 | - rc* 7 | 8 | jobs: 9 | test-application: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [ 16.14.2 ] 14 | env: 15 | working-directory: ./nuxt-app 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: 'npm' 23 | cache-dependency-path: ./nuxt-app/package-lock.json 24 | - run: npm ci 25 | working-directory: ${{env.working-directory}} 26 | - run: npm run build 27 | working-directory: ${{env.working-directory}} 28 | - run: npm test 29 | working-directory: ${{env.working-directory}} 30 | 31 | create-deployment-artifacts: 32 | needs: test-application 33 | name: Create Deployment Artefacts 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | node-version: [ 16.14.2 ] 38 | env: 39 | working-directory: ./nuxt-app 40 | outputs: 41 | deployment-matrix: ${{ steps.export-deployment-matrix.outputs.deployment-matrix }} 42 | steps: 43 | - uses: actions/checkout@v3 44 | - name: Use Node.js ${{ matrix.node-version }} 45 | uses: actions/setup-node@v3 46 | with: 47 | node-version: ${{ matrix.node-version }} 48 | cache: 'npm' 49 | cache-dependency-path: ./nuxt-app/package-lock.json 50 | - run: npm ci 51 | working-directory: ${{env.working-directory}} 52 | - run: npm run build 53 | working-directory: ${{env.working-directory}} 54 | - run: npm test 55 | working-directory: ${{env.working-directory}} 56 | - name: Create deployment artifact 57 | env: 58 | GITHUB_SHA: ${{ github.sha }} 59 | run: tar -czf "${GITHUB_SHA}".tar.gz --exclude=node_modules --exclude=tests * .??* 60 | 61 | - name: Store artifact for distribution 62 | uses: actions/upload-artifact@v2 63 | with: 64 | name: app-build 65 | path: ${{ github.sha }}.tar.gz 66 | 67 | - name: Export deployment matrix 68 | id: export-deployment-matrix 69 | run: | 70 | JSON="$(cat ./config/deployment/pre-release-int-config.json)" 71 | JSON="${JSON//'%'/'%25'}" 72 | JSON="${JSON//$'\n'/'%0A'}" 73 | JSON="${JSON//$'\r'/'%0D'}" 74 | echo "::set-output name=deployment-matrix::$JSON" 75 | 76 | prepare-release-on-servers: 77 | needs: create-deployment-artifacts 78 | name: "${{ matrix.server.name }}: Prepare release" 79 | runs-on: ubuntu-latest 80 | strategy: 81 | matrix: 82 | server: ${{ fromJson(needs.create-deployment-artifacts.outputs.deployment-matrix) }} 83 | steps: 84 | - uses: actions/download-artifact@v2 85 | with: 86 | name: app-build 87 | - name: Upload 88 | uses: appleboy/scp-action@master 89 | with: 90 | host: ${{ matrix.server.ip }} 91 | port: ${{ matrix.server.port }} 92 | username: ${{ matrix.server.username }} 93 | key: ${{ secrets.SSH_KEY_INT }} 94 | source: ${{ github.sha }}.tar.gz 95 | target: ${{ matrix.server.path }}/artifacts 96 | 97 | - name: Extract archive and create directories 98 | uses: appleboy/ssh-action@master 99 | env: 100 | GITHUB_SHA: ${{ github.sha }} 101 | with: 102 | host: ${{ matrix.server.ip }} 103 | username: ${{ matrix.server.username }} 104 | key: ${{ secrets.SSH_KEY_INT }} 105 | port: ${{ matrix.server.port }} 106 | envs: GITHUB_SHA 107 | script: | 108 | mkdir -p "${{ matrix.server.path }}/releases/${GITHUB_SHA}" 109 | tar xzf ${{ matrix.server.path }}/artifacts/${GITHUB_SHA}.tar.gz -C "${{ matrix.server.path }}/releases/${GITHUB_SHA}" 110 | rm -rf ${{ matrix.server.path }}/releases/${GITHUB_SHA}/storage 111 | 112 | run-before-hooks: 113 | name: "${{ matrix.server.name }}: Before hook" 114 | runs-on: ubuntu-latest 115 | needs: [ create-deployment-artifacts, prepare-release-on-servers ] 116 | strategy: 117 | matrix: 118 | server: ${{ fromJson(needs.create-deployment-artifacts.outputs.deployment-matrix) }} 119 | steps: 120 | - name: Run before hooks 121 | uses: appleboy/ssh-action@master 122 | env: 123 | GITHUB_SHA: ${{ github.sha }} 124 | RELEASE_PATH: ${{ matrix.server.path }}/releases/${{ github.sha }} 125 | ACTIVE_RELEASE_PATH: ${{ matrix.server.path }}/current 126 | STORAGE_PATH: ${{ matrix.server.path }}/storage 127 | BASE_PATH: ${{ matrix.server.path }} 128 | with: 129 | host: ${{ matrix.server.ip }} 130 | username: ${{ matrix.server.username }} 131 | key: ${{ secrets.SSH_KEY_INT }} 132 | port: ${{ matrix.server.port }} 133 | envs: GITHUB_SHA,RELEASE_PATH,ACTIVE_RELEASE_PATH,STORAGE_PATH,BASE_PATH 134 | script: | 135 | ${{ matrix.server.beforeHooks }} 136 | 137 | 138 | activate-release: 139 | name: "${{ matrix.server.name }}: Activate release" 140 | runs-on: ubuntu-latest 141 | needs: [ create-deployment-artifacts, prepare-release-on-servers, run-before-hooks ] 142 | strategy: 143 | matrix: 144 | server: ${{ fromJson(needs.create-deployment-artifacts.outputs.deployment-matrix) }} 145 | steps: 146 | - name: Activate release 147 | uses: appleboy/ssh-action@master 148 | env: 149 | GITHUB_SHA: ${{ github.sha }} 150 | GITHUB_REF: ${{ github.ref }} 151 | RELEASE_PATH: ${{ matrix.server.path }}/releases/${{ github.sha }} 152 | ACTIVE_RELEASE_PATH: ${{ matrix.server.path }}/current 153 | STORAGE_PATH: ${{ matrix.server.path }}/storage 154 | BASE_PATH: ${{ matrix.server.path }} 155 | PLATFORM_PROD_ENV: ${{ secrets.PLATFORM_PROD_ENV }} 156 | ROOT_PROD_ENV: ${{ secrets.ROOT_PROD_ENV }} 157 | with: 158 | host: ${{ matrix.server.ip }} 159 | username: ${{ matrix.server.username }} 160 | key: ${{ secrets.SSH_KEY_INT }} 161 | port: ${{ matrix.server.port }} 162 | envs: GITHUB_SHA,RELEASE_PATH,ACTIVE_RELEASE_PATH,STORAGE_PATH,BASE_PATH,ENV_PATH,PLATFORM_PROD_ENV,ROOT_PROD_ENV,GITHUB_REF 163 | script: | 164 | cp "${RELEASE_PATH}/apps/nuxt-app/.env.prod" "${RELEASE_PATH}/apps/nuxt-app/.env" 165 | echo "RELEASE_VERSION=${GITHUB_REF}" >> "${RELEASE_PATH}/apps/nuxt-app/.env" 166 | cp "${RELEASE_PATH}/.env.prod" "${RELEASE_PATH}/.env" 167 | ln -s -n -f $RELEASE_PATH $ACTIVE_RELEASE_PATH 168 | systemctl restart fullstackjack 169 | chown -R www-data:www-data ${RELEASE_PATH} 170 | 171 | clean-up: 172 | name: "${{ matrix.server.name }}: Clean up" 173 | runs-on: ubuntu-latest 174 | needs: [ create-deployment-artifacts, prepare-release-on-servers, run-before-hooks, activate-release ] 175 | strategy: 176 | matrix: 177 | server: ${{ fromJson(needs.create-deployment-artifacts.outputs.deployment-matrix) }} 178 | steps: 179 | - name: Run after hooks 180 | uses: appleboy/ssh-action@master 181 | env: 182 | RELEASES_PATH: ${{ matrix.server.path }}/releases 183 | ARTIFACTS_PATH: ${{ matrix.server.path }}/artifacts 184 | with: 185 | host: ${{ matrix.server.ip }} 186 | username: ${{ matrix.server.username }} 187 | key: ${{ secrets.SSH_KEY_INT }} 188 | port: ${{ matrix.server.port }} 189 | envs: RELEASES_PATH 190 | script: | 191 | cd $RELEASES_PATH && ls -t -1 | tail -n +4 | xargs rm -rf 192 | cd $ARTIFACTS_PATH && echo "now cleaning . . . ." && pwd && ls && echo "fies to clean . . . ." && ls -t -1 && ls -t -1 | tail -n +4 | xargs rm -rf 193 | # delete-artifact 194 | - uses: geekyeggo/delete-artifact@v1 195 | with: 196 | name: create-deployment-artifacts 197 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .idea 3 | .vscode/ 4 | apps/nuxt-app/.env 5 | apps/nuxt-app/.env 6 | -------------------------------------------------------------------------------- /avantage/.example.env: -------------------------------------------------------------------------------- 1 | 2 | DATABASE_URL=mysql://root:pw@localhost:3306/unavance 3 | DATABASE_ROOT_PASSWORD=pw 4 | DATABASE_NAME=unavance 5 | DATABASE_USER=unavance 6 | DATABASE_PASSWORD=unavance 7 | 8 | NUXT_MAILER_DRIVER=smtp 9 | NUXT_MAILER_HOST=mailhog 10 | NUXT_MAILER_PORT=1025 11 | NUXT_MAILER_USER=null 12 | NUXT_MAILER_PASS=null 13 | NUXT_MAILER_ENCRYPTION=null 14 | NUXT_MAILER_FROM_ADDRESS="noreply@example.dev" 15 | NUXT_MAILER_FROM_NAME="Fake Company" 16 | NUXT_MAILER_TO_ADDRESS="info@example.dev" 17 | NUXT_MAILER_TO_NAME="Fake Recipient" 18 | 19 | APP_DOMAIN=http://localhost 20 | 21 | EMAIL=info@unavance.dev 22 | 23 | STRIPE_SECRET_KEY=sk_test_unavanceunavanceunavanceunavanceunavanceunavanceunavanceunavanceunavance 24 | 25 | NUXT_PUBLIC_SOMETHING_ELSE='i was set in .example.env' -------------------------------------------------------------------------------- /avantage/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | .env.production 9 | dist 10 | .idea 11 | .vscode 12 | -------------------------------------------------------------------------------- /avantage/Changelog.md: -------------------------------------------------------------------------------- 1 | Release 0.5.0 2 | - added nuxt-mailer 3 | - added otp to registration process 4 | - code quality refactors -------------------------------------------------------------------------------- /avantage/README.md: -------------------------------------------------------------------------------- 1 | ![avantage](https://user-images.githubusercontent.com/45824492/202878741-3fdbb781-1944-461f-81a4-58cdd834df4e.png) 2 | 3 | avantage seeks to accelerate building fullstack node applications with Nuxt 3. 4 | 5 | Features: 6 | - Auth (Can now send OTP via nuxt-mailer) 7 | - Tailwind CSS 8 | - Stripe Checkout Integration 9 | - Prisma Js (database ORM) 10 | - Nuxt-Mailer (node mailer integration) 11 | - what feature do you think belong? open a discussion 12 | 13 | Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more. 14 | 15 | ## Demo App 16 | Check out [avantage.fullStackJack.dev](https://avantage.fullstackjack.dev) to see this code live in action. 17 | 18 | ## Setup 19 | 20 | Make sure to install the dependencies: 21 | 22 | ```bash 23 | # yarn 24 | yarn install 25 | 26 | # npm 27 | npm install 28 | 29 | # pnpm 30 | pnpm install --shamefully-hoist 31 | ``` 32 | 33 | ## Development Server 34 | 35 | Start the development server on http://localhost:3000 36 | 37 | ```bash 38 | npm run dev 39 | ``` 40 | 41 | ## Production 42 | 43 | Build the application for production: 44 | 45 | ```bash 46 | npm run build 47 | ``` 48 | 49 | Locally preview production build: 50 | 51 | ```bash 52 | npm run preview 53 | ``` 54 | 55 | Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information. 56 | 57 | 58 | ## Hi there, Full Stack Jack here 59 | I'm glad you found this awesome repo. 60 | 61 | If you'd like a walk through of how nuxt3 and in particular this set up 62 | works, 63 | 64 | check out the Full Stack Jack Youtube Channel here :point_right: ![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCFDF_U_uoKc6MhIZPZKo5CA?label=FullStackJack&style=social) 65 | FullStackJack Youtube Channel. 66 | 67 | ## Connect with me 68 | 69 | 70 | 71 |
72 | 73 | github 74 | 75 | 76 | gmail 77 | 78 | 79 | youtube 80 | 81 |
82 | -------------------------------------------------------------------------------- /avantage/app.vue: -------------------------------------------------------------------------------- 1 | 22 | 29 | 30 | -------------------------------------------------------------------------------- /avantage/assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | /* /assets/css/tailwind.css */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | -------------------------------------------------------------------------------- /avantage/components/User.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 102 | -------------------------------------------------------------------------------- /avantage/components/content/Alert.vue: -------------------------------------------------------------------------------- 1 | 24 | 39 | 40 | -------------------------------------------------------------------------------- /avantage/components/content/BaseCard.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /avantage/components/content/ContentHeader.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /avantage/components/content/Hero.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /avantage/components/content/ProseCode.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 80 | 81 | 156 | -------------------------------------------------------------------------------- /avantage/components/content/TitleCard.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /avantage/components/elements/Logos/FlameLogo.vue: -------------------------------------------------------------------------------- 1 | 7 | 63 | -------------------------------------------------------------------------------- /avantage/components/elements/TestingPhaseDialog.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 49 | -------------------------------------------------------------------------------- /avantage/components/elements/TheBillboard.vue: -------------------------------------------------------------------------------- 1 | 4 | 18 | -------------------------------------------------------------------------------- /avantage/components/elements/TheFeatureMapGrid.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 166 | -------------------------------------------------------------------------------- /avantage/components/layout/TheFooter.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | -------------------------------------------------------------------------------- /avantage/composables/getParam.ts: -------------------------------------------------------------------------------- 1 | import {H3Error, createError} from "h3"; 2 | 3 | export default (param: string) => { 4 | const route = useRoute() 5 | const value = route.params[param] 6 | 7 | if (value == null) { 8 | const paramNotFound = new H3Error() 9 | paramNotFound.statusCode = 501 10 | paramNotFound.message = param + ' not found on this route. Are you sure you spelled it correctly? ' 11 | + 'params for this route are ' + JSON.stringify(route.params) 12 | throw createError(paramNotFound) 13 | } 14 | 15 | return value 16 | } 17 | 18 | -------------------------------------------------------------------------------- /avantage/composables/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useRouter, useState } from "#app"; 2 | import { ISession } from "~~/types/ISession"; 3 | import { IUser } from "~/types/IUser"; 4 | import useErrorMapper from "./useErrorMapper"; 5 | 6 | export const useAuthCookie = () => useCookie('auth_token') 7 | 8 | export async function useUser(): Promise { 9 | const authCookie = useAuthCookie().value 10 | const user = useState('user') 11 | 12 | if (authCookie && !user.value) { 13 | 14 | const cookieHeaders = useRequestHeaders(['cookie']) 15 | 16 | const { data } = await useFetch(`/api/auth/getByAuthToken`, { 17 | headers: cookieHeaders as HeadersInit, 18 | }) 19 | 20 | user.value = data.value 21 | } 22 | 23 | return user.value 24 | } 25 | 26 | export async function useLoggedIn() { 27 | const user = await useUser() 28 | 29 | if (!user) { 30 | return false 31 | } 32 | 33 | if (user?.id == null) { 34 | return false 35 | } 36 | 37 | return true 38 | } 39 | 40 | export async function userLogout() { 41 | await useFetch('/api/auth/logout') 42 | useState('user').value = null 43 | await useRouter().push('/') 44 | } 45 | 46 | export async function registerWithEmail( 47 | username: string, 48 | name: string, 49 | email: string, 50 | password: string 51 | ): Promise { 52 | 53 | try { 54 | const data = await $fetch('/api/auth/register', { 55 | method: 'POST', 56 | body: { username, name, email, password } 57 | }) 58 | 59 | if (data) { 60 | useState('user').value = data 61 | await useRouter().push('/verify') 62 | } 63 | 64 | return { hasErrors: false, loggedIn: true } 65 | } catch (error: any) { 66 | return useErrorMapper(error.data.data) 67 | } 68 | } 69 | 70 | export async function updateUser( 71 | username: string, 72 | name: string, 73 | email: string, 74 | password: string 75 | ): Promise { 76 | 77 | try { 78 | const data = await $fetch('/api/auth/update', { 79 | method: 'POST', 80 | body: { username, name, email, password } 81 | }) 82 | 83 | if (data) { 84 | useState('user').value = data 85 | await useRouter().push('/account') 86 | } 87 | 88 | return { hasErrors: false, loggedIn: true } 89 | } catch (error: any) { 90 | return useErrorMapper(error.data.data) 91 | } 92 | } 93 | 94 | export async function loginWithEmail(usernameOrEmail: string, password: string): Promise { 95 | try { 96 | const result = await $fetch('/api/auth/login', { method: 'POST', body: { usernameOrEmail: usernameOrEmail, password: password } }) 97 | 98 | if (!result?.id) { 99 | throw Error('something went wrong') 100 | } 101 | useState('user').value = result 102 | await useRouter().push('/dashboard') 103 | 104 | return { hasErrors: false, loggedIn: true } 105 | } catch (error: any) { 106 | return useErrorMapper(error.data.data) 107 | } 108 | } -------------------------------------------------------------------------------- /avantage/composables/useErrorMapper.ts: -------------------------------------------------------------------------------- 1 | export default (errorData: string) => { 2 | const parsedErrors = JSON.parse(errorData) 3 | const errorMap = new Map(Object.entries(parsedErrors)) 4 | return { hasErrors: true, errors: errorMap } 5 | } -------------------------------------------------------------------------------- /avantage/composables/useOtp.ts: -------------------------------------------------------------------------------- 1 | export async function verifyOtp(otp: number|undefined) { 2 | try { 3 | const isVerified = await $fetch('/api/auth/verifyOtp', { method: 'POST', body: { otp } }) 4 | 5 | if (!isVerified) { 6 | throw Error('invalid otp, try resending') 7 | } 8 | 9 | return { hasErrors: false, isVerified: isVerified } 10 | } catch (error: any) { 11 | return useErrorMapper(error.data.data) 12 | } 13 | } -------------------------------------------------------------------------------- /avantage/composables/useSpark.ts: -------------------------------------------------------------------------------- 1 | export const useSpark = () => { 2 | const spark = ref(false) 3 | const spark2 = ref(false) 4 | setInterval(() => { 5 | spark.value = !spark.value 6 | },200) 7 | 8 | 9 | setInterval(() => { 10 | spark2.value = !spark2.value 11 | },100) 12 | 13 | return {spark, spark2} 14 | } -------------------------------------------------------------------------------- /avantage/composables/useVisitCounter.ts: -------------------------------------------------------------------------------- 1 | export const useVisitCounter = () => { 2 | return ref() 3 | } 4 | -------------------------------------------------------------------------------- /avantage/config/fullstackjack.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=fullstackjack service 3 | Documentation=https://fullstackjack.dev 4 | After=network.target 5 | 6 | 7 | [Service] 8 | Restart=always 9 | RestartSec=10 10 | TimeoutSec=300 11 | WorkingDirectory=/var/www/html/live 12 | ExecStart=/usr/bin/bash -c 'node .output/server/index.mjs' 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | 17 | # /etc/systemd/system/fullstackjack.service 18 | -------------------------------------------------------------------------------- /avantage/content/articles/state-in-nuxt3.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: State in Nuxt 3 3 | description: Learn about the differnt kinds of state and how to use them 4 | slug: the-latest-features-added-to-javascript-in-ecmascript-2020 5 | author: Rick Rohrig 6 | date: 18 Sep 2022 7 | subject: Nuxt Routing 8 | position: 1 9 | --- 10 | 11 | 12 | # Dynamic vs Persistent 13 | 14 | Hello [World]{.bg-blue-500}! 15 | 16 | It's important to understand the differences between dynamic and persistent state. 17 | 18 | The simplest way to think about it is, persistent state can survive a restart or refresh while dynamic state is wiped clean. 19 | 20 | ## Let's start server side. 21 | 22 | Suppose we have an API Route like **/api/counter** 23 | 24 | Every time we visit or refresh **localhost:3000/api/counter** the count will increment. 25 | You may be tempted to believe that if the count survived a browser refresh, it must must persistent, right? 26 | We must keep in mind, the browser is not the only thing that can restart. The server can as well. 27 | 28 | ::alert{type=info icon=🚨} 29 | Don't let this fool you into thinking this is persistent state. As soon as you restart the server, the count will be back to 0! 30 | :: 31 | 32 | 33 | 34 | ```js [/api/counter.ts] 35 | let count = 0 36 | 37 | export default defineEventHandler( () => { 38 | count++ 39 | 40 | return count 41 | }) 42 | ``` 43 | 44 | If you want persistent state on the server side you can use a full blown database like Postgres or MySql, or even a simple file: Whatever works for you. My recommendation is to use Prisma.js. 45 | You can find out how to use it in my tutorials on my youtube channel: [full stack jack](https://youtube.com/c/fullstackjack) 46 | 47 | ## Client Side State 48 | 49 | ### Dynamic State 50 | Just like the server side, there is also persistent and dynamic state on the client side. 51 | 52 | 53 | In Nuxt 3, we get some pretty awesome state management build right into the framework. It looks like this. 54 | 55 | ```js 56 | const useX = () => useState('x') 57 | 58 | ``` 59 | 60 | If you have some very complex state needs you may want to reach for pinia. But I built the entire site you're browsing now with nothing more 61 | than useState(). Checkout the Repo and see for yourself. There are no other state management solutions at work here. 62 | 63 | The 'x' in that useState('x') is the key. Anywhere you call useState('x') in the app it all points to the same entity in state. By the way, useState() is also SSR-friendly. 64 | 65 | ### Persistent State 66 | 67 | On the client side your persistent state options are rather limited. 68 | 69 | You can store state in cookies or in local storage. 70 | 71 | Nuxt already has a build in way for you to access cookies. I use it in this app like so. 72 | 73 | ```js [/composables/getAuth.ts] 74 | export const useAuthCookie = () => useCookie('auth_token') 75 | ``` 76 | 77 | If you're logged in, open up the browser dev tools and you'll see the **auth_token** cookie is set. 78 | 79 | Alternatively you could use Local Storage. 80 | 81 | I recommend using vueUse for this. It's just so beautiful and buttery smooth to use. 82 | 83 | ```js 84 | export const myCoolLocalStorageValue = () => useLocalStorage('any_key_you_wish') 85 | ``` -------------------------------------------------------------------------------- /avantage/content/index.md: -------------------------------------------------------------------------------- 1 | # Hello Content 2 | -------------------------------------------------------------------------------- /avantage/content/nuxt3-data-fetching.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Data Fetching in Nuxt3 3 | description: data fetching 4 | slug: nuxt3-data-fetching 5 | author: Rick Rohrig 6 | date: 20 Sep 2022 7 | subject: Data Fetching 8 | position: 2 9 | --- 10 | 11 | data fetching 12 | 13 | 14 | ```js 15 | const answer = await useFetch( 16 | () => `/api/ask-jack/answer`, { method: 'post', body: { data } } 17 | ); 18 | ``` 19 | 20 | useFetch() is the standard way to fetch data in Nuxt3. You don't need to add any outside depencencies to such as axios to make fetch requests. 21 | 22 | wip 23 | 24 | -------------------------------------------------------------------------------- /avantage/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | mysql: 5 | image: mysql:8.0.30 6 | ports: 7 | - "127.0.0.1:3306:3306" 8 | environment: 9 | MYSQL_ROOT_PASSWORD: ${DATABASE_ROOT_PASSWORD} 10 | MYSQL_DATABASE: ${DATABASE_NAME} 11 | MYSQL_USER: ${DATABASE_USER} 12 | MYSQL_PASSWORD: ${DATABASE_PASSWORD} 13 | volumes: 14 | - database:/var/lib/mysql 15 | networks: 16 | - default 17 | 18 | mailhog: 19 | container_name: mailhog_avantage 20 | image: mailhog/mailhog 21 | restart: always 22 | ports: 23 | - "1025:1025" 24 | - "8025:8025" 25 | 26 | volumes: 27 | database: 28 | driver: local -------------------------------------------------------------------------------- /avantage/hello.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurassicjs/nuxt3-docker-tut/d748a2bb1dce454ce6ae6b838722a5d1690f1da1/avantage/hello.js -------------------------------------------------------------------------------- /avantage/layouts/MobileOnly.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /avantage/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /avantage/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import {defineNuxtRouteMiddleware, useNuxtApp} from "#app"; 2 | import {useUser} from "~/composables/useAuth"; 3 | 4 | export default defineNuxtRouteMiddleware(async(to) => { 5 | const user = await useUser() 6 | 7 | if (user == null && user == undefined) { 8 | return '/' 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /avantage/middleware/guest.ts: -------------------------------------------------------------------------------- 1 | import {defineNuxtRouteMiddleware, useNuxtApp} from "#app"; 2 | import {useUser} from "~/composables/useAuth"; 3 | 4 | export default defineNuxtRouteMiddleware(async(to) => { 5 | const user = await useUser() 6 | 7 | if (user !== null && user !== undefined) { 8 | return '/' 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /avantage/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | 2 | // https://v3.nuxtjs.org/api/configuration/nuxt.config 3 | export default defineNuxtConfig({ 4 | modules: ['@nuxtjs/tailwindcss', '@nuxtjs/color-mode', '@nuxt/content', 'nuxt-icon', 'nuxt-mailer'], 5 | tailwindcss: { 6 | cssPath: '~/assets/css/tailwind.css', 7 | configPath: 'tailwind.config.js', 8 | exposeConfig: false, 9 | injectPosition: 0, 10 | viewer: true, 11 | }, 12 | colorMode: { 13 | classSuffix: '' 14 | }, 15 | content: { 16 | highlight: { 17 | theme: 'github-dark', 18 | preload: [ 19 | 'vue', 20 | ] 21 | }, 22 | navigation: { 23 | fields: ['author', 'subject', 'position'] 24 | } 25 | }, 26 | runtimeConfig: { 27 | mailerUser: '', 28 | mailerPass: '', 29 | mailerLog: '', 30 | mailerDriver: '', 31 | mailerHost: '', 32 | mailerPort: '', 33 | mailerSmtpTls: '', 34 | mailerFromAddress: '', 35 | mailerToAddress: '', 36 | stripeSecretKey: process.env.STRIPE_SECRET_KEY, 37 | db: process.env.DATABASE_URL, 38 | public: { 39 | appDomain: process.env.APP_DOMAIN, 40 | gitHash: process.env.GITHUB_SHA, 41 | releaseVersion: process.env.RELEASE_VERSION, 42 | } 43 | }, 44 | experimental: { 45 | writeEarlyHints: false, 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /avantage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev --trace-warnings", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "docker:up": "docker-compose up -d", 9 | "docker:down": "docker-compose down", 10 | "test": "yarn docker:up && yarn prisma migrate deploy && vitest", 11 | "ci:test": "yarn prisma migrate deploy && vitest", 12 | "prisma:generate": "dotenv -e .env -- npx prisma generate", 13 | "prisma:migrate": "dotenv -e .env -- npx prisma migrate deploy --name prod" 14 | }, 15 | "devDependencies": { 16 | "@nuxt/content": "^2.3.0", 17 | "@nuxt/postcss8": "^1.1.3", 18 | "@nuxt/test-utils-edge": "3.0.1-rc.0-27863365.da6fa9a", 19 | "@nuxtjs/color-mode": "^3.2.0", 20 | "@nuxtjs/tailwindcss": "^6.2.0", 21 | "@types/bcrypt": "^5.0.0", 22 | "@types/uuid": "^9.0.0", 23 | "autoprefixer": "^10.4.13", 24 | "jsdom": "^20.0.3", 25 | "nuxt": "^3.0.0", 26 | "nuxt-icon": "^0.1.8", 27 | "postcss": "^8.4.20", 28 | "tailwindcss": "^3.2.4", 29 | "vitest": "^0.26.2" 30 | }, 31 | "dependencies": { 32 | "@formkit/auto-animate": "^1.0.0-beta.5", 33 | "@prisma/client": "^4.8.0", 34 | "@sidebase/nuxt-parse": "^0.3.0", 35 | "@tailwindcss/aspect-ratio": "^0.4.2", 36 | "@tailwindcss/typography": "^0.5.8", 37 | "@vueuse/core": "^9.9.0", 38 | "bcrypt": "^5.1.0", 39 | "dotenv-cli": "^6.0.0", 40 | "nuxt-mailer": "^0.10.0", 41 | "prisma": "^4.5.0", 42 | "stripe": "^10.15.0", 43 | "uuid": "^9.0.0", 44 | "zod": "^3.20.2" 45 | }, 46 | "prisma": { 47 | "schema": "server/database/schema.prisma" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /avantage/pages/articles/[...slug].vue: -------------------------------------------------------------------------------- 1 | 43 | -------------------------------------------------------------------------------- /avantage/pages/articles/overview.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /avantage/pages/dashboard.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | -------------------------------------------------------------------------------- /avantage/pages/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /avantage/pages/info.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /avantage/pages/login.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 77 | -------------------------------------------------------------------------------- /avantage/pages/register.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 103 | -------------------------------------------------------------------------------- /avantage/pages/subscribe/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 104 | -------------------------------------------------------------------------------- /avantage/pages/subscribe/success.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /avantage/pages/verify.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 88 | -------------------------------------------------------------------------------- /avantage/public/img/avantage-clear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /avantage/public/img/color-mode.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurassicjs/nuxt3-docker-tut/d748a2bb1dce454ce6ae6b838722a5d1690f1da1/avantage/public/img/color-mode.webp -------------------------------------------------------------------------------- /avantage/public/img/logo-shadow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /avantage/server/api/auth/getByAuthToken.ts: -------------------------------------------------------------------------------- 1 | import { getCookie } from 'h3' 2 | import { getSanitizedUserBySessionToken } from '~~/server/app/services/sessionService' 3 | 4 | export default defineEventHandler(async (event) => { 5 | const authToken = getCookie(event, 'auth_token') 6 | 7 | if(!authToken) { 8 | return null 9 | } 10 | 11 | const user = await getSanitizedUserBySessionToken(authToken) 12 | 13 | return user 14 | }) -------------------------------------------------------------------------------- /avantage/server/api/auth/login.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | import { getUserByEmail } from '~/server/database/repositories/userRespository'; 3 | import { sendError, H3Event } from "h3" 4 | import { ZodError } from "zod" 5 | import loginRequest from '~~/server/app/formRequests/LoginRequest'; 6 | import sendDefaultErrorResponse from '~~/server/app/errors/responses/DefaultErrorsResponse'; 7 | import { getMappedError } from '~~/server/app/errors/errorMapper'; 8 | import { makeSession } from '~~/server/app/services/sessionService'; 9 | import { sanitizeUserForFrontend } from '~~/server/app/services/userService'; 10 | import sendZodErrorResponse from '~~/server/app/errors/responses/ZodErrorsResponse'; 11 | 12 | const standardAuthError = getMappedError('Authentication', 'Invalid Credentials') 13 | 14 | export default eventHandler(async (event: H3Event) => { 15 | 16 | try { 17 | const data = await loginRequest(event) 18 | const user = await getUserByEmail(data.usernameOrEmail) 19 | 20 | if (user === null) { 21 | return sendError(event, createError({ statusCode: 401, data: standardAuthError })) 22 | } 23 | 24 | if (user.password == undefined) { 25 | return sendError(event, createError({ statusCode: 401, data: standardAuthError })) 26 | } 27 | 28 | const isPasswordCorrect = await bcrypt.compare(data.password, user.password) 29 | 30 | if (!isPasswordCorrect) { 31 | sendError(event, createError({ statusCode: 401, data: standardAuthError })) 32 | } 33 | 34 | await makeSession(user, event) 35 | return sanitizeUserForFrontend(user) 36 | } catch (error: any) { 37 | 38 | if (error.data instanceof ZodError) { 39 | return await sendZodErrorResponse(event, error.data) 40 | } 41 | 42 | return await sendDefaultErrorResponse(event, 'Unauthenticated', 401, error) 43 | } 44 | }) 45 | -------------------------------------------------------------------------------- /avantage/server/api/auth/logout.ts: -------------------------------------------------------------------------------- 1 | import { deleteCookie } from "h3"; 2 | 3 | export default eventHandler((event) => { 4 | deleteCookie(event, 'auth_token') 5 | return 'successfully logged out' 6 | }) 7 | -------------------------------------------------------------------------------- /avantage/server/api/auth/register.ts: -------------------------------------------------------------------------------- 1 | import { H3Event, sendError } from 'h3' 2 | import bcrypt from 'bcrypt' 3 | import { IUser } from '~/types/IUser'; 4 | import { createUser } from '~/server/database/repositories/userRespository' 5 | import { ZodError } from "zod" 6 | import sendDefaultErrorResponse from '~~/server/app/errors/responses/DefaultErrorsResponse'; 7 | import registerRequest from '~/server/app/formRequests/RegisterRequest'; 8 | import { validateUser } from '~/server/app/services/userService'; 9 | import { makeSession } from '~~/server/app/services/sessionService'; 10 | import sendZodErrorResponse from '~~/server/app/errors/responses/ZodErrorsResponse'; 11 | import sendVerificationEmail from '~~/server/app/email/verifyEmail'; 12 | 13 | export default eventHandler(async (event: H3Event) => { 14 | try { 15 | const data = await registerRequest(event) 16 | const validation = await validateUser(data) 17 | 18 | if (validation.hasErrors === true && validation.errors) { 19 | const errors = JSON.stringify(Object.fromEntries(validation.errors)) 20 | return sendError(event, createError({ statusCode: 422, data: errors })) 21 | } 22 | 23 | const encryptedPassword: string = await bcrypt.hash(data.password, 10) 24 | 25 | const userData: IUser = { 26 | username: data.username, 27 | name: data.name, 28 | email: data.email, 29 | loginType: 'email', 30 | password: encryptedPassword 31 | } 32 | 33 | const user = await createUser(userData) 34 | 35 | sendVerificationEmail(user.email as string, user.id) 36 | 37 | return await makeSession(user, event) 38 | } catch (error: any) { 39 | 40 | if (error.data instanceof ZodError) { 41 | return await sendZodErrorResponse(event, error.data) 42 | } 43 | 44 | return await sendDefaultErrorResponse(event, 'oops', 500, error) 45 | } 46 | }) -------------------------------------------------------------------------------- /avantage/server/api/auth/update.ts: -------------------------------------------------------------------------------- 1 | import { H3Event, sendError } from 'h3' 2 | import { IUser } from '~/types/IUser'; 3 | import { updateUser } from '~/server/database/repositories/userRespository' 4 | import { ZodError } from "zod" 5 | import sendDefaultErrorResponse from '~~/server/app/errors/responses/DefaultErrorsResponse'; 6 | import { validateUser } from '~/server/app/services/userService'; 7 | import { makeSession } from '~~/server/app/services/sessionService'; 8 | import sendZodErrorResponse from '~~/server/app/errors/responses/ZodErrorsResponse'; 9 | import { getSanitizedUserBySessionToken } from '~/server/app/services/sessionService' 10 | import { getMappedError } from '~~/server/app/errors/errorMapper'; 11 | import updateUserRequest from '~~/server/app/formRequests/UpdateUserRequest'; 12 | 13 | 14 | const standardAuthError = getMappedError('Authentication', 'Invalid Credentials') 15 | 16 | export default eventHandler(async (event: H3Event) => { 17 | try { 18 | 19 | const authToken = getCookie(event, 'auth_token') 20 | 21 | if (!authToken) { 22 | return null 23 | } 24 | 25 | const auth = await getSanitizedUserBySessionToken(authToken) 26 | 27 | if(!auth) { 28 | return sendError(event, createError({ statusCode: 401, data: standardAuthError })) 29 | } 30 | 31 | const data = await updateUserRequest(event) 32 | const validation = await validateUser(data) 33 | 34 | if (validation.hasErrors === true && validation.errors) { 35 | const errors = JSON.stringify(Object.fromEntries(validation.errors)) 36 | return sendError(event, createError({ statusCode: 422, data: errors })) 37 | } 38 | 39 | const userData: IUser = { 40 | username: data.username, 41 | name: data.name, 42 | email: data.email, 43 | loginType: 'email', 44 | } 45 | 46 | const user = await updateUser(userData) 47 | 48 | return await makeSession(user, event) 49 | } catch (error: any) { 50 | 51 | if (error.data instanceof ZodError) { 52 | return await sendZodErrorResponse(event, error.data) 53 | } 54 | 55 | return await sendDefaultErrorResponse(event, 'oops', 500, error) 56 | } 57 | }) -------------------------------------------------------------------------------- /avantage/server/api/auth/verifyOtp.ts: -------------------------------------------------------------------------------- 1 | import { getSanitizedUserBySessionToken } from '~/server/app/services/sessionService' 2 | import { verifyOtp } from '~/server/app/services/otp' 3 | import verifyOtpRequest from '~/server/app/formRequests/VerifyOtpRequest' 4 | import { updateIsEmailVerified } from '~~/server/database/repositories/userRespository' 5 | import { ZodError } from "zod" 6 | import sendZodErrorResponse from '~/server/app/errors/responses/ZodErrorsResponse'; 7 | import sendDefaultErrorResponse from '~/server/app/errors/responses/DefaultErrorsResponse'; 8 | import { getMappedError } from '~~/server/app/errors/errorMapper'; 9 | 10 | const standardOtpError = getMappedError('Verification', 'Invalid Otp. Try requesting new otp') 11 | 12 | export default eventHandler(async (event) => { 13 | 14 | try { 15 | const authToken = getCookie(event, 'auth_token') 16 | 17 | if (authToken === undefined) { 18 | return await sendDefaultErrorResponse(event, 'Unauthenticated', 403, 'Unauthenticated') 19 | } 20 | 21 | const user = await getSanitizedUserBySessionToken(authToken) 22 | 23 | if (user?.id == undefined) { 24 | throw Error('user id is missing') 25 | } 26 | 27 | const request = await verifyOtpRequest(event) 28 | const isVerified = await verifyOtp(user.id, parseInt(request.otp)) 29 | 30 | if (!isVerified) { 31 | return sendError(event, createError({ statusCode: 403, data: standardOtpError })) 32 | } 33 | 34 | await updateIsEmailVerified(user.id, isVerified) 35 | 36 | return isVerified 37 | } catch (error: any) { 38 | if (error.data instanceof ZodError) { 39 | return await sendZodErrorResponse(event, error.data) 40 | } 41 | 42 | return await sendDefaultErrorResponse(event, 'Unauthenticated', 403, error) 43 | } 44 | }) -------------------------------------------------------------------------------- /avantage/server/api/counter.ts: -------------------------------------------------------------------------------- 1 | 2 | let count = 0 3 | 4 | export default defineEventHandler( () => { 5 | 6 | count++ 7 | 8 | return count 9 | 10 | }) -------------------------------------------------------------------------------- /avantage/server/api/stripe/createPortalSession.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | import { readBody } from "h3"; 3 | 4 | const config = useRuntimeConfig() 5 | const stripe = new Stripe(config.stripeSecretKey, null); 6 | 7 | export default eventHandler(async (event) => { 8 | const body = await readBody(event) 9 | const session_id = body.session_id 10 | const returnUrl = config.public.appDomain 11 | const checkoutSession = await stripe.checkout.sessions.retrieve(session_id); 12 | const portalSession = await stripe.billingPortal.sessions.create({ 13 | customer: checkoutSession.customer as string, 14 | return_url: returnUrl, 15 | }); 16 | 17 | await sendRedirect(event, portalSession.url) 18 | }) -------------------------------------------------------------------------------- /avantage/server/api/stripe/webhooks.post.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | import sendDefaultErrorResponse from '~~/server/app/errors/responses/DefaultErrorsResponse'; 3 | import { handleSubscriptionChange } from '~~/server/app/services/stripeService'; 4 | 5 | export default defineEventHandler(async (event) => { 6 | 7 | const stripeEvent = await readBody(event) 8 | 9 | let subscription: Stripe.Subscription | undefined 10 | 11 | const isSubscriptionEvent = stripeEvent.type.startsWith('customer.subscription') 12 | 13 | if (isSubscriptionEvent) { 14 | handleSubscriptionChange(subscription, stripeEvent.created); 15 | return `handled ${stripeEvent.type}.` 16 | } 17 | 18 | return sendDefaultErrorResponse(event, 'oops', 400, `could not handle ${stripeEvent.type}. No functionality set.`) 19 | }) -------------------------------------------------------------------------------- /avantage/server/api/subscribe.post.ts: -------------------------------------------------------------------------------- 1 | import { getUserById } from "~/server/database/repositories/userRespository" 2 | import { getSubscribeUrl } from "~/server/app/services/stripeService" 3 | import { updateStripeCustomerId } from "~/server/database/repositories/userRespository" 4 | 5 | export default defineEventHandler(async (event) => { 6 | const body = await readBody(event) 7 | const lookupKey = body.lookup_key 8 | const userId = body.user_id 9 | 10 | const user = await getUserById(parseInt(userId)) 11 | 12 | const { url, user: customer, shouldUpdateUser } = await getSubscribeUrl(lookupKey, user) 13 | 14 | if(shouldUpdateUser) { 15 | await updateStripeCustomerId(customer) 16 | } 17 | 18 | await sendRedirect(event, url) 19 | }) -------------------------------------------------------------------------------- /avantage/server/app/email/emailSender.ts: -------------------------------------------------------------------------------- 1 | 2 | import { useMailer } from '#mailer' 3 | import { EmailTemplate } from './types/emailTypes' 4 | 5 | type SendMail = {template: EmailTemplate , to: string, fromEmail: string, fromName: string , subject: string} 6 | export async function sendEmail(request: SendMail) { 7 | const mailService = useMailer() 8 | const runtimeConfig = useRuntimeConfig() 9 | 10 | console.log('mailerUser', runtimeConfig.mailerUser) 11 | 12 | await mailService.sendMail({ 13 | requestId: 'test-key', 14 | options: { 15 | fromEmail: request.fromEmail, 16 | fromName: request.fromName, 17 | to: request.to, 18 | subject: request.subject, 19 | text: request.template.text, 20 | html: request.template.html 21 | } 22 | }) 23 | 24 | return 25 | } -------------------------------------------------------------------------------- /avantage/server/app/email/templates/verifyEmailTemplate.ts: -------------------------------------------------------------------------------- 1 | import { EmailTemplate } from "../types/emailTypes" 2 | 3 | const verifyEmailTemplate = function ( 4 | otp: number, 5 | supportEmail: string, 6 | supportName: string, 7 | accountName: string 8 | ): EmailTemplate { 9 | const html = ` 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Reset your Password 20 | 23 | 79 | 80 | 81 | 83 |
A request to create your ${accountName} account was received. 84 | Use this link to confirm your account and log in
85 |
86 | 88 | 89 | 158 | 159 |
91 | 93 | 94 | 101 | 102 | 103 | 155 | 156 | 157 |
160 |
161 | 162 | 163 | ` 164 | const text = ` 165 | Verify Email, A request to create your ${accountName} account was received. 166 | Use this OTP to confirm your account and log in` 167 | return { 168 | html, 169 | text 170 | } 171 | } 172 | 173 | export default verifyEmailTemplate 174 | -------------------------------------------------------------------------------- /avantage/server/app/email/types/emailTypes.ts: -------------------------------------------------------------------------------- 1 | export type EmailTemplate = { html: string, text: string } -------------------------------------------------------------------------------- /avantage/server/app/email/verifyEmail.ts: -------------------------------------------------------------------------------- 1 | import verifyEmailTemplate from './templates/verifyEmailTemplate' 2 | import { sendEmail } from './emailSender' 3 | import { createOtp } from '../services/otp' 4 | 5 | export default async function sendVerificationEmail(email: string, userId: number) { 6 | const otp = await createOtp(userId) 7 | const template = verifyEmailTemplate(otp, 'support@fullstackjack.dev', 'Avantage Support', 'Avantage') 8 | sendEmail({template, to: email, fromEmail: 'jack@fullstackjack.dev', fromName: "Jack", subject:'Avantage email verification'}) 9 | } -------------------------------------------------------------------------------- /avantage/server/app/errors/errorMapper.ts: -------------------------------------------------------------------------------- 1 | export function getMappedZodErrors(zodError: any) { 2 | const errors = new Map() 3 | JSON.parse(zodError).forEach((zodError: any) => { 4 | errors.set(zodError.path[0], { 'message': zodError.message }) 5 | }) 6 | return JSON.stringify(Object.fromEntries(errors)) 7 | } 8 | 9 | export function getMappedError(errorType: string, message: string) { 10 | const errors = new Map() 11 | errors.set(errorType, { 'message': message }) 12 | return JSON.stringify(Object.fromEntries(errors)) 13 | } -------------------------------------------------------------------------------- /avantage/server/app/errors/responses/DefaultErrorsResponse.ts: -------------------------------------------------------------------------------- 1 | import { H3Event } from "h3" 2 | import { getMappedError } from "~/server/app/errors/errorMapper" 3 | 4 | export default async function sendDefaultErrorResponse(event: H3Event, errorType: string, statusCode: number, error: any) { 5 | const parsedErrors = getMappedError(errorType, error) 6 | return sendError(event, createError({ statusCode: statusCode, statusMessage: 'Invalid Data Provided', data: parsedErrors })) 7 | } 8 | -------------------------------------------------------------------------------- /avantage/server/app/errors/responses/ZodErrorsResponse.ts: -------------------------------------------------------------------------------- 1 | import { H3Event } from "h3" 2 | import { getMappedZodErrors } from "../errorMapper" 3 | 4 | export default async function sendZodErrorResponse(event: H3Event, errorData: any) { 5 | const parsedErrors = getMappedZodErrors(errorData) 6 | return sendError(event, createError({ statusCode: 422, statusMessage: 'Invalid Data Provided', data: parsedErrors })) 7 | } 8 | -------------------------------------------------------------------------------- /avantage/server/app/formRequests/LoginRequest.ts: -------------------------------------------------------------------------------- 1 | import { z, parseBodyAs, } from "@sidebase/nuxt-parse" 2 | import { H3Event } from "h3" 3 | 4 | const bodySchema = z.object({ 5 | usernameOrEmail: z.string({ 6 | required_error: 'username or email required', 7 | }) 8 | .min(1, { message: 'username or email required' }), 9 | password: z.string({ 10 | required_error: 'password required', 11 | }) 12 | .min(8, { message: 'password must be at least 8 characters' }) 13 | }) 14 | 15 | export default async function loginRequest(event: H3Event) { 16 | return await parseBodyAs(event, bodySchema) 17 | } -------------------------------------------------------------------------------- /avantage/server/app/formRequests/RegisterRequest.ts: -------------------------------------------------------------------------------- 1 | import { z, parseBodyAs, } from "@sidebase/nuxt-parse" 2 | import { H3Event } from "h3" 3 | 4 | const bodySchema = z.object({ 5 | username: z.string({ 6 | required_error: 'username required', 7 | }) 8 | .min(1, { message: 'username required' }), 9 | 10 | name: z.string({ 11 | required_error: 'name required', 12 | }) 13 | .min(1, { message: 'name required' }), 14 | 15 | email: z.string({ 16 | required_error: 'valid email required', 17 | }).email({ message: 'valid email required' }), 18 | 19 | password: z.string({ 20 | required_error: 'password required', 21 | }) 22 | .min(8, { message: 'password must be at least 8 characters' }) 23 | }) 24 | 25 | export default async function registerRequest(event: H3Event) { 26 | return await parseBodyAs(event, bodySchema) 27 | } -------------------------------------------------------------------------------- /avantage/server/app/formRequests/UpdateUserRequest.ts: -------------------------------------------------------------------------------- 1 | import { z, parseBodyAs, } from "@sidebase/nuxt-parse" 2 | import { H3Event } from "h3" 3 | 4 | const bodySchema = z.object({ 5 | username: z.string({ 6 | required_error: 'username required', 7 | }) 8 | .min(1, { message: 'username required' }), 9 | 10 | name: z.string({ 11 | required_error: 'name required', 12 | }) 13 | .min(1, { message: 'name required' }), 14 | 15 | email: z.string({ 16 | required_error: 'valid email required', 17 | }).email({ message: 'valid email required' }) 18 | }) 19 | 20 | export default async function updateUserRequest(event: H3Event) { 21 | return await parseBodyAs(event, bodySchema) 22 | } -------------------------------------------------------------------------------- /avantage/server/app/formRequests/VerifyOtpRequest.ts: -------------------------------------------------------------------------------- 1 | import { z, parseBodyAs, } from "@sidebase/nuxt-parse" 2 | import { H3Event } from "h3" 3 | 4 | const bodySchema = z.object({ 5 | otp: z.string({ 6 | required_error: 'otp is required', 7 | }) 8 | .min(6, { message: 'otp is required' }) 9 | }) 10 | 11 | export default async function verifyOtpRequest(event: H3Event) { 12 | return await parseBodyAs(event, bodySchema) 13 | } -------------------------------------------------------------------------------- /avantage/server/app/services/otp.ts: -------------------------------------------------------------------------------- 1 | import { createStorage } from 'unstorage' 2 | const storage = createStorage() 3 | 4 | export const createOtp = async (userId: number): Promise => { 5 | const otp = Math.floor(100000 + Math.random() * 900000) 6 | 7 | await storage.setItem(`otp:${userId}`, otp) 8 | 9 | return otp 10 | } 11 | 12 | export const verifyOtp = async (userId: number, otp: number) => { 13 | 14 | const key = `otp:${userId}` 15 | 16 | console.log('verify-key', key) 17 | 18 | const storedOtp = await storage.getItem(key) 19 | 20 | if (otp === storedOtp) { 21 | return true 22 | } 23 | 24 | return false 25 | } -------------------------------------------------------------------------------- /avantage/server/app/services/sessionService.ts: -------------------------------------------------------------------------------- 1 | import { sanitizeUserForFrontend } from '~~/server/app/services/userService'; 2 | import { H3Event } from "h3" 3 | import { createSession, getUserByAuthToken } from "~~/server/database/repositories/sessionRepository" 4 | import { v4 as uuidv4 } from 'uuid' 5 | import { User } from '@prisma/client'; 6 | import sendDefaultErrorResponse from '~/server/app/errors/responses/DefaultErrorsResponse'; 7 | import { IUserSanitized } from '~~/types/IUser'; 8 | 9 | export async function makeSession(user: User, event: H3Event): Promise { 10 | const authToken = uuidv4().replaceAll('-', '') 11 | const session = await createSession(authToken, user ) 12 | const userId = session.user.id 13 | 14 | if (userId) { 15 | setCookie(event, 'auth_token', authToken, { path: '/', httpOnly: true }) 16 | return getSanitizedUserBySessionToken(authToken) 17 | } 18 | 19 | throw Error('Error Creating Session') 20 | } 21 | 22 | export async function getUserByEvent(event: H3Event) { 23 | const authToken = getCookie(event, 'auth_token') 24 | 25 | if (authToken === undefined) { 26 | return await sendDefaultErrorResponse(event, 'Unauthenticated', 403, 'Unauthenticated') 27 | } 28 | 29 | return await getSanitizedUserBySessionToken(authToken) 30 | 31 | } 32 | 33 | export async function getSanitizedUserBySessionToken(authToken: string): Promise { 34 | const user = await getUserByAuthToken(authToken) 35 | 36 | return sanitizeUserForFrontend(user) 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /avantage/server/app/services/stripeService.ts: -------------------------------------------------------------------------------- 1 | import { SubPostRes } from '~/types/SubPostRes'; 2 | import { createOrUpdateSubscription, getSubscriptionById, getUserByStripeCustomerId} from "~/server/database/repositories/userRespository" 3 | import { IUser } from '~/types/IUser'; 4 | import Stripe from 'stripe'; 5 | import { ISubscription } from '~~/types/ISubscription'; 6 | 7 | const config = useRuntimeConfig() 8 | const stripe = new Stripe(config.stripeSecretKey, null); 9 | 10 | export async function getSubscribeUrl(lookupKey: string, user: IUser): Promise { 11 | 12 | const customerEmail = user.email 13 | 14 | const price = await stripe.prices.retrieve( 15 | lookupKey 16 | ); 17 | 18 | let shouldUpdateUser = false 19 | 20 | if(!user.stripeCustomerId) { 21 | shouldUpdateUser = true 22 | const customer = await stripe.customers.create({ email: customerEmail }) 23 | user.stripeCustomerId = customer.id 24 | } 25 | 26 | const session = await stripe.checkout.sessions.create({ 27 | billing_address_collection: 'auto', 28 | line_items: [ 29 | { 30 | price: price.id, 31 | quantity: 1, 32 | 33 | }, 34 | ], 35 | mode: 'subscription', 36 | success_url: `${config.public.appDomain}/subscribe/success?session_id={CHECKOUT_SESSION_ID}`, 37 | cancel_url: `${config.public.appDomain}/subscribe/cancel`, 38 | customer: user.stripeCustomerId 39 | }); 40 | 41 | return {url: session.url, user, shouldUpdateUser} 42 | } 43 | 44 | export async function handleSubscriptionChange(subscription: Stripe.Subscription, lastEventDate: number): Promise { 45 | const localSubscription = await getSubscriptionById(subscription.id) 46 | 47 | if(localSubscription?.lastEventDate > lastEventDate){ 48 | return true 49 | } 50 | 51 | const stripeCustomerId = subscription.customer as string 52 | 53 | const user = await getUserByStripeCustomerId(stripeCustomerId) 54 | 55 | const data = { 56 | userId: user.id, 57 | name: subscription.id, 58 | stripeId: subscription.id, 59 | stripeStatus: subscription.status, 60 | stripePriceId: subscription.items.data[0].price.id, 61 | quantity: subscription.description, 62 | trialEndsAt: subscription.trial_end, 63 | endsAt: subscription.ended_at, 64 | startDate: subscription.start_date, 65 | lastEventDate: lastEventDate 66 | } as unknown as ISubscription 67 | 68 | await createOrUpdateSubscription(data) 69 | 70 | return true; 71 | } -------------------------------------------------------------------------------- /avantage/server/app/services/userService.ts: -------------------------------------------------------------------------------- 1 | import { RegistationRequest } from '~~/types/IRegistration'; 2 | import { H3Event } from 'h3'; 3 | import { getSanitizedUserBySessionToken } from './sessionService'; 4 | import { isString } from '@vueuse/core'; 5 | import { IUserSanitized } from '~~/types/IUser'; 6 | import { validate } from './validator'; 7 | import { validateRegistration } from '~/server/app/services/validator' 8 | import { User } from '@prisma/client'; 9 | 10 | export async function validateUser(data: RegistationRequest) { 11 | 12 | const errors = await validate(data, validateRegistration) 13 | 14 | if (errors.size > 0) { 15 | 16 | return { hasErrors: true, errors } 17 | } 18 | 19 | return { hasErrors: false } 20 | } 21 | 22 | export function sanitizeUserForFrontend(user: User): IUserSanitized { 23 | 24 | const userSanitized = { 25 | id: user.id, 26 | username: user.username, 27 | name: user.name, 28 | email: user.email 29 | } 30 | 31 | return userSanitized as IUserSanitized 32 | } 33 | 34 | export async function authCheck(event: H3Event): Promise { 35 | 36 | const authToken = getCookie(event, 'auth_token') 37 | const hasAuthToken = isString(authToken) 38 | 39 | if (!hasAuthToken) { 40 | return false 41 | } 42 | 43 | const user = await getSanitizedUserBySessionToken(authToken) 44 | 45 | if (user?.id) { 46 | return true 47 | } 48 | 49 | return false 50 | } 51 | -------------------------------------------------------------------------------- /avantage/server/app/services/validator.ts: -------------------------------------------------------------------------------- 1 | import { getUserByEmail, getUserByUserName } from "~/server/database/repositories/userRespository" 2 | import { RegistationRequest } from "~/types/IRegistration" 3 | 4 | 5 | export async function validate(data: RegistationRequest, validationCallback: { (key: string, value: string): Promise}) { 6 | 7 | const errors = new Map() 8 | 9 | for (const [key, value] of Object.entries(data)) { 10 | let val = await validationCallback(key, value) 11 | 12 | if (val.hasError) { 13 | errors.set(key, { 'message': val.errorMessage }) 14 | } 15 | } 16 | 17 | return errors 18 | } 19 | 20 | export async function validateRegistration(key: string, value: string): Promise { 21 | const check: InputValidation = { 22 | value, 23 | isBlank: false, 24 | lenghtMin8: true, 25 | key, 26 | hasError: false 27 | } 28 | 29 | if (key == 'password') { 30 | if (value.length < 8) { 31 | check.hasError = true 32 | check.errorMessage = `password must be at least 8 characters` 33 | } 34 | check.lenghtMin8 = false 35 | } 36 | 37 | if (key == 'email') { 38 | const email = await getUserByEmail(value) 39 | if (email) { 40 | check.emailTaken = true 41 | check.hasError = true 42 | check.errorMessage = `Email is invalid or already taken` 43 | } 44 | } 45 | 46 | if (key == 'username') { 47 | const username = await getUserByUserName(value) 48 | if (username) { 49 | check.usernameTaken = true 50 | check.hasError = true 51 | check.errorMessage = `Username is invalid or already taken` 52 | } 53 | } 54 | 55 | return check 56 | } 57 | -------------------------------------------------------------------------------- /avantage/server/database/client.ts: -------------------------------------------------------------------------------- 1 | import pkg from "@prisma/client"; 2 | 3 | const { PrismaClient } = pkg; 4 | const prisma = new PrismaClient() 5 | export default prisma 6 | -------------------------------------------------------------------------------- /avantage/server/database/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jurassicjs/nuxt3-docker-tut/d748a2bb1dce454ce6ae6b838722a5d1690f1da1/avantage/server/database/dev.db -------------------------------------------------------------------------------- /avantage/server/database/migrations/20220928204235_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `User` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `loginType` VARCHAR(191) NULL DEFAULT 'email', 5 | `password` VARCHAR(191) NULL, 6 | `email` VARCHAR(191) NULL, 7 | `name` VARCHAR(191) NULL, 8 | `username` VARCHAR(191) NULL, 9 | `stripeCustomerId` VARCHAR(191) NULL, 10 | 11 | UNIQUE INDEX `User_email_key`(`email`), 12 | UNIQUE INDEX `User_username_key`(`username`), 13 | PRIMARY KEY (`id`) 14 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 15 | 16 | -- CreateTable 17 | CREATE TABLE `Session` ( 18 | `id` INTEGER NOT NULL AUTO_INCREMENT, 19 | `authToken` VARCHAR(191) NOT NULL, 20 | `userId` INTEGER NOT NULL, 21 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 22 | `updatedAt` DATETIME(3) NOT NULL, 23 | `deletedAt` DATETIME(3) NULL, 24 | 25 | UNIQUE INDEX `Session_authToken_key`(`authToken`), 26 | PRIMARY KEY (`id`) 27 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 28 | 29 | -- CreateTable 30 | CREATE TABLE `Subscription` ( 31 | `id` INTEGER NOT NULL AUTO_INCREMENT, 32 | `userId` INTEGER NOT NULL, 33 | `stripeId` VARCHAR(191) NOT NULL, 34 | `stripeStatus` VARCHAR(191) NULL, 35 | `stripePriceId` VARCHAR(191) NULL, 36 | `quantity` INTEGER NULL, 37 | `trialEndsAt` INTEGER NULL, 38 | `endsAt` INTEGER NULL, 39 | `startDate` INTEGER NOT NULL, 40 | `lastEventDate` INTEGER NOT NULL, 41 | 42 | UNIQUE INDEX `Subscription_stripeId_key`(`stripeId`), 43 | PRIMARY KEY (`id`) 44 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 45 | 46 | -- CreateTable 47 | CREATE TABLE `Question` ( 48 | `id` INTEGER NOT NULL AUTO_INCREMENT, 49 | `authorId` INTEGER NOT NULL, 50 | `title` VARCHAR(191) NOT NULL, 51 | `description` VARCHAR(191) NOT NULL, 52 | 53 | FULLTEXT INDEX `Question_title_description_idx`(`title`, `description`), 54 | PRIMARY KEY (`id`) 55 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 56 | 57 | -- CreateTable 58 | CREATE TABLE `Answer` ( 59 | `id` INTEGER NOT NULL AUTO_INCREMENT, 60 | `questionId` INTEGER NOT NULL, 61 | `authorId` INTEGER NOT NULL, 62 | `text` VARCHAR(191) NOT NULL, 63 | 64 | FULLTEXT INDEX `Answer_text_idx`(`text`), 65 | PRIMARY KEY (`id`) 66 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 67 | 68 | -- CreateTable 69 | CREATE TABLE `Category` ( 70 | `id` INTEGER NOT NULL AUTO_INCREMENT, 71 | `name` VARCHAR(191) NOT NULL, 72 | `url` VARCHAR(191) NOT NULL, 73 | `image` VARCHAR(191) NULL, 74 | `accentColor` VARCHAR(191) NULL, 75 | 76 | UNIQUE INDEX `Category_name_key`(`name`), 77 | UNIQUE INDEX `Category_url_key`(`url`), 78 | PRIMARY KEY (`id`) 79 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 80 | 81 | -- CreateTable 82 | CREATE TABLE `Topic` ( 83 | `id` INTEGER NOT NULL AUTO_INCREMENT, 84 | `name` VARCHAR(191) NOT NULL, 85 | `displayName` VARCHAR(191) NULL, 86 | `showName` BOOLEAN NULL, 87 | `url` VARCHAR(191) NOT NULL, 88 | `image` VARCHAR(191) NULL, 89 | `accentColor` VARCHAR(191) NULL, 90 | 91 | UNIQUE INDEX `Topic_name_key`(`name`), 92 | UNIQUE INDEX `Topic_url_key`(`url`), 93 | PRIMARY KEY (`id`) 94 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 95 | 96 | -- CreateTable 97 | CREATE TABLE `Series` ( 98 | `id` INTEGER NOT NULL AUTO_INCREMENT, 99 | `name` VARCHAR(191) NOT NULL, 100 | `displayName` VARCHAR(191) NULL, 101 | `url` VARCHAR(191) NOT NULL, 102 | `topicId` INTEGER NULL, 103 | `image` VARCHAR(191) NULL, 104 | `accentColor` VARCHAR(191) NULL, 105 | 106 | UNIQUE INDEX `Series_name_key`(`name`), 107 | UNIQUE INDEX `Series_url_key`(`url`), 108 | PRIMARY KEY (`id`) 109 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 110 | 111 | -- CreateTable 112 | CREATE TABLE `Video` ( 113 | `id` INTEGER NOT NULL AUTO_INCREMENT, 114 | `url` VARCHAR(191) NOT NULL, 115 | `host_type` VARCHAR(191) NOT NULL, 116 | `host_id` VARCHAR(191) NOT NULL, 117 | `title` VARCHAR(191) NOT NULL, 118 | `subtitle` VARCHAR(191) NOT NULL, 119 | `description` VARCHAR(191) NULL, 120 | `image` VARCHAR(191) NULL, 121 | `topicId` INTEGER NOT NULL, 122 | `seriesId` INTEGER NULL, 123 | `seriesPosition` INTEGER NULL, 124 | `accentColor` VARCHAR(191) NULL, 125 | 126 | UNIQUE INDEX `Video_url_key`(`url`), 127 | PRIMARY KEY (`id`) 128 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 129 | 130 | -- CreateTable 131 | CREATE TABLE `Tag` ( 132 | `id` INTEGER NOT NULL AUTO_INCREMENT, 133 | `name` VARCHAR(191) NOT NULL, 134 | `accentColor` VARCHAR(191) NULL, 135 | 136 | PRIMARY KEY (`id`) 137 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 138 | 139 | -- CreateTable 140 | CREATE TABLE `TagAssignment` ( 141 | `id` INTEGER NOT NULL AUTO_INCREMENT, 142 | `entity_type` VARCHAR(191) NOT NULL, 143 | `entity_id` INTEGER NOT NULL, 144 | `tagId` INTEGER NOT NULL, 145 | 146 | PRIMARY KEY (`id`) 147 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 148 | 149 | -- CreateTable 150 | CREATE TABLE `CategoryAssignment` ( 151 | `id` INTEGER NOT NULL AUTO_INCREMENT, 152 | `entity_type` VARCHAR(191) NOT NULL, 153 | `entity_id` INTEGER NOT NULL, 154 | `categoryId` INTEGER NOT NULL, 155 | 156 | PRIMARY KEY (`id`) 157 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 158 | 159 | -- AddForeignKey 160 | ALTER TABLE `Session` ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 161 | 162 | -- AddForeignKey 163 | ALTER TABLE `Subscription` ADD CONSTRAINT `Subscription_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 164 | 165 | -- AddForeignKey 166 | ALTER TABLE `Question` ADD CONSTRAINT `Question_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 167 | 168 | -- AddForeignKey 169 | ALTER TABLE `Answer` ADD CONSTRAINT `Answer_questionId_fkey` FOREIGN KEY (`questionId`) REFERENCES `Question`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 170 | 171 | -- AddForeignKey 172 | ALTER TABLE `Answer` ADD CONSTRAINT `Answer_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 173 | 174 | -- AddForeignKey 175 | ALTER TABLE `Series` ADD CONSTRAINT `Series_topicId_fkey` FOREIGN KEY (`topicId`) REFERENCES `Topic`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; 176 | 177 | -- AddForeignKey 178 | ALTER TABLE `Video` ADD CONSTRAINT `Video_topicId_fkey` FOREIGN KEY (`topicId`) REFERENCES `Topic`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 179 | 180 | -- AddForeignKey 181 | ALTER TABLE `Video` ADD CONSTRAINT `Video_seriesId_fkey` FOREIGN KEY (`seriesId`) REFERENCES `Series`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; 182 | 183 | -- AddForeignKey 184 | ALTER TABLE `TagAssignment` ADD CONSTRAINT `TagAssignment_tagId_fkey` FOREIGN KEY (`tagId`) REFERENCES `Topic`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 185 | 186 | -- AddForeignKey 187 | ALTER TABLE `CategoryAssignment` ADD CONSTRAINT `CategoryAssignment_categoryId_fkey` FOREIGN KEY (`categoryId`) REFERENCES `Category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 188 | -------------------------------------------------------------------------------- /avantage/server/database/migrations/20220929201021_add_test_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `TestMigration` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | 5 | PRIMARY KEY (`id`) 6 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 7 | -------------------------------------------------------------------------------- /avantage/server/database/migrations/20220930194208_add_test_another_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `TestAnotherMigration` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | 5 | PRIMARY KEY (`id`) 6 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 7 | -------------------------------------------------------------------------------- /avantage/server/database/migrations/20221104204024_add_test_another_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `Subscription` MODIFY `startDate` INTEGER NULL, 3 | MODIFY `lastEventDate` INTEGER NULL; 4 | -------------------------------------------------------------------------------- /avantage/server/database/migrations/20221215190709_dev/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Answer` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `Category` table. If the table is not empty, all the data it contains will be lost. 6 | - You are about to drop the `CategoryAssignment` table. If the table is not empty, all the data it contains will be lost. 7 | - You are about to drop the `Question` table. If the table is not empty, all the data it contains will be lost. 8 | - You are about to drop the `Series` table. If the table is not empty, all the data it contains will be lost. 9 | - You are about to drop the `Tag` table. If the table is not empty, all the data it contains will be lost. 10 | - You are about to drop the `TagAssignment` table. If the table is not empty, all the data it contains will be lost. 11 | - You are about to drop the `TestAnotherMigration` table. If the table is not empty, all the data it contains will be lost. 12 | - You are about to drop the `TestMigration` table. If the table is not empty, all the data it contains will be lost. 13 | - You are about to drop the `Topic` table. If the table is not empty, all the data it contains will be lost. 14 | - You are about to drop the `Video` table. If the table is not empty, all the data it contains will be lost. 15 | - Made the column `startDate` on table `Subscription` required. This step will fail if there are existing NULL values in that column. 16 | - Made the column `lastEventDate` on table `Subscription` required. This step will fail if there are existing NULL values in that column. 17 | 18 | */ 19 | -- DropForeignKey 20 | ALTER TABLE `Answer` DROP FOREIGN KEY `Answer_authorId_fkey`; 21 | 22 | -- DropForeignKey 23 | ALTER TABLE `Answer` DROP FOREIGN KEY `Answer_questionId_fkey`; 24 | 25 | -- DropForeignKey 26 | ALTER TABLE `CategoryAssignment` DROP FOREIGN KEY `CategoryAssignment_categoryId_fkey`; 27 | 28 | -- DropForeignKey 29 | ALTER TABLE `Question` DROP FOREIGN KEY `Question_authorId_fkey`; 30 | 31 | -- DropForeignKey 32 | ALTER TABLE `Series` DROP FOREIGN KEY `Series_topicId_fkey`; 33 | 34 | -- DropForeignKey 35 | ALTER TABLE `TagAssignment` DROP FOREIGN KEY `TagAssignment_tagId_fkey`; 36 | 37 | -- DropForeignKey 38 | ALTER TABLE `Video` DROP FOREIGN KEY `Video_seriesId_fkey`; 39 | 40 | -- DropForeignKey 41 | ALTER TABLE `Video` DROP FOREIGN KEY `Video_topicId_fkey`; 42 | 43 | -- AlterTable 44 | ALTER TABLE `Subscription` MODIFY `startDate` INTEGER NOT NULL, 45 | MODIFY `lastEventDate` INTEGER NOT NULL; 46 | 47 | -- DropTable 48 | DROP TABLE `Answer`; 49 | 50 | -- DropTable 51 | DROP TABLE `Category`; 52 | 53 | -- DropTable 54 | DROP TABLE `CategoryAssignment`; 55 | 56 | -- DropTable 57 | DROP TABLE `Question`; 58 | 59 | -- DropTable 60 | DROP TABLE `Series`; 61 | 62 | -- DropTable 63 | DROP TABLE `Tag`; 64 | 65 | -- DropTable 66 | DROP TABLE `TagAssignment`; 67 | 68 | -- DropTable 69 | DROP TABLE `TestAnotherMigration`; 70 | 71 | -- DropTable 72 | DROP TABLE `TestMigration`; 73 | 74 | -- DropTable 75 | DROP TABLE `Topic`; 76 | 77 | -- DropTable 78 | DROP TABLE `Video`; 79 | -------------------------------------------------------------------------------- /avantage/server/database/migrations/20221226101719_dev/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `User` ADD COLUMN `isEmailVerified` BOOLEAN NULL; 3 | -------------------------------------------------------------------------------- /avantage/server/database/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /avantage/server/database/repositories/askJackRespository.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/server/database/client"; 2 | 3 | export async function createQuestion(data: IQuestionPost, authorId: number) { 4 | return await prisma.question.create({ 5 | data: { 6 | authorId: authorId, 7 | title: data.title, 8 | description: data.description 9 | } 10 | }) 11 | } 12 | 13 | export async function findQuestion(id: number) { 14 | return await prisma.question.findUnique({ 15 | where: { 16 | id: id, 17 | }, 18 | include: { 19 | answers: true, 20 | }, 21 | }) 22 | } 23 | 24 | export async function createAnswer(data: IAnswerPost, authorId: number) { 25 | return await prisma.answer.create({ 26 | data: { 27 | authorId: authorId, 28 | questionId: data.questionId, 29 | text: data.text, 30 | 31 | } 32 | }) 33 | } 34 | 35 | export async function searchQuestions(query: string) { 36 | return await prisma.question.findMany({ 37 | where: { 38 | OR: [ 39 | { 40 | title: { 41 | contains: query, 42 | } 43 | }, 44 | { 45 | description: { 46 | contains: query 47 | } 48 | }, 49 | ], 50 | }, 51 | }) 52 | } 53 | 54 | 55 | export async function editQuestion(question: IQuestionPost) { 56 | return await prisma.question.update({ 57 | where: { 58 | id: question.id, 59 | }, 60 | data: { 61 | title: question.title, 62 | description: question.description, 63 | }, 64 | }) 65 | 66 | } export async function deleteQuestion(questionId: number) { 67 | return await prisma.question.delete({ 68 | where: { 69 | id: questionId, 70 | } 71 | }) 72 | } -------------------------------------------------------------------------------- /avantage/server/database/repositories/sessionRepository.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/server/database/client"; 2 | import { IUserSession } from '~~/types/ISession'; 3 | import { User } from '@prisma/client'; 4 | 5 | export async function createSession(authToken: string, user: User): Promise { 6 | if (!authToken) { 7 | throw Error('missing auth token for session') 8 | } 9 | 10 | const session = await prisma.session.create({ 11 | data: { 12 | userId: user.id, 13 | authToken: authToken 14 | }, 15 | }) 16 | 17 | return {user: user, authToken: authToken, sessionId: session.id} 18 | } 19 | 20 | async function getSessionByAuthToken(authToken: string) { 21 | const session = await prisma.session.findUnique({ 22 | where: { 23 | authToken: authToken, 24 | } 25 | }) 26 | 27 | return session 28 | } 29 | 30 | export async function getUserByAuthToken(authToken: string): Promise { 31 | const session = await getSessionByAuthToken(authToken) 32 | const user = await prisma.session.findUnique({ 33 | where: { 34 | id: session?.id, 35 | } 36 | }).user() 37 | 38 | if(user === null) { 39 | throw new Error(`no user found for authToken ${authToken}`) 40 | } 41 | 42 | return user 43 | } 44 | -------------------------------------------------------------------------------- /avantage/server/database/repositories/userRespository.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | import prisma from "~/server/database/client"; 3 | import { IUser } from '~/types/IUser'; 4 | import { ISubscription } from "~/types/ISubscription"; 5 | 6 | export async function getUserByEmail(emailOrEmail: string): Promise { 7 | return await prisma.user.findFirst({ 8 | where: { 9 | OR: 10 | [ 11 | { email: emailOrEmail }, 12 | { username: emailOrEmail }, 13 | ] 14 | } 15 | }) 16 | } 17 | 18 | export async function getUserByUserName(username: string) { 19 | return await prisma.user.findUnique({ 20 | where: { 21 | username: username, 22 | }, 23 | select: { 24 | id: true, 25 | username: true, 26 | }, 27 | }) 28 | } 29 | 30 | export async function createUser(data: IUser) { 31 | const user = await prisma.user.create({ 32 | data: { 33 | username: data.username, 34 | name: data.name, 35 | email: data.email, 36 | loginType: data.loginType, 37 | password: data.password, 38 | }, 39 | }) 40 | 41 | return user 42 | } 43 | export async function updateUser(data: IUser) { 44 | const user = await prisma.user.update({ 45 | where: { 46 | id: data.id 47 | }, 48 | data: { 49 | username: data.username, 50 | name: data.name, 51 | email: data.email 52 | }, 53 | }) 54 | 55 | return user 56 | } 57 | 58 | export async function getUserById(id: number) { 59 | return await prisma.user.findUnique({ 60 | where: { 61 | id: id, 62 | }, 63 | select: { 64 | id: true, 65 | username: true, 66 | email: true, 67 | stripeCustomerId: true, 68 | }, 69 | }) 70 | } 71 | 72 | export async function getUserByStripeCustomerId(stripeCustomerId: string) { 73 | return await prisma.user.findFirst({ 74 | where: { 75 | stripeCustomerId: stripeCustomerId, 76 | }, 77 | select: { 78 | id: true, 79 | username: true, 80 | email: true, 81 | stripeCustomerId: true, 82 | }, 83 | }) 84 | } 85 | 86 | export async function getSubscriptionById(stripeId: string) { 87 | return await prisma.subscription.findFirst({ 88 | where: { 89 | stripeId: stripeId, 90 | } 91 | }) 92 | } 93 | 94 | export async function updateStripeCustomerId(data: IUser) { 95 | return await prisma.user.update({ 96 | where: { email: data.email }, 97 | data: { 98 | stripeCustomerId: data.stripeCustomerId, 99 | } 100 | }) 101 | } 102 | 103 | export async function createOrUpdateSubscription(data: ISubscription) { 104 | return await prisma.subscription.upsert({ 105 | where: { 106 | stripeId: data.stripeId 107 | }, 108 | create: { 109 | userId: data.userId, 110 | stripeId: data.stripeId, 111 | stripeStatus: data.stripeStatus, 112 | stripePriceId: data.stripePriceId, 113 | quantity: data.quantity, 114 | trialEndsAt: data.trialEndsAt, 115 | endsAt: data.endsAt, 116 | lastEventDate: data.lastEventDate, 117 | startDate: data.startDate 118 | }, 119 | update: { 120 | stripeStatus: data.stripeStatus, 121 | stripePriceId: data.stripePriceId, 122 | quantity: data.quantity, 123 | trialEndsAt: data.trialEndsAt, 124 | endsAt: data.endsAt, 125 | lastEventDate: data.lastEventDate, 126 | startDate: data.startDate 127 | } 128 | }) 129 | } 130 | 131 | export async function updateIsEmailVerified(userId: number, isVerified: boolean) { 132 | const user = await prisma.user.update({ 133 | where: { 134 | id: userId 135 | }, 136 | data: { 137 | isEmailVerified: isVerified, 138 | } 139 | }) 140 | 141 | return user 142 | } 143 | -------------------------------------------------------------------------------- /avantage/server/database/repositories/videoRepository.ts: -------------------------------------------------------------------------------- 1 | import { Series, Topic, Video } from "@prisma/client"; 2 | import prisma from "~/server/database/client"; 3 | 4 | 5 | export async function getTopics(): Promise { 6 | return await prisma.topic.findMany() 7 | } 8 | 9 | export async function getSeriesByTopicId(topicId: number): Promise { 10 | return await prisma.series.findMany({ 11 | where: { 12 | topicId: { 13 | equals: topicId 14 | } 15 | } 16 | }) 17 | } 18 | 19 | export async function getVideosBySeriesId(seriesId: number): Promise { 20 | return await prisma.video.findMany({ 21 | where: { 22 | seriesId: { 23 | equals: seriesId 24 | } 25 | } 26 | }) 27 | } 28 | 29 | export async function getVideosByTopicId(topicId: number): Promise { 30 | return await prisma.video.findMany({ 31 | where: { 32 | topicId: { 33 | equals: topicId 34 | } 35 | } 36 | }) 37 | } 38 | 39 | export async function getTopicIdByName(topicName: string): Promise { 40 | const res = await prisma.topic.findFirst( 41 | { 42 | where: { 43 | name: { 44 | equals: topicName 45 | } 46 | } 47 | }) 48 | 49 | return res 50 | } -------------------------------------------------------------------------------- /avantage/server/database/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | generator client { 4 | provider = "prisma-client-js" 5 | binaryTargets = ["native", "debian-openssl-1.1.x"] 6 | previewFeatures = ["fullTextSearch", "fullTextIndex"] 7 | } 8 | 9 | datasource db { 10 | provider = "mysql" 11 | url = env("DATABASE_URL") 12 | } 13 | 14 | model User { 15 | id Int @id @default(autoincrement()) 16 | loginType String? @default("email") 17 | password String? 18 | email String? @unique 19 | name String? 20 | username String? @unique 21 | session Session[] 22 | stripeCustomerId String? 23 | isEmailVerified Boolean? 24 | Subscription Subscription[] 25 | } 26 | 27 | model Session { 28 | id Int @id @default(autoincrement()) 29 | authToken String @unique 30 | user User @relation(fields: [userId], references: [id]) 31 | userId Int 32 | createdAt DateTime @default(now()) 33 | updatedAt DateTime @updatedAt 34 | deletedAt DateTime? 35 | } 36 | 37 | model Subscription { 38 | id Int @id @default(autoincrement()) 39 | user User @relation(fields: [userId], references: [id]) 40 | userId Int 41 | stripeId String @unique 42 | stripeStatus String? 43 | stripePriceId String? 44 | quantity Int? 45 | trialEndsAt Int? 46 | endsAt Int? 47 | startDate Int 48 | lastEventDate Int 49 | } 50 | 51 | -------------------------------------------------------------------------------- /avantage/server/middleware/serverAuth.ts: -------------------------------------------------------------------------------- 1 | import { H3Event } from "h3" 2 | import { authCheck } from "~/server/app/services/userService" 3 | 4 | export default eventHandler(async (event) => { 5 | 6 | const isAllowed = await protectAuthRoute(event) 7 | 8 | if (!isAllowed) { 9 | return sendError(event, createError({ statusCode: 401, statusMessage: 'Unauthorized' })) 10 | } 11 | }) 12 | 13 | async function protectAuthRoute(event: H3Event): Promise { 14 | 15 | const protectedRoutes = [ 16 | '/api/dasboard', 17 | '/api/members-only', 18 | '/api/auth/verifyOtp' 19 | ] 20 | 21 | if (!event?.path || !protectedRoutes.includes(event.path)) { 22 | return true 23 | } 24 | 25 | return await authCheck(event) 26 | } -------------------------------------------------------------------------------- /avantage/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import colors from 'tailwindcss/colors' 2 | 3 | export default { 4 | darkMode: 'class', 5 | plugins: [ 6 | require('@tailwindcss/typography') 7 | ], 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: colors.gray 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /avantage/tests/feature/register.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest' 2 | import { setup, $fetch } from '@nuxt/test-utils-edge' 3 | import { v4 as uuidv4 } from 'uuid' 4 | import useErrorMapper from '../../composables/useErrorMapper' 5 | 6 | describe('Test Registration', async () => { 7 | 8 | await setup({ 9 | // test context options 10 | }) 11 | 12 | test('incorrectly formatted email returns error', async () => { 13 | const givenEmail = 'test@fullstackjack' 14 | 15 | await $fetch('/api/auth/register', 16 | { 17 | method: 'POST', 18 | responseType: 'json', 19 | body: { 20 | username: 'testUsername', 21 | name: 'test_name', 22 | email: givenEmail, 23 | password: '12345678' 24 | }, 25 | }).catch(error => { 26 | const errorMap = useErrorMapper(error.data.data) 27 | expect(errorMap.hasErrors).toBe(true) 28 | expect(errorMap.errors.has('email')).toBe(true) 29 | expect(errorMap.errors.get('email')?.message).toBe('valid email required') 30 | expect(error.message).toContain(`422 Invalid Data Provided`) 31 | 32 | }) 33 | }) 34 | 35 | test('already used email returns error', async () => { 36 | const givenUsername2 = uuidv4().replaceAll('-', '') 37 | const givenUsername = uuidv4().replaceAll('-', '') 38 | const givenName = uuidv4().replaceAll('-', '') 39 | const givenName2 = uuidv4().replaceAll('-', '') 40 | 41 | await $fetch('/api/auth/register', 42 | { 43 | method: 'POST', 44 | responseType: 'json', 45 | body: { 46 | username: givenUsername, 47 | name: givenName, 48 | email: 'testDublicate@fullstackjack.dev', 49 | password: '12345678' 50 | }, 51 | }).catch(error => {}) 52 | 53 | await $fetch('/api/auth/register', 54 | { 55 | method: 'POST', 56 | responseType: 'json', 57 | body: { 58 | username: givenUsername2, 59 | name: givenName2, 60 | email: 'testDublicate@fullstackjack.dev', 61 | password: '12345678' 62 | }, 63 | }).catch(error => { 64 | const errorMap = useErrorMapper(error.data.data) 65 | expect(errorMap.hasErrors).toBe(true) 66 | expect(errorMap.errors.has('email')).toBe(true) 67 | expect(errorMap.errors.get('email')?.message).toBe('Email is invalid or already taken') 68 | expect(error.message).toContain(`422 Unprocessable Entity`) 69 | }) 70 | }) 71 | 72 | test('already used name returns error', async () => { 73 | const givenEmail = uuidv4().replaceAll('-', '') + '@fullstackjack.dev' 74 | const givenEmail2 = uuidv4().replaceAll('-', '') + '@fullstackjack.dev' 75 | const givenName = uuidv4().replaceAll('-', '') 76 | const givenName2 = uuidv4().replaceAll('-', '') 77 | 78 | await $fetch('/api/auth/register', 79 | { 80 | method: 'POST', 81 | responseType: 'json', 82 | body: { 83 | username: 'duplicateUsername', 84 | name: givenName, 85 | email: givenEmail, 86 | password: '12345678' 87 | }, 88 | }).catch(error => { 89 | }) 90 | 91 | await $fetch('/api/auth/register', 92 | { 93 | method: 'POST', 94 | responseType: 'json', 95 | body: { 96 | username: 'duplicateUsername', 97 | name: givenName2, 98 | email: givenEmail2, 99 | password: '12345678' 100 | }, 101 | }).catch(error => { 102 | const errorMap = useErrorMapper(error.data.data) 103 | expect(errorMap.hasErrors).toBe(true) 104 | expect(errorMap.errors.has('username')).toBe(true) 105 | expect(errorMap.errors.get('username')?.message).toBe('Username is invalid or already taken') 106 | expect(error.message).toContain(`422 Unprocessable Entity`) 107 | }) 108 | }) 109 | 110 | test('vaild data registers new user', async () => { 111 | const givenEmail = uuidv4().replaceAll('-', '') + '@fullstackjack.dev' 112 | const givenUsername = uuidv4().replaceAll('-', '') 113 | const givenName = uuidv4().replaceAll('-', '') 114 | 115 | await $fetch('/api/auth/register', 116 | { 117 | method: 'POST', 118 | responseType: 'json', 119 | body: { 120 | username: givenUsername, 121 | name: givenName, 122 | email: givenEmail, 123 | password: '12345678' 124 | }, 125 | }).then(res => { 126 | console.log('****** 3 >>>>> ', res) 127 | expect(res.email).toBe(givenEmail) 128 | }) 129 | }) 130 | }) 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /avantage/tests/unit/register_validation.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import { validate } from '../../server/app/services/validator' 3 | import { validateRegistration } from '../..//server/app/services/validator' 4 | 5 | 6 | describe('test email validation', async () => { 7 | 8 | vi.mock('~/server/database/repositories/userRespository', () => { 9 | return { 10 | getUserByEmail: vi.fn(() => ({ email: 'test@fullstackjack.com' })), 11 | getUserByUserName: vi.fn(() => ({ email: 'test@fullstackjack.com' })) 12 | } 13 | }) 14 | 15 | it('should return error if email missing', async () => { 16 | const res = await validate({ 17 | username: '', 18 | name: '', 19 | email: undefined, 20 | password: '1234567' 21 | }, validateRegistration) 22 | 23 | const emailVal = res.get('email') 24 | 25 | expect(res.has('email')).toBe(true) 26 | expect(emailVal?.message).toContain('Email is invalid or already taken') 27 | }) 28 | 29 | 30 | it('should return error for improperly formatted email', async () => { 31 | const res = await validate({ 32 | username: '', 33 | name: '', 34 | email: 'test', 35 | password: '1234567' 36 | }, validateRegistration) 37 | 38 | const emailVal = res.get('email') 39 | 40 | expect(res.has('email')).toBe(true) 41 | expect(emailVal?.message).toContain('Email is invalid or already taken') 42 | }) 43 | 44 | it('should return error if email taken', async () => { 45 | 46 | const email = 'test@fullstackjack.com' 47 | 48 | const res = await validate({ 49 | username: '', 50 | name: '', 51 | email: email, 52 | password: '1234567' 53 | }, validateRegistration) 54 | 55 | const emailVal = res.get('email') 56 | 57 | expect(res.has('email')).toBe(true) 58 | expect(emailVal?.message).toContain(`Email is invalid or already taken`) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /avantage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /avantage/types/FormValidation.ts: -------------------------------------------------------------------------------- 1 | type FormValidation = { 2 | hasErrors: boolean 3 | errors?: Map 4 | loggedIn?: boolean 5 | }; 6 | 7 | type otpValidation = { 8 | hasErrors: boolean 9 | errors?: Map 10 | isVerified?: boolean 11 | }; 12 | 13 | type FormErrors = { 14 | field: string 15 | message: InputValidation 16 | } -------------------------------------------------------------------------------- /avantage/types/IAnswer.ts: -------------------------------------------------------------------------------- 1 | type IAnswer = { 2 | text: string 3 | authorId: number 4 | authorName?: string 5 | }; -------------------------------------------------------------------------------- /avantage/types/IAnswerPost.ts: -------------------------------------------------------------------------------- 1 | type IAnswerPost = { 2 | text: string; 3 | questionId: number 4 | }; -------------------------------------------------------------------------------- /avantage/types/ICategory.ts: -------------------------------------------------------------------------------- 1 | export interface ICategory { 2 | title: string; 3 | message?: string; 4 | image?: string; 5 | link?: string; 6 | lessonQuantity?: number; 7 | tags?: ITag[]; 8 | } 9 | 10 | export interface ITag { 11 | title: string; 12 | link: string; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /avantage/types/ILogin.ts: -------------------------------------------------------------------------------- 1 | export type ILoginErrors = { 2 | hasErrors?: string 3 | } 4 | 5 | export type LoginResponse = { 6 | hasErrors: boolean, 7 | errors?: ILoginErrors 8 | } 9 | 10 | export type LoginRequest = { 11 | usernameOrEmail: string, 12 | password: string 13 | } -------------------------------------------------------------------------------- /avantage/types/IQuestion.ts: -------------------------------------------------------------------------------- 1 | type IQuestion = { 2 | id: number; 3 | authorId: number; 4 | authName?: string; 5 | title: string; 6 | description: string; 7 | answers: IAnswer[]; 8 | }; -------------------------------------------------------------------------------- /avantage/types/IQuestionPost.ts: -------------------------------------------------------------------------------- 1 | type IQuestionPost = { 2 | id?: number 3 | title: string; 4 | description: string; 5 | }; -------------------------------------------------------------------------------- /avantage/types/IRegistration.ts: -------------------------------------------------------------------------------- 1 | export type IRegistrationErrors = { 2 | hasErrors?: string 3 | } 4 | 5 | export type RegistationResponse = { 6 | hasErrors: boolean, 7 | errors?: IRegistrationErrors 8 | } 9 | 10 | export type RegistationRequest = { 11 | name: string, 12 | username?: string 13 | email?: string 14 | password?: string 15 | } -------------------------------------------------------------------------------- /avantage/types/ISession.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | 3 | export interface IUserSession { 4 | authToken: string; 5 | user: User 6 | sessionId: number 7 | } 8 | -------------------------------------------------------------------------------- /avantage/types/ISubscription.ts: -------------------------------------------------------------------------------- 1 | export type ISubscription = { 2 | id?: number 3 | userId: number 4 | stripeId: string 5 | stripeStatus: string | null 6 | stripePriceId: string | null 7 | quantity: number | null 8 | trialEndsAt: number | null 9 | endsAt: number | null 10 | startDate: number | null 11 | lastEventDate: number | null 12 | } -------------------------------------------------------------------------------- /avantage/types/ITag.ts: -------------------------------------------------------------------------------- 1 | export interface ITag { 2 | title: string; 3 | link?: string; 4 | } 5 | -------------------------------------------------------------------------------- /avantage/types/IUser.ts: -------------------------------------------------------------------------------- 1 | import { ISubscription } from '~/types/ISubscription'; 2 | 3 | export interface IUser { 4 | id?: number 5 | username: string 6 | name?: string 7 | loginType?: string 8 | password?: string 9 | email?: string 10 | avatarUrl?: string 11 | subscription?: ISubscription | null 12 | stripeCustomerId?: string | null 13 | } 14 | 15 | export interface IUserSanitized { 16 | id: number 17 | username: string 18 | name: string 19 | email: string 20 | } 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /avantage/types/InputValidation.ts: -------------------------------------------------------------------------------- 1 | type InputValidation = { 2 | key: string 3 | isBlank: boolean 4 | lenghtMin8: boolean 5 | hasError: boolean 6 | value: string 7 | emailTaken?: boolean 8 | usernameTaken?: boolean 9 | errorMessage?: string 10 | } -------------------------------------------------------------------------------- /avantage/types/SubPostRes.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from '~/types/IUser'; 2 | export type SubPostRes = { 3 | url : string; 4 | user: IUser; 5 | shouldUpdateUser: boolean; 6 | }; -------------------------------------------------------------------------------- /avantage/types/TopicData.ts: -------------------------------------------------------------------------------- 1 | import { Series, Video } from ".prisma/client"; 2 | 3 | export type TopicData = { 4 | series: Series[] 5 | videos: Video[] 6 | } -------------------------------------------------------------------------------- /avantage/types/theme.ts: -------------------------------------------------------------------------------- 1 | type Theme = 'light' | 'dark'; -------------------------------------------------------------------------------- /avantage/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | export default defineConfig({ 3 | test: { 4 | environment: "jsdom", 5 | deps: { 6 | inline: [/@nuxt\/test-utils-edge/], 7 | }, 8 | }, 9 | }) -------------------------------------------------------------------------------- /bin/npm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose exec -u 1000 avantage npm "$@" 3 | -------------------------------------------------------------------------------- /bin/npx: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose exec -u 1000 avantage npx "$@" 3 | -------------------------------------------------------------------------------- /bin/prisma: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose exec -u 1000 avantage npx prisma "$@" 3 | -------------------------------------------------------------------------------- /config/avantage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.12.1 2 | 3 | ENV HOST='0.0.0.0' 4 | ENV PORT='3000' 5 | 6 | WORKDIR /var/www/html/avantage 7 | -------------------------------------------------------------------------------- /config/deployment/fullstackjack.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=package-maker Service 3 | After=network.service docker.service 4 | Requires=docker.service 5 | 6 | [Service] 7 | Restart=always 8 | RestartSec=10 9 | TimeoutSec=300 10 | WorkingDirectory=/var/www/html 11 | ExecStartPre=/usr/bin/env docker-compose pull 12 | ExecStart=/usr/bin/env docker-compose up 13 | ExecStop=/usr/bin/env docker-compose stop 14 | ExecStopPost=/usr/bin/env docker-compose down 15 | 16 | [Install] 17 | WantedBy=docker.service 18 | 19 | # etc/systemd/system/fullstackjack.service 20 | -------------------------------------------------------------------------------- /config/deployment/pre-release-int-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "int-server", 4 | "ip": "49.12.188.8", 5 | "username": "root", 6 | "port": "22", 7 | "beforeHooks": "", 8 | "afterHooks": "", 9 | "path": "/var/www/html" 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /config/deployment/release-prod-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "prod-server", 4 | "ip": "49.12.188.8", 5 | "username": "root", 6 | "port": "22", 7 | "beforeHooks": "", 8 | "afterHooks": "", 9 | "path": "/var/www/html" 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /config/nginx/auth/.htpasswd: -------------------------------------------------------------------------------- 1 | whodey:$apr1$oJEOxcEn$9g9YW262OVGTMEQsMCbEu0 2 | -------------------------------------------------------------------------------- /config/nginx/fastcgi_params: -------------------------------------------------------------------------------- 1 | fastcgi_param QUERY_STRING $query_string; 2 | fastcgi_param REQUEST_METHOD $request_method; 3 | fastcgi_param CONTENT_TYPE $content_type; 4 | fastcgi_param CONTENT_LENGTH $content_length; 5 | 6 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 7 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 8 | fastcgi_param PATH_INFO $fastcgi_path_info; 9 | fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; 10 | fastcgi_param REQUEST_URI $request_uri; 11 | fastcgi_param DOCUMENT_URI $document_uri; 12 | fastcgi_param DOCUMENT_ROOT $document_root; 13 | fastcgi_param SERVER_PROTOCOL $server_protocol; 14 | 15 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 16 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 17 | 18 | fastcgi_param REMOTE_ADDR $remote_addr; 19 | fastcgi_param REMOTE_PORT $remote_port; 20 | fastcgi_param SERVER_ADDR $server_addr; 21 | fastcgi_param SERVER_PORT $server_port; 22 | fastcgi_param SERVER_NAME $server_name; 23 | 24 | fastcgi_param HTTPS $https; 25 | 26 | # PHP only, required if PHP was built with --enable-force-cgi-redirect 27 | fastcgi_param REDIRECT_STATUS 200; 28 | -------------------------------------------------------------------------------- /config/nginx/includes/cors.conf: -------------------------------------------------------------------------------- 1 | add_header 'Access-Control-Allow-Origin' $http_origin always; 2 | add_header 'Access-Control-Allow-Credentials' 'true' always; 3 | add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PUT, OPTIONS' always; 4 | add_header 'Access-Control-Allow-Headers' 'Version,Accept,Accept-Encoding,Accept-Language,Connection,Coockie,Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type' always; 5 | 6 | if ($request_method = OPTIONS ) { 7 | add_header Content-Length 0; 8 | add_header Content-Type text/plain; 9 | 10 | add_header 'Access-Control-Allow-Origin' $http_origin always; 11 | add_header 'Access-Control-Allow-Credentials' 'true' always; 12 | add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PUT, OPTIONS' always; 13 | add_header 'Access-Control-Allow-Headers' 'Version,Accept,Accept-Encoding,Accept-Language,Connection,Coockie,Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type' always; 14 | 15 | return 200; 16 | } 17 | -------------------------------------------------------------------------------- /config/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | #daemon off; 2 | 3 | user www-data; 4 | worker_processes 1; 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | http { 11 | include /etc/nginx/mime.types; 12 | default_type application/octet-stream; 13 | 14 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 15 | '$status $body_bytes_sent "$http_referer" ' 16 | '"$http_user_agent" "$http_x_forwarded_for"'; 17 | 18 | access_log /var/log/nginx/access.log main; 19 | error_log /var/log/nginx/error.log warn; 20 | 21 | sendfile on; 22 | keepalive_timeout 65; 23 | 24 | include /etc/nginx/sites-enabled/*.conf; 25 | } 26 | -------------------------------------------------------------------------------- /config/nginx/sites-dev/avantage.conf: -------------------------------------------------------------------------------- 1 | map $sent_http_content_type $expires { 2 | "text/html" epoch; 3 | "text/html; charset=utf-8" epoch; 4 | default off; 5 | } 6 | 7 | server { 8 | listen 80; 9 | listen 443 ssl; 10 | 11 | server_name my.avantage.dev; 12 | 13 | ssl_certificate /etc/ssl/avantage.dev/fullchain.pem; 14 | ssl_certificate_key /etc/ssl/avantage.dev/privkey.pem; 15 | 16 | root /var/www/html/avantage; 17 | index index.html; 18 | 19 | charset utf-8; 20 | client_max_body_size 100M; 21 | 22 | gzip on; 23 | gzip_types text/plain application/xml text/css application/javascript; 24 | gzip_min_length 1000; 25 | 26 | #include includes/api.conf; 27 | 28 | location / { 29 | expires $expires; 30 | 31 | proxy_set_header Host $host; 32 | proxy_set_header X-Real-IP $remote_addr; 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 34 | proxy_set_header X-Forwarded-Proto $scheme; 35 | proxy_redirect off; 36 | proxy_read_timeout 1m; 37 | proxy_connect_timeout 1m; 38 | proxy_pass http://avantage:3000; # set the address of the Node.js 39 | } 40 | 41 | 42 | location /testing { 43 | fastcgi_pass unix:/does/not/exist; 44 | } 45 | 46 | access_log off; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /config/nginx/sites-prod/fullstackjack.conf: -------------------------------------------------------------------------------- 1 | map $sent_http_content_type $expires { 2 | "text/html" epoch; 3 | "text/html; charset=utf-8" epoch; 4 | default off; 5 | } 6 | 7 | server { 8 | listen 80; 9 | listen 443 ssl; 10 | 11 | server_name avantage.dev; 12 | 13 | ssl_certificate /etc/letsencrypt/live/avantage.dev/fullchain.pem; 14 | ssl_certificate_key /etc/letsencrypt/live/avantage.dev/privkey.pem; 15 | 16 | root /var/www/html/avantage; 17 | index index.html; 18 | 19 | location /.well-known/acme-challenge/ { 20 | root /var/www/html/current/avantage/.output/public; 21 | } 22 | 23 | charset utf-8; 24 | client_max_body_size 100M; 25 | 26 | gzip on; 27 | gzip_types text/plain application/xml text/css application/javascript; 28 | gzip_min_length 1000; 29 | 30 | location / { 31 | expires $expires; 32 | 33 | proxy_set_header Host $host; 34 | proxy_set_header X-Real-IP $remote_addr; 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 36 | proxy_set_header X-Forwarded-Proto $scheme; 37 | proxy_redirect off; 38 | proxy_read_timeout 1m; 39 | proxy_connect_timeout 1m; 40 | proxy_pass http://avantage:3000; # set the address of the Node.js 41 | } 42 | 43 | 44 | location /testing { 45 | fastcgi_pass unix:/does/not/exist; 46 | } 47 | 48 | access_log off; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /config/ssl/avantage.dev/create-certificate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | openssl req -x509 -nodes -newkey rsa:2048 \ 4 | -config ./openssl.cnf \ 5 | -keyout privkey.pem \ 6 | -out fullchain.pem \ 7 | -days 3600 \ 8 | -subj '/C=DE/ST=NRW/L=Bielefeld/O=fullstackjack. KG/OU=fsj /CN=my.avantage.dev/emailAddress=local@avantage.dev' 9 | -------------------------------------------------------------------------------- /config/ssl/avantage.dev/fullchain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE0TCCA7mgAwIBAgIJAImWjjnqZA0qMA0GCSqGSIb3DQEBCwUAMIGXMQswCQYD 3 | VQQGEwJERTEMMAoGA1UECBMDTlJXMRIwEAYDVQQHEwlCaWVsZWZlbGQxGjAYBgNV 4 | BAoTEWZ1bGxzdGFja2phY2suIEtHMQ0wCwYDVQQLEwRmc2ogMRgwFgYDVQQDEw9t 5 | eS5hdmFudGFnZS5kZXYxITAfBgkqhkiG9w0BCQEWEmxvY2FsQGF2YW50YWdlLmRl 6 | djAeFw0yMzAxMDIyMTIwNDlaFw0zMjExMTAyMTIwNDlaMIGXMQswCQYDVQQGEwJE 7 | RTEMMAoGA1UECBMDTlJXMRIwEAYDVQQHEwlCaWVsZWZlbGQxGjAYBgNVBAoTEWZ1 8 | bGxzdGFja2phY2suIEtHMQ0wCwYDVQQLEwRmc2ogMRgwFgYDVQQDEw9teS5hdmFu 9 | dGFnZS5kZXYxITAfBgkqhkiG9w0BCQEWEmxvY2FsQGF2YW50YWdlLmRldjCCASIw 10 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALnVZp2EI9Pd8lJMCOR3VHwHqdWL 11 | EZsSi9DRdgsqpVJ63+sa7If1k1+Gg6UahbAv22E4dgmV9aMkjlyu+SbjCrMxwLCG 12 | V7CTRzliSP5lG2OHVdIin4390ClVIjLeua5UW85y2/Y50NZ5D+9TF5baYjnYpqxP 13 | ADQm5WhMsV9ed/u7DYIoW/MmBjuncnG2dZ6B2C3TM9pWLnzwI0Heqz/G1t10kzs/ 14 | qdzlnCruZIXlXUUXfhB6x4pNdMNiVU2pBPJHUIXZPpr9w35OIcE6o5BDsE4Eezmp 15 | BD+j0yZEPDoODf+UU9i772S4KQKU6sjxpkcX5GRLLJSJibSOxXECPWm5IWsCAwEA 16 | AaOCARwwggEYMBoGA1UdEQQTMBGCD215LmF2YW50YWdlLmRldjAdBgNVHQ4EFgQU 17 | VLk3U7bVt4rHV3+e3sSG9+nSnjowgcwGA1UdIwSBxDCBwYAUVLk3U7bVt4rHV3+e 18 | 3sSG9+nSnjqhgZ2kgZowgZcxCzAJBgNVBAYTAkRFMQwwCgYDVQQIEwNOUlcxEjAQ 19 | BgNVBAcTCUJpZWxlZmVsZDEaMBgGA1UEChMRZnVsbHN0YWNramFjay4gS0cxDTAL 20 | BgNVBAsTBGZzaiAxGDAWBgNVBAMTD215LmF2YW50YWdlLmRldjEhMB8GCSqGSIb3 21 | DQEJARYSbG9jYWxAYXZhbnRhZ2UuZGV2ggkAiZaOOepkDSowDAYDVR0TBAUwAwEB 22 | /zANBgkqhkiG9w0BAQsFAAOCAQEAEuN5WCBeSIOkHyjCXrbK62aqkHr4xK1JPcuh 23 | BpAEIvkpwj77S8WEJjp6AZLw5gXxWXVQhNAX9JhGFBXdQiXSXBDJg84rPIqYpUD/ 24 | l6EZedcJaFB03k91TW0LUxz2eCGltFa5ttuxLEyIK1ntmneIpxQfzgsjae9pUNr0 25 | bDnBYv67D+4aXqtGu0gx/wJxhz18biSYfPOYXCpV4f4NrmC6RqpmIxsKFZzvGB0Q 26 | 8CzRp2zSRv7BJYl9OgAevnzu5Tz7NbcaC2XpikbD+7v7SkgLWs+IlDo8rpY3ex+c 27 | K+sxKIap5HKCNj68mqV3uJLoJ8oUKtb7BLBTteU+Qliw2g4t3w== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /config/ssl/avantage.dev/openssl.cnf: -------------------------------------------------------------------------------- 1 | # 2 | # OpenSSL example configuration file. 3 | # This is mostly being used for generation of certificate requests. 4 | # 5 | 6 | # This definition stops the following lines choking if HOME isn't 7 | # defined. 8 | HOME = . 9 | RANDFILE = $ENV::HOME/.rnd 10 | 11 | # Extra OBJECT IDENTIFIER info: 12 | #oid_file = $ENV::HOME/.oid 13 | oid_section = new_oids 14 | 15 | # To use this configuration file with the "-extfile" option of the 16 | # "openssl x509" utility, name here the section containing the 17 | # X.509v3 extensions to use: 18 | # extensions = 19 | # (Alternatively, use a configuration file that has only 20 | # X.509v3 extensions in its main [= default] section.) 21 | 22 | [ new_oids ] 23 | 24 | # We can add new OIDs in here for use by 'ca' and 'req'. 25 | # Add a simple OID like this: 26 | # testoid1=1.2.3.4 27 | # Or use config file substitution like this: 28 | # testoid2=${testoid1}.5.6 29 | 30 | #################################################################### 31 | [ ca ] 32 | default_ca = CA_default # The default ca section 33 | 34 | #################################################################### 35 | [ CA_default ] 36 | 37 | dir = ./demoCA # Where everything is kept 38 | certs = $dir/certs # Where the issued certs are kept 39 | crl_dir = $dir/crl # Where the issued crl are kept 40 | database = $dir/index.txt # database index file. 41 | #unique_subject = no # Set to 'no' to allow creation of 42 | # several ctificates with same subject. 43 | new_certs_dir = $dir/newcerts # default place for new certs. 44 | 45 | certificate = $dir/cacert.pem # The CA certificate 46 | serial = $dir/serial # The current serial number 47 | crlnumber = $dir/crlnumber # the current crl number 48 | # must be commented out to leave a V1 CRL 49 | crl = $dir/crl.pem # The current CRL 50 | private_key = $dir/private/cakey.pem# The private key 51 | RANDFILE = $dir/private/.rand # private random number file 52 | 53 | x509_extensions = usr_cert # The extentions to add to the cert 54 | 55 | # Comment out the following two lines for the "traditional" 56 | # (and highly broken) format. 57 | name_opt = ca_default # Subject Name options 58 | cert_opt = ca_default # Certificate field options 59 | 60 | # Extension copying option: use with caution. 61 | # copy_extensions = copy 62 | 63 | # Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs 64 | # so this is commented out by default to leave a V1 CRL. 65 | # crlnumber must also be commented out to leave a V1 CRL. 66 | # crl_extensions = crl_ext 67 | 68 | default_days = 3600 # how long to certify for 69 | default_crl_days= 30 # how long before next CRL 70 | default_md = sha1 # which md to use. 71 | preserve = no # keep passed DN ordering 72 | 73 | # A few difference way of specifying how similar the request should look 74 | # For type CA, the listed attributes must be the same, and the optional 75 | # and supplied fields are just that :-) 76 | policy = policy_match 77 | 78 | # For the CA policy 79 | [ policy_match ] 80 | countryName = match 81 | stateOrProvinceName = match 82 | organizationName = match 83 | organizationalUnitName = optional 84 | commonName = supplied 85 | emailAddress = optional 86 | 87 | # For the 'anything' policy 88 | # At this point in time, you must list all acceptable 'object' 89 | # types. 90 | [ policy_anything ] 91 | countryName = optional 92 | stateOrProvinceName = optional 93 | localityName = optional 94 | organizationName = optional 95 | organizationalUnitName = optional 96 | commonName = supplied 97 | emailAddress = optional 98 | 99 | #################################################################### 100 | [ req ] 101 | default_bits = 1024 102 | default_keyfile = privkey.pem 103 | distinguished_name = req_distinguished_name 104 | attributes = req_attributes 105 | x509_extensions = v3_ca # The extentions to add to the self signed cert 106 | 107 | # Passwords for private keys if not present they will be prompted for 108 | # input_password = secret 109 | # output_password = secret 110 | 111 | # This sets a mask for permitted string types. There are several options. 112 | # default: PrintableString, T61String, BMPString. 113 | # pkix : PrintableString, BMPString. 114 | # utf8only: only UTF8Strings. 115 | # nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). 116 | # MASK:XXXX a literal mask value. 117 | # WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings 118 | # so use this option with caution! 119 | string_mask = nombstr 120 | 121 | # req_extensions = v3_req # The extensions to add to a certificate request 122 | 123 | [ req_distinguished_name ] 124 | countryName = Country Name (2 letter code) 125 | countryName_default = AU 126 | countryName_min = 2 127 | countryName_max = 2 128 | 129 | stateOrProvinceName = State or Province Name (full name) 130 | stateOrProvinceName_default = Some-State 131 | 132 | localityName = Locality Name (eg, city) 133 | 134 | 0.organizationName = Organization Name (eg, company) 135 | 0.organizationName_default = Internet Widgits Pty Ltd 136 | 137 | # we can do this but it is not needed normally :-) 138 | #1.organizationName = Second Organization Name (eg, company) 139 | #1.organizationName_default = World Wide Web Pty Ltd 140 | 141 | organizationalUnitName = Organizational Unit Name (eg, section) 142 | #organizationalUnitName_default = 143 | 144 | commonName = Common Name (e.g. server FQDN or YOUR name) 145 | commonName_max = 64 146 | 147 | emailAddress = Email Address 148 | emailAddress_max = 64 149 | 150 | # SET-ex3 = SET extension number 3 151 | 152 | [ req_attributes ] 153 | challengePassword = A challenge password 154 | challengePassword_min = 4 155 | challengePassword_max = 20 156 | 157 | unstructuredName = An optional company name 158 | 159 | [ usr_cert ] 160 | 161 | # These extensions are added when 'ca' signs a request. 162 | 163 | # This goes against PKIX guidelines but some CAs do it and some software 164 | # requires this to avoid interpreting an end user certificate as a CA. 165 | 166 | basicConstraints=CA:FALSE 167 | 168 | # Here are some examples of the usage of nsCertType. If it is omitted 169 | # the certificate can be used for anything *except* object signing. 170 | 171 | # This is OK for an SSL server. 172 | # nsCertType = server 173 | 174 | # For an object signing certificate this would be used. 175 | # nsCertType = objsign 176 | 177 | # For normal client use this is typical 178 | # nsCertType = client, email 179 | 180 | # and for everything including object signing: 181 | # nsCertType = client, email, objsign 182 | 183 | # This is typical in keyUsage for a client certificate. 184 | # keyUsage = nonRepudiation, digitalSignature, keyEncipherment 185 | 186 | # This will be displayed in Netscape's comment listbox. 187 | nsComment = "OpenSSL Generated Certificate" 188 | 189 | # PKIX recommendations harmless if included in all certificates. 190 | subjectKeyIdentifier=hash 191 | authorityKeyIdentifier=keyid,issuer 192 | 193 | # This stuff is for subjectAltName and issuerAltname. 194 | # Import the email address. 195 | # subjectAltName=email:copy 196 | # An alternative to produce certificates that aren't 197 | # deprecated according to PKIX. 198 | # subjectAltName=email:move 199 | 200 | # Copy subject details 201 | # issuerAltName=issuer:copy 202 | 203 | #nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem 204 | #nsBaseUrl 205 | #nsRevocationUrl 206 | #nsRenewalUrl 207 | #nsCaPolicyUrl 208 | #nsSslServerName 209 | 210 | [ v3_req ] 211 | 212 | # Extensions to add to a certificate request 213 | 214 | basicConstraints = CA:FALSE 215 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 216 | 217 | [ v3_ca ] 218 | # Extensions for a typical CA 219 | subjectAltName = @alt_names 220 | 221 | # PKIX recommendation. 222 | 223 | subjectKeyIdentifier=hash 224 | 225 | authorityKeyIdentifier=keyid:always,issuer:always 226 | 227 | # This is what PKIX recommends but some broken software chokes on critical 228 | # extensions. 229 | #basicConstraints = critical,CA:true 230 | # So we do this instead. 231 | basicConstraints = CA:true 232 | 233 | # Key usage: this is typical for a CA certificate. However since it will 234 | # prevent it being used as an test self-signed certificate it is best 235 | # left out by default. 236 | # keyUsage = cRLSign, keyCertSign 237 | 238 | # Some might want this also 239 | # nsCertType = sslCA, emailCA 240 | 241 | # Include email address in subject alt name: another PKIX recommendation 242 | # subjectAltName=email:copy 243 | # Copy issuer details 244 | # issuerAltName=issuer:copy 245 | 246 | # DER hex encoding of an extension: beware experts only! 247 | # obj=DER:02:03 248 | # Where 'obj' is a standard or added object 249 | # You can even override a supported extension: 250 | # basicConstraints= critical, DER:30:03:01:01:FF 251 | 252 | [ crl_ext ] 253 | 254 | # CRL extensions. 255 | # Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. 256 | 257 | # issuerAltName=issuer:copy 258 | authorityKeyIdentifier=keyid:always,issuer:always 259 | 260 | [ proxy_cert_ext ] 261 | # These extensions should be added when creating a proxy certificate 262 | 263 | # This goes against PKIX guidelines but some CAs do it and some software 264 | # requires this to avoid interpreting an end user certificate as a CA. 265 | 266 | basicConstraints=CA:FALSE 267 | 268 | # Here are some examples of the usage of nsCertType. If it is omitted 269 | # the certificate can be used for anything *except* object signing. 270 | 271 | # This is OK for an SSL server. 272 | # nsCertType = server 273 | 274 | # For an object signing certificate this would be used. 275 | # nsCertType = objsign 276 | 277 | # For normal client use this is typical 278 | # nsCertType = client, email 279 | 280 | # and for everything including object signing: 281 | # nsCertType = client, email, objsign 282 | 283 | # This is typical in keyUsage for a client certificate. 284 | # keyUsage = nonRepudiation, digitalSignature, keyEncipherment 285 | 286 | # This will be displayed in Netscape's comment listbox. 287 | nsComment = "OpenSSL Generated Certificate" 288 | 289 | # PKIX recommendations harmless if included in all certificates. 290 | subjectKeyIdentifier=hash 291 | authorityKeyIdentifier=keyid,issuer:always 292 | 293 | # This stuff is for subjectAltName and issuerAltname. 294 | # Import the email address. 295 | # subjectAltName=email:copy 296 | # An alternative to produce certificates that aren't 297 | # deprecated according to PKIX. 298 | # subjectAltName=email:move 299 | 300 | # Copy subject details 301 | # issuerAltName=issuer:copy 302 | 303 | #nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem 304 | #nsBaseUrl 305 | #nsRevocationUrl 306 | #nsRenewalUrl 307 | #nsCaPolicyUrl 308 | #nsSslServerName 309 | 310 | # This really needs to be in place for it to be a proxy certificate. 311 | proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo 312 | 313 | [ alt_names ] 314 | DNS.1 = my.avantage.dev 315 | -------------------------------------------------------------------------------- /config/ssl/avantage.dev/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC51WadhCPT3fJS 3 | TAjkd1R8B6nVixGbEovQ0XYLKqVSet/rGuyH9ZNfhoOlGoWwL9thOHYJlfWjJI5c 4 | rvkm4wqzMcCwhlewk0c5Ykj+ZRtjh1XSIp+N/dApVSIy3rmuVFvOctv2OdDWeQ/v 5 | UxeW2mI52KasTwA0JuVoTLFfXnf7uw2CKFvzJgY7p3JxtnWegdgt0zPaVi588CNB 6 | 3qs/xtbddJM7P6nc5Zwq7mSF5V1FF34QeseKTXTDYlVNqQTyR1CF2T6a/cN+TiHB 7 | OqOQQ7BOBHs5qQQ/o9MmRDw6Dg3/lFPYu+9kuCkClOrI8aZHF+RkSyyUiYm0jsVx 8 | Aj1puSFrAgMBAAECggEAPS+AK9i4Gyf2gxY5z09i57t2NbMmRtKiakys+xw9dpyy 9 | YSxqOJGoxkj5y0CiR9JZ/vaiFqHjUKXWobmSmzUh7sIw2W5CLQcw6jnsIqaTj/+d 10 | SCTSN+Qbx6AaNHmU1Us9NwomVjnPAu61Sm0nVSnuMXGd3xnbzVAJNIAb3nSyOJVc 11 | M++Woyl+ujAJ7V9cv+JqwxLcwo9n4zAP7GKRLYftNVMgaOzy2ZZU2s9oRjr3WPOE 12 | 4Qg4tK7cS07RVEnDqY12lPOJUiiqJsij5qIr8GMQTG4yhetTrcTI/I4ebDpoKq01 13 | UdZHxH7Ej3m/MB76LZo2QKoqOfcOVj6n5h2YZxCioQKBgQDh0torsWab7QqvGcJa 14 | wDai4DO5NbKOA2CHFKO/wyqdtma3VwIBhRWOS8rWaXQZcQvubWIxub71SVUxyYF1 15 | EunLsUFA3ol+9zXabekiNV612Yorvkxn6udJuX684i0bqryXMbLKVhqYMWg+SyCT 16 | FxTZ5XPA+N0sJe9ue/iVXX2bSQKBgQDSqonhiuYwGzuAS6vB2a0+fe/3St/qAB9R 17 | DIb/IdycpwO69OfKAaqhTZbJ3LfoZ+IcShRphxmsjsa7B3/CU3nlU6eyHnljpDko 18 | RjhAhh1fETHJawPZdvhkraFfPD/mnueid96U5qzVe4+KOuDfyT3pXGhKynkiJpyD 19 | rNDqvS7DEwKBgQC2qKiUAvBuWzPzIjDU2vjWkecEfmyo9g5T9NvmtmR4IRvAXH5g 20 | 4FbpPGEbQT0JfykZeByfABF3shNZLBasrdmySvPvFpG8wxUqUxDp/KVZDlb2vvxq 21 | adUfCw16lq/J2zakTSzDARaN2BjrmjUFBPx2q2QPyLyNgznB8kDGAFMjEQKBgA32 22 | r9z7T/awV2lRmrjmrM0Pm/BQTjc8etbsdaZDoFPh3iVuRk7lfWHjurL2ploJSLuH 23 | TYMRKWp+rD2JabZ/wfypZtwvmOw53nAE94WPXjMG+L3ZEhBACobh22hsne+zaLck 24 | KuTDxYEBB6qp7G3o8Ome7mrGsPDKjmVL9y0YDRlxAoGBAN/Fkk+vQGd8cu9ZXv/r 25 | WG1BqwMhzxa3gXFxwLuWsQhG5Wp0AjpIr+Xup9SvavxE++YL9GPAejX3n+p8qDiB 26 | jkwF5ZewTvmh/Sl+pM1PLEGjleJaJPXs9SxzxH1UcqApRDm+K+6sOiRIZmJaIFz8 27 | JXWE4Xm6YI3VRWhfoRLtrGoF 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | nginx: 4 | volumes: 5 | - ./config/ssl:/etc/ssl:ro 6 | - ./config/nginx/sites-dev:/etc/nginx/sites-enabled 7 | 8 | avantage: 9 | command: sh -c "yarn && yarn dev" 10 | 11 | mysql: 12 | platform: linux/amd64 13 | image: mariadb:10.4.25 14 | 15 | mailhog: 16 | container_name: mailhog_avantage 17 | image: mailhog/mailhog 18 | restart: always 19 | ports: 20 | - "1025:1025" 21 | - "8025:8025" -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | nginx: 4 | volumes: 5 | - ./config/nginx/sites-prod:/etc/nginx/sites-enabled 6 | - /etc/letsencrypt:/etc/letsencrypt 7 | 8 | nuxt-app: 9 | command: sh -c "node .output/server/index.mjs" 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | nginx: 4 | image: nginx:1.15 5 | working_dir: /var/www/html 6 | ports: 7 | - "80:80" 8 | - "443:443" 9 | volumes: 10 | - ./avantage:/var/www/html/avantage 11 | - ./config/nginx/includes:/etc/nginx/includes:ro 12 | - ./config/nginx/auth:/etc/nginx/auth 13 | - ./config/nginx/nginx.conf:/etc/nginx/nginx.conf 14 | 15 | mysql: 16 | image: mysql:5.7 17 | ports: 18 | - "3306:3306" 19 | environment: 20 | - MYSQL_DATABASE=${DB_NAME} 21 | - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} 22 | - MYSQL_USER=${DB_USERNAME} 23 | - MYSQL_PASSWORD=${DB_PASSWORD} 24 | volumes: 25 | - mysqldata:/var/lib/mysql 26 | 27 | avantage: 28 | ports: 29 | - "24678:24678" 30 | - "9222:9222" 31 | - "5454:5555" 32 | user: node 33 | build: 34 | dockerfile: Dockerfile 35 | context: ./config/avantage 36 | working_dir: /var/www/html/avantage 37 | volumes: 38 | - ./avantage:/var/www/html/avantage 39 | command: sh -c "node .output/server/index.mjs" 40 | stdin_open: true 41 | tty: true 42 | networks: 43 | - default 44 | 45 | volumes: 46 | mysqldata: 47 | driver: local 48 | --------------------------------------------------------------------------------