├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── README.md ├── firebase.json ├── functions ├── README.md ├── modules │ ├── README.md │ ├── handlers-module │ │ ├── README.md │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ ├── api-handler.ts │ │ │ │ ├── auth-handler.ts │ │ │ │ ├── healthy-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── notification-handler.ts │ │ │ ├── handler-config.ts │ │ │ ├── index.ts │ │ │ ├── service │ │ │ │ ├── firebase-admin-service.ts │ │ │ │ ├── firestore-admin-collection-service.ts │ │ │ │ ├── global-service.ts │ │ │ │ └── request-handler-service.ts │ │ │ ├── sitemap-handler.ts │ │ │ └── types │ │ │ │ └── index.d.ts │ │ ├── tsconfig-cjs.json │ │ ├── tsconfig.json │ │ └── tslint.json │ ├── package.json │ └── types-module │ │ ├── README.md │ │ ├── package.json │ │ ├── src │ │ └── index.ts │ │ ├── tsconfig-cjs.json │ │ ├── tsconfig.json │ │ └── tslint.json ├── package.json ├── src │ ├── apiApp.ts │ ├── index.ts │ ├── nuxtOnFunction.ts │ ├── sitemapApp.ts │ ├── types │ │ └── index.d.ts │ └── warmUpScheduledFunction.ts ├── tsconfig.json └── tslint.json ├── package.json ├── sonar-project.properties └── src ├── .babelrc ├── .editorconfig ├── .env.template ├── .eslintrc.js ├── .prettierrc ├── README.md ├── assets ├── main.scss ├── style.css └── variables.scss ├── components ├── Loading.vue ├── card │ ├── ProfileCard.vue │ └── ProviderCard.vue ├── error │ ├── Error403.vue │ ├── Error404.vue │ └── Error500.vue ├── footer │ └── AppFooter.vue ├── form │ ├── ForgetPasswordForm.vue │ ├── LoginForm.vue │ ├── ProfilePrivacyForm.vue │ ├── ProfileUpdateForm.vue │ ├── RegisterForm.vue │ ├── SetEmailPasswordForm.vue │ ├── SetPasswordForm.vue │ └── SocialLogin.vue ├── image │ ├── BackgroundImage.vue │ ├── BackgroundSquareImage.vue │ ├── ProfilePhotoUpdater.vue │ ├── crop │ │ └── ImageCropper.vue │ ├── lightbox │ │ └── Lightbox.vue │ └── upload │ │ ├── SingleImageUpload.vue │ │ └── SingleValidatedImageUpload.vue ├── modal │ └── SetEmailPasswordModal.vue ├── navbar │ ├── LanguageSwitcher.vue │ ├── Logo.vue │ ├── ProfileNavigator.vue │ ├── SearchBar.vue │ ├── TopImage.vue │ ├── TopNavbar.vue │ ├── TopPushNotification.vue │ └── TopShare.vue ├── notification │ ├── FollowingNotification.vue │ └── TopMessage.vue ├── profile │ ├── Profile.vue │ ├── header │ │ ├── CoverPhoto.vue │ │ ├── ProfileFollow.vue │ │ ├── ProfileHeader.vue │ │ ├── ProfilePhoto.vue │ │ ├── ProfilePrivacy.vue │ │ └── ProfileShort.vue │ └── module │ │ ├── LinkedAccounts.vue │ │ ├── ProfileAboutMe.vue │ │ ├── ProfileFollowers.vue │ │ ├── ProfileFollowings.vue │ │ ├── ProfileModule.vue │ │ └── ProfileSettings.vue ├── search │ ├── SearchField.vue │ └── SearchResultCard.vue └── ui │ ├── FieldWithValue.vue │ ├── PageTitle.vue │ ├── RememberMe.vue │ ├── dropdown │ └── PrivacyDropdown.vue │ ├── input │ ├── InputNoValidation.vue │ └── InputWithValidation.vue │ ├── paging │ ├── Paging.vue │ ├── PagingConfig.vue │ └── PagingNavigator.vue │ └── social │ └── SocialShare.vue ├── i18n ├── en.ts └── tr.ts ├── layouts ├── README.md ├── default.vue └── error.vue ├── middleware ├── clear-messages.ts ├── router-auth.ts └── user-private.ts ├── mixin └── BaseModule.ts ├── now.json ├── nuxt.config.ts ├── package.json ├── pages ├── auth │ ├── action.vue │ ├── forget-password.vue │ └── reset-password.vue ├── crop.vue ├── images.vue ├── index.vue ├── lightbox.vue ├── login.vue ├── privacy-policy.vue ├── profile │ └── index.vue ├── register.vue ├── search.vue ├── terms.vue └── u │ └── _username │ ├── index.vue │ └── notification.vue ├── plugins ├── axios-plugin.ts ├── buefy-plugin.ts ├── fire-init-plugin.ts ├── firebase-auth-listener.ts ├── notification-plugin.ts ├── rxjs-plugin.ts ├── vee-validate-plugin.ts └── vue-lazyload-plugin.ts ├── server ├── api.ts ├── config │ ├── .gitkeep │ └── README.md └── sitemap.ts ├── service ├── api-service.ts ├── error-service.ts ├── firebase │ ├── firebase-service.ts │ └── firestore │ │ ├── collection-base-service.ts │ │ ├── following-collection.ts │ │ ├── index.ts │ │ ├── push-notification-collection.ts │ │ ├── user-collection.ts │ │ └── user-device-collection.ts ├── global-service.ts ├── notification-service.ts ├── rx-service.ts └── seo-service.ts ├── shim-ts.d.ts ├── shims-vue.d.ts ├── static ├── favicon.ico ├── icon.png ├── img │ ├── default-cover.jpg │ ├── default-profile.png │ └── flag │ │ ├── rectangular │ │ ├── tr.svg │ │ └── uk.svg │ │ ├── rounded │ │ ├── tr.svg │ │ └── uk.svg │ │ └── square │ │ ├── tr.svg │ │ └── uk.svg └── robots.txt ├── store ├── auth.ts ├── index.ts ├── loading.ts ├── notification.ts └── profile.ts ├── stylelint.config.js ├── tests ├── jest-mocks.ts └── pages │ └── index.spec.ts ├── tsconfig.json └── types ├── firebase-types.ts ├── index.ts ├── route-types.ts ├── rx-types.ts ├── seo-types.ts └── state-types.ts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | 4 | deploy_master: 5 | docker: 6 | - image: circleci/node:10.16.3 7 | steps: 8 | - checkout 9 | - run: 10 | name: Install dependencies 11 | command: npm run install 12 | - run: 13 | name: Add .env 14 | command: | 15 | echo "WEBSITE_URL=$WEBSITE_URL"> src/.env 16 | echo "API_URL=API_URL">> src/.env 17 | echo "FIREBASE_API_KEY=$FIREBASE_API_KEY" >> src/.env 18 | echo "FIREBASE_AUTH_DOMAIN=$FIREBASE_AUTH_DOMAIN" >> src/.env 19 | echo "FIREBASE_DATABASE_URL=$FIREBASE_DATABASE_URL" >> src/.env 20 | echo "FIREBASE_PROJECT_ID=$FIREBASE_PROJECT_ID" >> src/.env 21 | echo "FIREBASE_STORAGE_BUCKET=$FIREBASE_STORAGE_BUCKET" >> src/.env 22 | echo "FIREBASE_MESSAGING_SENDER_ID=$FIREBASE_MESSAGING_SENDER_ID" >> src/.env 23 | echo "FIREBASE_APP_ID=$FIREBASE_APP_ID" >> src/.env 24 | echo "FIREBASE_MEASUREMENT_ID=$FIREBASE_MEASUREMENT_ID" >> src/.env 25 | echo "FIREBASE_MESSAGING_VAP_ID=FIREBASE_MESSAGING_VAP_ID" >> src/.env 26 | - run: 27 | name: Add firebase-messaging-sw.js 28 | command: | 29 | echo "importScripts('https://www.gstatic.com/firebasejs/7.15.0/firebase-app.js')" > src/static/firebase-messaging-sw.js 30 | echo "importScripts('https://www.gstatic.com/firebasejs/7.15.0/firebase-messaging.js')" >> src/static/firebase-messaging-sw.js 31 | echo "" >> src/static/firebase-messaging-sw.js 32 | echo "firebase.initializeApp({" >> src/static/firebase-messaging-sw.js 33 | echo " apiKey: '$FIREBASE_API_KEY'," >> src/static/firebase-messaging-sw.js 34 | echo " projectId: '$FIREBASE_PROJECT_ID'," >> src/static/firebase-messaging-sw.js 35 | echo " messagingSenderId: '$FIREBASE_MESSAGING_SENDER_ID'," >> src/static/firebase-messaging-sw.js 36 | echo " appId: '$FIREBASE_APP_ID'" >> src/static/firebase-messaging-sw.js 37 | echo "})" >> src/static/firebase-messaging-sw.js 38 | echo "" >> src/static/firebase-messaging-sw.js 39 | echo "const messaging = firebase.messaging()" >> src/static/firebase-messaging-sw.js 40 | echo "" >> src/static/firebase-messaging-sw.js 41 | echo "messaging.setBackgroundMessageHandler((payload) => {" >> src/static/firebase-messaging-sw.js 42 | echo " console.log('firebase-messaging-sw.js setBackgroundMessageHandler', payload)" >> src/static/firebase-messaging-sw.js 43 | echo "})" >> src/static/firebase-messaging-sw.js 44 | echo "" >> src/static/firebase-messaging-sw.js 45 | - run: 46 | name: Deploy to Firebase Hosting&Functions 47 | command: ./src/node_modules/.bin/firebase deploy --project=$FIREBASE_PROJECT_ID --token=$FIREBASE_CI_TOKEN 48 | - run: 49 | name: Call warming up 50 | command: | 51 | curl "$WEBSITE_URL" 52 | curl "$WEBSITE_URL/api/healthy" 53 | curl "$WEBSITE_URL/sitemap.xml" 54 | 55 | orbs: 56 | sonarcloud: sonarsource/sonarcloud@1.0.1 57 | 58 | workflows: 59 | version: 2 60 | build-and-deploy: 61 | jobs: 62 | - deploy_master: 63 | filters: 64 | branches: 65 | only: master 66 | context: SonarCloud 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by .ignore support plugin (hsz.mobi) 3 | ### Node template 4 | # Logs 5 | /logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | #.env 62 | #.env.development 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # Nuxt generate 74 | dist 75 | 76 | # vuepress build output 77 | .vuepress/dist 78 | 79 | # Serverless directories 80 | .serverless 81 | 82 | # IDE / Editor 83 | .idea 84 | 85 | # Service worker 86 | sw.* 87 | 88 | # macOS 89 | .DS_Store 90 | 91 | # Vim swap files 92 | *.swp 93 | yarn.lock 94 | 95 | .firebase 96 | firebase-debug.log 97 | 98 | # dotenv environment variables file 99 | .env 100 | 101 | **/*.log 102 | node_modules 103 | 104 | # modules 105 | lib 106 | **/*.tgz 107 | 108 | # firebase 109 | public 110 | firebase-admin-credentials.json 111 | firebase-messaging-sw.js 112 | 113 | package-lock.json 114 | 115 | tmp 116 | 117 | .vercel 118 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "source": "functions", 4 | "predeploy": [ 5 | "npm run build" 6 | ] 7 | }, 8 | "hosting": { 9 | "predeploy": [ 10 | "npm run hosting" 11 | ], 12 | "public": "public", 13 | "ignore": [ 14 | "firebase.json", 15 | "**/.*", 16 | "**/node_modules/**" 17 | ], 18 | "rewrites": [ 19 | { 20 | "source": "/api/**", 21 | "function": "apiApp" 22 | }, 23 | { 24 | "source": "/sitemap.xml", 25 | "function": "sitemapApp" 26 | }, 27 | { 28 | "source": "**", 29 | "function": "nuxtOnFunction" 30 | } 31 | ] 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /functions/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [functions of nuxt-typescript-ssr-firebase-auth](#functions-of-nuxt-typescript-ssr-firebase-auth) 6 | - [build](#build) 7 | - [serve](#serve) 8 | - [deploy](#deploy) 9 | - [deploy all](#deploy-all) 10 | - [deploy nuxtOnFunction](#deploy-nuxtonfunction) 11 | - [deploy authApi](#deploy-authapi) 12 | - [deploy sitemapApp](#deploy-sitemapapp) 13 | 14 | 15 | 16 | # functions of nuxt-typescript-ssr-firebase-auth 17 | 18 | ## build 19 | npm run build 20 | 21 | ## serve 22 | npm run serve 23 | 24 | ## deploy 25 | 26 | ### deploy all 27 | firebase deploy 28 | 29 | > `firebase deploy --only functions` may not work properly. `.nuxt` is mandatory to run `nuxtOnFunction` function. 30 | 31 | ### deploy nuxtOnFunction 32 | firebase deploy --only functions:nuxtOnFunction 33 | firebase deploy --only hosting 34 | 35 | ### deploy authApi 36 | firebase deploy --only functions:apiApp 37 | 38 | ### deploy sitemapApp 39 | firebase deploy --only functions:sitemapApp 40 | 41 | ## install modules 42 | go to [modules](./modules) 43 | -------------------------------------------------------------------------------- /functions/modules/README.md: -------------------------------------------------------------------------------- 1 | # modules 2 | The code has nuxt frontend application and firebase functions as backend. Additionally, nuxt application has `serverMiddleware`. That `serverMiddleware` has same functionality as `firebase-functions`. That means there are some shared types and functions. 3 | 4 | The `modules` has been created to keep consistency, clean code and reduce the maintenance. 5 | 6 | The modules: 7 | - [types-module](./types-module): has all shared types (enums, interfaces, constants) 8 | - [handlers-module](./handlers-module): has all express handlers. Also, has all backend services like firebase-admin and firestore 9 | 10 | ## npm install on all modules 11 | npm run install 12 | 13 | ## build all modules 14 | npm run build 15 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/README.md: -------------------------------------------------------------------------------- 1 | # handlers-module 2 | 3 | ## build 4 | npm i 5 | npm run build 6 | 7 | ## pack (to create handlers-module-.tgz) 8 | npm pack 9 | 10 | ## install 11 | npm i 12 | 13 | # or 14 | npm i /handlers-module-1.0.0.tgz 15 | 16 | `on function` 17 | 18 | npm i /handlers-module-1.0.0.tgz 19 | 20 | # or 21 | npm i ../modules/handlers-module 22 | 23 | `on src (nuxt)` 24 | 25 | npm i ../functions/modules/handlers-module 26 | 27 | # or 28 | npm i ../functions/modules/handlers-module/handlers-module-1.0.0.tgz 29 | 30 | ## remove local modules 31 | npm remove handlers-module 32 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handlers-module", 3 | "version": "1.0.0", 4 | "description": "Common types between backend and frontend", 5 | "main": "./lib/cjs/index.js", 6 | "typings": "./lib/cjs/index.d.ts", 7 | "scripts": { 8 | "prebuild": "npm run lint && rm -rf ./lib", 9 | "build": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json", 10 | "lint": "tslint --project tsconfig.json", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [], 14 | "author": "resul avan ", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@types/pluralize": "0.0.29", 18 | "prettier": "^2.2.0", 19 | "tslint": "^6.1.2", 20 | "tslint-config-prettier": "^1.18.0", 21 | "typescript": "^4.1.2" 22 | }, 23 | "dependencies": { 24 | "@types/express": "^4.17.9", 25 | "@types/uuid": "^8.3.0", 26 | "express": "^4.17.1", 27 | "firebase": "^8.1.1", 28 | "firebase-admin": "^9.4.1", 29 | "http-status-codes": "^2.1.4", 30 | "sitemap": "^6.3.3", 31 | "types-module": "file:../types-module", 32 | "uuid": "^8.3.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/api/api-handler.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, RequestHandler, Response } from 'express' 2 | import admin from 'firebase-admin' 3 | import { 4 | extractHeadersFromRequest, 5 | getTokenFromRequest, 6 | handleApiErrors, 7 | handlerCalledLog 8 | } from '../service/request-handler-service' 9 | import { getDecodedIdToken } from '../service/firebase-admin-service' 10 | import { ApiErrorCode } from 'types-module' 11 | import DecodedIdToken = admin.auth.DecodedIdToken 12 | 13 | export const extractHeaderHandler: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { 14 | await extractHeadersFromRequest(req) 15 | .then(() => { 16 | handlerCalledLog(req, 'extractHeaderHandler') 17 | next() 18 | }) 19 | .catch((error: Error) => handleApiErrors(req, res, error)) 20 | } 21 | 22 | export const tokenHandler: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { 23 | handlerCalledLog(req, 'tokenHandler') 24 | await getTokenFromRequest(req) 25 | .then(async (token: string) => { 26 | if (!token) { 27 | throw new Error(ApiErrorCode.FORBIDDEN) 28 | } 29 | 30 | await getDecodedIdToken(token) 31 | .then((decodedIdToken: DecodedIdToken) => { 32 | req.user = decodedIdToken 33 | }) 34 | 35 | next() 36 | }) 37 | .catch((error: Error) => handleApiErrors(req, res, error)) 38 | } 39 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/api/auth-handler.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express' 2 | import { NO_CONTENT, OK } from 'http-status-codes' 3 | import { setCustomClaims, toAuthUser, validateClaimsAndGet } from '../service/firebase-admin-service' 4 | import { ApiErrorCode, FirebaseClaimKey, FirebaseClaims } from 'types-module' 5 | import { 6 | getDecodedIdTokenFromRequest, 7 | handleApiErrors, 8 | handlerCalledLog, 9 | handlerLog 10 | } from '../service/request-handler-service' 11 | import admin from 'firebase-admin' 12 | import DecodedIdToken = admin.auth.DecodedIdToken 13 | 14 | export const verifyHandler: RequestHandler = async (req, res) => { 15 | handlerCalledLog(req, 'verifyHandler') 16 | await getDecodedIdTokenFromRequest(req) 17 | .then(async (decodedIdToken: DecodedIdToken) => { 18 | const claims = await validateClaimsAndGet(decodedIdToken) 19 | handlerLog(req, `current claims of: ${decodedIdToken.uid}`, JSON.stringify(claims)) 20 | 21 | const authUser = toAuthUser(decodedIdToken, claims) 22 | handlerLog(req, 'returning authUser: ', JSON.stringify(authUser)) 23 | 24 | return res.status(OK).json(authUser) 25 | }) 26 | .catch((error: Error) => handleApiErrors(req, res, error)) 27 | } 28 | 29 | export const claimsHandler: RequestHandler = async (req, res) => { 30 | 31 | handlerCalledLog(req, 'claimsHandler') 32 | await getDecodedIdTokenFromRequest(req) 33 | .then(async (decodedIdToken: DecodedIdToken) => { 34 | if (!req.body.claims) { 35 | throw new Error(ApiErrorCode.BAD_REQUEST) 36 | } 37 | 38 | const firebaseClaims = req.body.claims as FirebaseClaims 39 | handlerLog(req, 'claims from body: ', JSON.stringify(firebaseClaims)) 40 | if (!firebaseClaims[FirebaseClaimKey.USERNAME]) { 41 | throw new Error(ApiErrorCode.BAD_REQUEST) 42 | } 43 | 44 | await setCustomClaims(decodedIdToken.sub, firebaseClaims) 45 | 46 | return res.status(NO_CONTENT).send() 47 | }) 48 | .catch((error: Error) => handleApiErrors(req, res, error)) 49 | } 50 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/api/healthy-handler.ts: -------------------------------------------------------------------------------- 1 | import { Request, RequestHandler, Response } from 'express' 2 | import { OK } from 'http-status-codes' 3 | import { ProviderType } from 'types-module' 4 | 5 | export const healthyHandler: RequestHandler = (req: Request, res: Response) => { 6 | console.log(`${req.originalUrl} - healthyHandler called`, ProviderType.PASSWORD) 7 | return res.status(OK).send('OK') 8 | } 9 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './healthy-handler' 2 | export * from './api-handler' 3 | export * from './auth-handler' 4 | export * from './notification-handler' 5 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/api/notification-handler.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express' 2 | import { NO_CONTENT } from 'http-status-codes' 3 | import admin from 'firebase-admin' 4 | import { 5 | getDecodedIdTokenFromRequest, 6 | handleApiErrors, 7 | handlerCalledLog, 8 | handlerLog 9 | } from '../service/request-handler-service' 10 | import { ApiErrorCode, UserDevice } from 'types-module' 11 | import { deleteUserDevice, getPushNotification, getUserDevices } from '../service/firebase-admin-service' 12 | import DecodedIdToken = admin.auth.DecodedIdToken 13 | 14 | export const notifyHandler: RequestHandler = async (req, res) => { 15 | handlerCalledLog(req, 'notifyHandler') 16 | await getDecodedIdTokenFromRequest(req) 17 | .then(async (decodedIdToken: DecodedIdToken) => { 18 | const notificationId = req.params.notificationId 19 | handlerLog(req, `notify request from ${decodedIdToken.uid} with notificationId ${notificationId}`) 20 | if (!notificationId) { 21 | throw new Error(ApiErrorCode.BAD_REQUEST) 22 | } 23 | 24 | const notification = await getPushNotification(notificationId) 25 | if (!notification) { 26 | throw new Error(ApiErrorCode.BAD_REQUEST) 27 | } 28 | handlerLog(req, `notification goes from ${notification.from} to ${notification.to}`) 29 | 30 | const userDevices: UserDevice[] = await getUserDevices(notification.to) 31 | if (userDevices.length <= 0) { 32 | handlerLog(req, `No userDevice for ${notification.to}`) 33 | return res.status(NO_CONTENT).send() 34 | } 35 | 36 | const deviceTokens = userDevices.map(value => value.deviceToken) 37 | handlerLog(req, `${userDevices.length} userDevice(s) found for ${notification.to}`) 38 | const payload = { 39 | data: { 40 | type: notification.notificationType 41 | } 42 | } 43 | const removeUserDevicePromises: Promise[] = [] 44 | 45 | const fcmResult = await admin.messaging().sendToDevice(deviceTokens, payload) 46 | fcmResult.results.forEach((result, index) => { 47 | const error = result.error 48 | if (error) { 49 | handlerLog(req, 'Failure sending notification to:', JSON.stringify(userDevices[index]), error) 50 | // Cleanup the tokens who are not registered anymore. 51 | if (error.code === 'messaging/invalid-registration-token' || 52 | error.code === 'messaging/registration-token-not-registered') { 53 | removeUserDevicePromises.push(deleteUserDevice(userDevices[index])) 54 | } 55 | } 56 | }) 57 | 58 | await Promise.all(removeUserDevicePromises) 59 | handlerLog(req, `notification sent from ${notification.from} to ${notification.to}`) 60 | 61 | return res.status(NO_CONTENT).send() 62 | }) 63 | .catch((error: Error) => handleApiErrors(req, res, error)) 64 | } 65 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/handler-config.ts: -------------------------------------------------------------------------------- 1 | import admin from 'firebase-admin' 2 | 3 | export class HandlerConfig { 4 | 5 | private static credentials = '' 6 | private static websiteUrl = 'https://nuxt-ts-firebase-auth-ssr.web.app/' 7 | 8 | private static readonly defaultInitializer = () => { 9 | admin.initializeApp({ 10 | credential: admin.credential.applicationDefault() 11 | }) 12 | console.log('firebase admin is initialized by default credentials') 13 | return admin 14 | } 15 | 16 | private static readonly credentialsInitializer = () => { 17 | const serviceAccount = HandlerConfig.credentials 18 | // const serviceAccount = require(HandlerConfig.credentials) 19 | admin.initializeApp({ 20 | credential: admin.credential.cert(serviceAccount) 21 | }) 22 | console.log('firebase admin is initialized by custom credentials') 23 | } 24 | 25 | static setCredentials (credentials: string) { 26 | HandlerConfig.credentials = credentials 27 | } 28 | 29 | static setWebSiteUrl (websiteUrl: string) { 30 | HandlerConfig.websiteUrl = websiteUrl 31 | } 32 | 33 | static getWebsiteUrl () { 34 | return HandlerConfig.websiteUrl 35 | } 36 | 37 | static getAdmin () { 38 | if (admin.apps.length === 0) { 39 | this.credentials ? this.credentialsInitializer() : this.defaultInitializer() 40 | } 41 | return admin; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './handler-config' 2 | export * from './api' 3 | export * from './sitemap-handler' 4 | export * from './service/request-handler-service' 5 | export * from './service/global-service' 6 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/service/firebase-admin-service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthUser, 3 | collection, 4 | CollectionField, 5 | FirebaseClaimKey, 6 | FirebaseClaims, 7 | FirebaseQueryOperator, 8 | ProviderType, 9 | PushNotification, 10 | User, 11 | UserDevice, 12 | WhereClause 13 | } from 'types-module' 14 | import { deleteModel, getModelById, getModelsByWhereClauses } from './firestore-admin-collection-service' 15 | import { HandlerConfig } from '../handler-config' 16 | import admin from 'firebase-admin' 17 | import DecodedIdToken = admin.auth.DecodedIdToken 18 | 19 | export const getDecodedIdToken = (idToken: string): Promise => { 20 | return HandlerConfig.getAdmin().auth() 21 | .verifyIdToken(idToken) 22 | .then((decodedIdToken: DecodedIdToken) => decodedIdToken) 23 | } 24 | 25 | export const setCustomClaims = async (uid: string, firebaseClaims: FirebaseClaims): Promise => { 26 | await HandlerConfig.getAdmin().auth().setCustomUserClaims(uid, firebaseClaims); 27 | } 28 | 29 | export const validateClaimsAndGet = async (decodedIdToken: DecodedIdToken) => { 30 | let username = decodedIdToken[FirebaseClaimKey.USERNAME] 31 | if (username) { 32 | return { username } 33 | } 34 | 35 | const user = await getUser(decodedIdToken.uid) 36 | if (!user) { 37 | throw new Error('User not found by id: ' + decodedIdToken.uid) 38 | } 39 | 40 | username = user.username || user.id 41 | const firebaseClaims = { username } 42 | 43 | await setCustomClaims(decodedIdToken.sub, firebaseClaims) 44 | 45 | return firebaseClaims 46 | } 47 | 48 | export const toAuthUser = (decodedIdToken: DecodedIdToken, firebaseClaims: FirebaseClaims): AuthUser => { 49 | return { 50 | name: decodedIdToken.name, 51 | verified: decodedIdToken.email_verified as boolean, 52 | email: decodedIdToken.email as string, 53 | profilePhoto: { 54 | src: decodedIdToken.picture as string, 55 | alt: `Profile photo of ${firebaseClaims[FirebaseClaimKey.USERNAME]}` 56 | }, 57 | userId: decodedIdToken.sub, 58 | username: firebaseClaims[FirebaseClaimKey.USERNAME], 59 | providers: [{ providerType: decodedIdToken.firebase.sign_in_provider as ProviderType }] 60 | }; 61 | } 62 | 63 | export const getUser = async (userId: string): Promise => { 64 | return await getModelById(collection.USER, userId) 65 | } 66 | 67 | export const getPushNotification = async (notificationId: string): Promise => { 68 | return await getModelById(collection.NOTIFICATION, notificationId) 69 | } 70 | 71 | export const getUserDevices = async (userId: string): Promise => { 72 | const userWhereClause: WhereClause = { 73 | field: CollectionField.USER_DEVICE.userId, 74 | operator: FirebaseQueryOperator.EQ, 75 | value: userId 76 | } 77 | 78 | return await getModelsByWhereClauses(collection.USER_DEVICE, userWhereClause) 79 | } 80 | 81 | export const deleteUserDevice = (userDevice: UserDevice) => { 82 | return deleteModel(collection.USER_DEVICE, userDevice) 83 | } 84 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/service/firestore-admin-collection-service.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, WhereClause } from 'types-module' 2 | import admin from 'firebase-admin' 3 | import { HandlerConfig } from '../handler-config' 4 | 5 | const toBaseModelArray = (querySnapshot: admin.firestore.QuerySnapshot): T[] => { 6 | const docs: T[] = [] 7 | querySnapshot.forEach(function (doc) { 8 | docs.push(doc.data() as T) 9 | }) 10 | 11 | return docs 12 | } 13 | 14 | const getQueryByWhereClauses = ( 15 | collection: string, 16 | whereClause: WhereClause, 17 | ...whereClauses: WhereClause[]) => { 18 | const collectionReference = HandlerConfig.getAdmin().firestore().collection(collection) 19 | 20 | let query = collectionReference.where(whereClause.field, whereClause.operator, whereClause.value) 21 | whereClauses.forEach((wc) => { 22 | query = query.where(wc.field, wc.operator, wc.value) 23 | }) 24 | 25 | return query 26 | } 27 | 28 | export async function getModelById (collection: string, modelId: string) { 29 | return await HandlerConfig.getAdmin().firestore() 30 | .collection(collection) 31 | .doc(modelId) 32 | .get() 33 | .then((document) => { 34 | return document.data() as T 35 | }) 36 | } 37 | 38 | export async function getModelsByWhereClauses ( 39 | collection: string, 40 | whereClause: WhereClause, 41 | ...whereClauses: WhereClause[] 42 | ): Promise { 43 | const query = getQueryByWhereClauses(collection, whereClause, ...whereClauses) 44 | 45 | return await query 46 | .get() 47 | .then(querySnapshot => toBaseModelArray(querySnapshot)) 48 | } 49 | 50 | export const deleteModel = (collection: string, model: T) => { 51 | return HandlerConfig.getAdmin().firestore().collection(collection).doc(model.id as string).delete() 52 | } 53 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/service/global-service.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | 3 | export const dashAllRegex = new RegExp('-', 'g') 4 | 5 | export const generateUuid = () => { 6 | return uuidv4().replace(dashAllRegex, '') 7 | } 8 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/service/request-handler-service.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | import { BAD_REQUEST, FORBIDDEN, INTERNAL_SERVER_ERROR, UNAUTHORIZED } from 'http-status-codes' 3 | import { ApiErrorCode, AppCookie, AppHeader, AuthHeaderValuePrefix } from 'types-module'; 4 | import admin, { FirebaseError } from 'firebase-admin'; 5 | import { generateUuid } from './global-service' 6 | import DecodedIdToken = admin.auth.DecodedIdToken 7 | 8 | const isFirebaseError = (error: any) => { 9 | return !!error.code 10 | } 11 | const sessinIdsLog = (req: Request) => { 12 | return `nsSessionId: ${req.nsSessionId}, nsRequestId: ${req.nsRequestId}` 13 | } 14 | 15 | const contextLog = (req: Request) => { 16 | return `context: ${req.originalUrl}` 17 | } 18 | 19 | export const handlerCalledLog = (req: Request, handler: string) => { 20 | console.log(handler, ` called - ${sessinIdsLog(req)},${contextLog(req)}`) 21 | } 22 | export const handlerLog = (req: Request, ...anyMessages: any[]) => { 23 | console.log(...anyMessages, ` - ${sessinIdsLog(req)}`) 24 | } 25 | 26 | export const handlerError = (req: Request, ...anyMessages: any[]) => { 27 | console.error(`${sessinIdsLog(req)},${contextLog(req)} - `, ...anyMessages) 28 | } 29 | 30 | export const extractHeadersFromRequest = (req: Request) => { 31 | return Promise.resolve().then(() => { 32 | req.nsSessionId = req.get(AppHeader.SESSION_ID) || generateUuid() 33 | req.nsRequestId = req.get(AppHeader.REQUEST_ID) || generateUuid() 34 | }) 35 | } 36 | 37 | export const getTokenFromRequest = (req: Request) => { 38 | return Promise.resolve().then(() => { 39 | const token = req.headers.authorization && req.headers.authorization.startsWith(AuthHeaderValuePrefix) 40 | ? req.headers.authorization.split(AuthHeaderValuePrefix)[1] : null 41 | 42 | return token || req.cookies[AppCookie.TOKEN] as string 43 | }) 44 | } 45 | 46 | export const getDecodedIdTokenFromRequest = (req: Request) => { 47 | return Promise.resolve().then(() => { 48 | if (!req.user) { 49 | throw new Error(ApiErrorCode.FORBIDDEN) 50 | } 51 | 52 | return req.user as DecodedIdToken 53 | }) 54 | } 55 | 56 | const handleFirebaseError = (request: Request, response: Response, error: FirebaseError) => { 57 | handlerError(request, 'Firebase error', error) 58 | switch (error?.code) { 59 | case 'auth/id-token-expired': 60 | response.status(UNAUTHORIZED).send('re-authentication required') 61 | break 62 | 63 | default: 64 | response.status(INTERNAL_SERVER_ERROR).send(ApiErrorCode.INTERNAL_ERROR) 65 | } 66 | } 67 | 68 | export const handleGenericError = (request: Request, response: Response, error: Error) => { 69 | handlerError(request, 'Error occurred', error) 70 | switch (error.message) { 71 | case ApiErrorCode.FORBIDDEN: 72 | response.status(FORBIDDEN).send(error.message) 73 | break 74 | 75 | case ApiErrorCode.BAD_REQUEST: 76 | response.status(BAD_REQUEST).send(error.message) 77 | break 78 | 79 | default: 80 | response.status(INTERNAL_SERVER_ERROR).send(ApiErrorCode.INTERNAL_ERROR) 81 | } 82 | } 83 | 84 | export const handleApiErrors = (request: Request, response: Response, error: Error|FirebaseError) => { 85 | isFirebaseError(error) 86 | ? handleFirebaseError(request, response, error as FirebaseError) 87 | : handleGenericError(request, response, error as Error) 88 | } 89 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/sitemap-handler.ts: -------------------------------------------------------------------------------- 1 | import { Request, RequestHandler, Response } from 'express' 2 | import { SitemapStream, streamToPromise } from 'sitemap' 3 | import { createGzip } from 'zlib' 4 | import { handleGenericError } from './service/request-handler-service' 5 | import { HandlerConfig } from './handler-config' 6 | 7 | const staticRoutes = [ 8 | '/', 9 | '/terms', 10 | '/privacy-policy', 11 | '/register', 12 | '/login', 13 | '/crop', 14 | '/lightbox', 15 | '/images' 16 | ] 17 | 18 | let sitemapBuffer: Buffer|null = null 19 | 20 | export const sitemapHandler: RequestHandler = async (req: Request, res: Response) => { 21 | console.log(`${req.originalUrl} - called (function)`) 22 | res.header('Content-Type', 'application/xml') 23 | res.header('Content-Encoding', 'gzip') 24 | if (sitemapBuffer) { 25 | res.send(sitemapBuffer) 26 | return 27 | } 28 | 29 | await Promise.resolve() 30 | .then(() => { 31 | const smStream = new SitemapStream({ hostname: HandlerConfig.getWebsiteUrl() }) 32 | const pipeline = smStream.pipe(createGzip()) 33 | 34 | staticRoutes.forEach((route) => 35 | smStream.write({ url: route, changefreq: 'weekly', priority: 0.8 }) 36 | ) 37 | smStream.end() 38 | 39 | streamToPromise(pipeline) 40 | .then((sm: Buffer) => (sitemapBuffer = sm)) 41 | .catch((error: Error) => console.log(error)) 42 | // stream write the response 43 | pipeline.pipe(res).on('error', (e: Error) => { 44 | throw e 45 | }) 46 | }) 47 | .catch((error) => handleGenericError(req, res, error)) 48 | } 49 | 50 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | user: any; 4 | nsSessionId: string; 5 | nsRequestId: string; 6 | } 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/tsconfig-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./lib/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /functions/modules/handlers-module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "ES2015", 5 | "module": "ES2020", 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "moduleResolution": "Node", 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "types": [ 15 | "./src/types", 16 | "types-module" 17 | ] 18 | }, 19 | "include": [ 20 | "./src/**/*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /functions/modules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm --prefix types-module run build && npm --prefix handlers-module run build", 4 | "install": "npm --prefix types-module install && npm --prefix handlers-module install", 5 | "re-install": "npm run clear-all && npm run install", 6 | "clear": "rm -rf */lib", 7 | "clear-all": "rm -rf */node_modules */lib */package-lock.json */*.tgz" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /functions/modules/types-module/README.md: -------------------------------------------------------------------------------- 1 | # types-module 2 | 3 | ## build 4 | npm i 5 | npm run build 6 | 7 | ## pack ( to create types-module-.tgz) 8 | npm pack 9 | 10 | ## install 11 | npm i 12 | 13 | # or 14 | npm i /types-module-1.0.0.tgz 15 | 16 | `on function` 17 | 18 | npm i ../modules/types-module/types-module-1.0.0.tgz 19 | 20 | # or 21 | npm i ../modules/types-module 22 | 23 | `on src (nuxt)` 24 | 25 | npm i ../functions/modules/types-module 26 | 27 | # or 28 | npm i ../functions/modules/types-module/types-module-1.0.0.tgz 29 | 30 | ## remove local modules 31 | npm remove types-module 32 | -------------------------------------------------------------------------------- /functions/modules/types-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "types-module", 3 | "version": "1.0.0", 4 | "description": "Common types between backend and frontend", 5 | "main": "./lib/cjs/index.js", 6 | "typings": "./lib/cjs/index.d.ts", 7 | "scripts": { 8 | "prebuild": "npm run lint && rm -rf ./lib", 9 | "build": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json", 10 | "lint": "tslint --project tsconfig.json", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [], 14 | "author": "resul avan ", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@types/pluralize": "0.0.29", 18 | "prettier": "^2.2.0", 19 | "tslint": "^6.1.2", 20 | "tslint-config-prettier": "^1.18.0", 21 | "typescript": "^4.1.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /functions/modules/types-module/tsconfig-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./lib/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /functions/modules/types-module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "ES2015", 5 | "module": "ES2020", 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "strict": false, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "isolatedModules": false, 13 | "allowJs": true 14 | }, 15 | "include": [ 16 | "./src" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "prebuild": "npm run lint && rm -rf lib .nuxt && cp -r ../src/.nuxt .", 5 | "lint": "tslint --project tsconfig.json", 6 | "build": "tsc", 7 | "serve": "npm run build && firebase emulators:start", 8 | "shell": "npm run build && firebase functions:shell", 9 | "start": "npm run shell", 10 | "deploy": "firebase deploy --only functions", 11 | "logs": "firebase functions:log" 12 | }, 13 | "engines": { 14 | "node": "10" 15 | }, 16 | "main": "lib/index.js", 17 | "dependencies": { 18 | "@nuxt/typescript-runtime": "^2.0.0", 19 | "@nuxtjs/axios": "^5.12.2", 20 | "@types/cookie-parser": "^1.4.2", 21 | "@types/cors": "^2.8.8", 22 | "@types/debounce": "^1.2.0", 23 | "@types/slug": "^0.9.1", 24 | "@types/uuid": "^8.3.0", 25 | "body-parser": "^1.19.0", 26 | "buefy": "^0.9.4", 27 | "bulma-helpers": "^0.3.12", 28 | "cookie-parser": "^1.4.5", 29 | "cookie-universal-nuxt": "^2.1.4", 30 | "cors": "^2.8.5", 31 | "cropperjs": "^1.5.9", 32 | "debounce": "^1.2.0", 33 | "dotenv": "^8.2.0", 34 | "express": "^4.17.1", 35 | "firebase": "^8.1.1", 36 | "firebase-admin": "^9.4.1", 37 | "firebase-functions": "^3.11.0", 38 | "handlers-module": "file:modules/handlers-module", 39 | "http-status-codes": "^2.1.4", 40 | "jwt-decode": "^3.1.2", 41 | "moment": "^2.29.1", 42 | "nuxt": "^2.14.7", 43 | "nuxt-i18n": "^6.15.4", 44 | "nuxt-property-decorator": "^2.8.8", 45 | "rxjs": "^6.6.3", 46 | "sitemap": "^6.3.3", 47 | "slug": "^4.0.2", 48 | "sync-request": "^6.1.0", 49 | "types-module": "file:modules/types-module", 50 | "uuid": "^8.3.1", 51 | "vee-validate": "^3.4.5", 52 | "vue-infinite-loading": "^2.4.5", 53 | "vue-lazyload": "^1.3.3", 54 | "vue-rx": "^6.2.0", 55 | "vuex-class": "^0.3.2", 56 | "zlib": "^1.0.5" 57 | }, 58 | "devDependencies": { 59 | "@nuxt/typescript-build": "^2.0.3", 60 | "@nuxtjs/stylelint-module": "^4.0.0", 61 | "firebase-functions-test": "^0.2.3", 62 | "stylelint": "^13.8.0", 63 | "tslint": "^6.1.2", 64 | "typescript": "^4.1.2" 65 | }, 66 | "private": true 67 | } 68 | -------------------------------------------------------------------------------- /functions/src/apiApp.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from 'express' 2 | import cookieParser from 'cookie-parser' 3 | import { json } from 'body-parser' 4 | import cors from 'cors' 5 | import { RuntimeOptions, runWith } from 'firebase-functions' 6 | import { ApiConfig } from 'types-module' 7 | import { 8 | claimsHandler, 9 | extractHeaderHandler, 10 | healthyHandler, 11 | notifyHandler, 12 | tokenHandler, 13 | verifyHandler 14 | } from 'handlers-module' 15 | 16 | const router = Router() 17 | router.get(ApiConfig.healthy, healthyHandler) 18 | router.get(ApiConfig.auth.verify, extractHeaderHandler, tokenHandler, verifyHandler) 19 | router.post(ApiConfig.auth.claims, extractHeaderHandler, tokenHandler, claimsHandler) 20 | router.post(ApiConfig.notification.notify.context, extractHeaderHandler, tokenHandler, notifyHandler) 21 | 22 | const app = express() 23 | app.disable('x-powered-by') 24 | app.use(cookieParser()) 25 | app.use(json()) 26 | app.use(cors({ origin: true })) 27 | app.use('/api', router) 28 | 29 | const runtimeOpts: RuntimeOptions = { 30 | timeoutSeconds: 300, 31 | memory: '1GB' 32 | } 33 | 34 | export const apiApp = runWith(runtimeOpts) 35 | .https 36 | .onRequest(app) 37 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | export { nuxtOnFunction } from './nuxtOnFunction' 2 | export { apiApp } from './apiApp' 3 | export { sitemapApp } from './sitemapApp' 4 | export { warmUpScheduledFunction } from './warmUpScheduledFunction' 5 | -------------------------------------------------------------------------------- /functions/src/nuxtOnFunction.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeOptions, runWith } from "firebase-functions"; 2 | import cookieParser from 'cookie-parser' 3 | import express, { Request, Response } from 'express' 4 | 5 | const { Nuxt } = require('nuxt'); 6 | 7 | const config = { 8 | // Don't start in dev mode. 9 | dev: false, 10 | // Set the path to the .nuxt folder. 11 | buildDir: '.nuxt', 12 | // // Enable debug when in the develop environment. 13 | // debug: process.env.GCP_PROJECT === 'nuxt2-example-dev', 14 | // Path to the assets. 15 | build: { 16 | publicPath: '/assets/', 17 | }, 18 | }; 19 | const nuxt = new Nuxt(config); 20 | 21 | let isReady = false 22 | const readyPromise = nuxt 23 | .ready() 24 | .then(() => { 25 | isReady = true 26 | }) 27 | .catch(() => { 28 | process.exit(1) 29 | }) 30 | 31 | const handleRequest = async (req: Request, res: Response) => { 32 | if (!isReady) { 33 | console.log('Creating nuxtOnFunction function.') 34 | await readyPromise 35 | } 36 | res.set('Cache-Control', 'public, max-age=31536000, s-maxage=1000') 37 | await nuxt.render(req, res) 38 | } 39 | 40 | // Init express. 41 | const app = express(); 42 | app.disable('x-powered-by') 43 | app.use(cookieParser()) 44 | // Give nuxt middleware to express. 45 | app.get('*', handleRequest) 46 | app.use(handleRequest) 47 | 48 | const runtimeOpts: RuntimeOptions = { 49 | timeoutSeconds: 300, 50 | memory: '2GB' 51 | } 52 | 53 | export const nuxtOnFunction = runWith(runtimeOpts) 54 | .https 55 | .onRequest(app); 56 | -------------------------------------------------------------------------------- /functions/src/sitemapApp.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from 'express' 2 | import { RuntimeOptions, runWith } from "firebase-functions" 3 | import { sitemapHandler } from 'handlers-module' 4 | 5 | const app = express() 6 | app.disable('x-powered-by') 7 | 8 | const router = Router() 9 | 10 | router.get('/sitemap.xml', sitemapHandler) 11 | app.use(router) 12 | 13 | const runtimeOpts: RuntimeOptions = { 14 | timeoutSeconds: 300 15 | } 16 | 17 | export const sitemapApp = runWith(runtimeOpts) 18 | .https 19 | .onRequest(app); 20 | -------------------------------------------------------------------------------- /functions/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | user: any; 4 | nsSessionId: string; 5 | nsRequestId: string; 6 | } 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /functions/src/warmUpScheduledFunction.ts: -------------------------------------------------------------------------------- 1 | import { pubsub } from "firebase-functions" 2 | import syncRequest from 'sync-request' 3 | import { HandlerConfig } from 'handlers-module' 4 | import { ApiConfig } from 'types-module' 5 | 6 | export const warmUpScheduledFunction = pubsub 7 | .schedule('*/15 * * * *') 8 | .onRun((context) => { 9 | syncRequest('GET', HandlerConfig.getWebsiteUrl()) 10 | syncRequest('GET', `${HandlerConfig.getWebsiteUrl()}${ApiConfig.healthy}`) 11 | syncRequest('GET', `${HandlerConfig.getWebsiteUrl()}/sitemap.xml`) 12 | }) 13 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2018", 10 | "allowSyntheticDefaultImports": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "types": [ 14 | "./src/types", 15 | "types-module", 16 | "handlers-module" 17 | ] 18 | }, 19 | "compileOnSave": true, 20 | "include": [ 21 | "src/**/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "install": "npm --prefix functions/modules install && npm --prefix functions install && npm --prefix src install", 4 | "build": "npm --prefix functions/modules run build && npm --prefix src run build && npm --prefix functions run build && npm run hosting", 5 | "hosting": "rm -rf public && mkdir -p public/assets && cp -r src/.nuxt/dist/* public/assets/. && cp -r src/static/* public/.", 6 | "clear": "rm -rf public functions/lib functions/.nuxt src/.nuxt", 7 | "clear-all": "npm run clear && rm -rf functions/node_modules src/node_modules */package-lock.json */*.log && npm --prefix functions/modules run clear-all" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=rslvn_nuxt-typescript-ssr-firebase-auth 2 | sonar.organization=rslvn 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=nuxt-typescript-ssr-firebase-auth 6 | #sonar.projectVersion=1.0 7 | 8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 9 | #sonar.sources=. 10 | 11 | # Encoding of the source code. Default is default system encoding 12 | sonar.sourceEncoding=UTF-8 13 | -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/.env.template: -------------------------------------------------------------------------------- 1 | WEBSITE_URL=https://nuxt-ts-firebase-auth-ssr.firebaseapp.com 2 | 3 | # axios config 4 | #API_URL=http://localhost:3000/api 5 | API_URL=https://nuxt-ts-firebase-auth-ssr.firebaseapp.com/api 6 | 7 | # firebase config 8 | FIREBASE_API_KEY= 9 | FIREBASE_AUTH_DOMAIN= 10 | FIREBASE_DATABASE_URL= 11 | FIREBASE_PROJECT_ID= 12 | FIREBASE_STORAGE_BUCKET= 13 | FIREBASE_MESSAGING_SENDER_ID= 14 | FIREBASE_APP_ID= 15 | FIREBASE_MEASUREMENT_ID= 16 | 17 | # messaging publicVapId 18 | FIREBASE_MESSAGING_VAP_ID= 19 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | browser: true 7 | }, 8 | parserOptions: { 9 | parser: '@typescript-eslint/parser', 10 | sourceType: 'module', 11 | ecmaFeatures: { 12 | legacyDecorators: true 13 | }, 14 | ecmaVersion: 6 15 | }, 16 | extends: [ 17 | '@nuxtjs/eslint-config-typescript' 18 | 19 | ], 20 | rules: { 21 | semi: [2, 'never'], 22 | 'no-console': 'off', 23 | 'vue/max-attributes-per-line': 'off', 24 | 'vue/script-indent': ['error', 2, { baseIndent: 0 }], 25 | indent: ['error', 2, { SwitchCase: 1 }] 26 | // 'prettier/prettier': ['error', { semi: false }] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "arrowParens": "always", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Nuxt Source](#nuxt-source) 6 | - [dev](#dev) 7 | - [start](#start) 8 | 9 | 10 | 11 | # Nuxt Source 12 | 13 | ## modules 14 | 15 | ### install modules 16 | 17 | npm i ../functions/modules/types-module ../functions/modules/handlers-module 18 | 19 | ### remove modules 20 | 21 | npm remove types-module handlers-module 22 | 23 | ## install 24 | npm i 25 | 26 | ## dev 27 | ```shell script 28 | npm run dev 29 | ``` 30 | 31 | ## start 32 | ```shell script 33 | npm run build 34 | npm run start 35 | ``` 36 | -------------------------------------------------------------------------------- /src/assets/main.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | // Import buefy and bulma styles 3 | @import "~bulma"; 4 | @import "~buefy/src/scss/buefy"; 5 | // Import your other app styles 6 | //@import "./style.css"; 7 | -------------------------------------------------------------------------------- /src/assets/variables.scss: -------------------------------------------------------------------------------- 1 | @import "~bulma/sass/utilities/_all"; 2 | 3 | $primary: #8c67ef; 4 | $primary-invert: findColorInvert($primary); 5 | 6 | $twitter: #55acee; 7 | $twitter-invert: findColorInvert($twitter); 8 | 9 | $facebook: #3b5999; 10 | $facebook-invert: findColorInvert($facebook); 11 | 12 | $whatsapp: #25D366; 13 | $whatsapp-invert: findColorInvert($whatsapp); 14 | 15 | $pinterest: #bd081c; 16 | $pinterest-invert: findColorInvert($pinterest); 17 | 18 | $colors: ( 19 | "white": ($white, $black), 20 | "black": ($black, $white), 21 | "light": ($light, $light-invert), 22 | "dark": ($dark, $dark-invert), 23 | "primary": ($primary, $primary-invert), 24 | "info": ($info, $info-invert), 25 | "success": ($success, $success-invert), 26 | "warning": ($warning, $warning-invert), 27 | "danger": ($danger, $danger-invert), 28 | "twitter": ($twitter, $twitter-invert), 29 | "facebook": ($facebook, $facebook-invert), 30 | "whatsapp": ($whatsapp, $whatsapp-invert), 31 | "pinterest": ($pinterest, $pinterest-invert) 32 | ); 33 | -------------------------------------------------------------------------------- /src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | 40 | 111 | -------------------------------------------------------------------------------- /src/components/card/ProfileCard.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 76 | -------------------------------------------------------------------------------- /src/components/error/Error403.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/components/error/Error404.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/components/error/Error500.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/components/footer/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /src/components/form/ForgetPasswordForm.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 52 | -------------------------------------------------------------------------------- /src/components/form/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 98 | -------------------------------------------------------------------------------- /src/components/form/ProfilePrivacyForm.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 89 | -------------------------------------------------------------------------------- /src/components/form/RegisterForm.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 102 | -------------------------------------------------------------------------------- /src/components/form/SetEmailPasswordForm.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 90 | -------------------------------------------------------------------------------- /src/components/form/SetPasswordForm.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 67 | -------------------------------------------------------------------------------- /src/components/form/SocialLogin.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 77 | -------------------------------------------------------------------------------- /src/components/image/BackgroundSquareImage.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 76 | 77 | 96 | -------------------------------------------------------------------------------- /src/components/image/ProfilePhotoUpdater.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 78 | -------------------------------------------------------------------------------- /src/components/image/crop/ImageCropper.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 63 | -------------------------------------------------------------------------------- /src/components/image/lightbox/Lightbox.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 73 | -------------------------------------------------------------------------------- /src/components/image/upload/SingleImageUpload.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 76 | -------------------------------------------------------------------------------- /src/components/modal/SetEmailPasswordModal.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 91 | -------------------------------------------------------------------------------- /src/components/navbar/LanguageSwitcher.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 44 | -------------------------------------------------------------------------------- /src/components/navbar/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /src/components/navbar/ProfileNavigator.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 51 | -------------------------------------------------------------------------------- /src/components/navbar/TopImage.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 47 | -------------------------------------------------------------------------------- /src/components/navbar/TopNavbar.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 93 | -------------------------------------------------------------------------------- /src/components/navbar/TopShare.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | -------------------------------------------------------------------------------- /src/components/notification/FollowingNotification.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 57 | -------------------------------------------------------------------------------- /src/components/notification/TopMessage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 34 | -------------------------------------------------------------------------------- /src/components/profile/Profile.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | -------------------------------------------------------------------------------- /src/components/profile/header/CoverPhoto.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 79 | -------------------------------------------------------------------------------- /src/components/profile/header/ProfileHeader.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | -------------------------------------------------------------------------------- /src/components/profile/header/ProfilePhoto.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 75 | -------------------------------------------------------------------------------- /src/components/profile/header/ProfilePrivacy.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | -------------------------------------------------------------------------------- /src/components/profile/header/ProfileShort.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 37 | -------------------------------------------------------------------------------- /src/components/profile/module/ProfileFollowers.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 109 | -------------------------------------------------------------------------------- /src/components/profile/module/ProfileFollowings.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 110 | -------------------------------------------------------------------------------- /src/components/profile/module/ProfileSettings.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 71 | -------------------------------------------------------------------------------- /src/components/search/SearchField.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /src/components/search/SearchResultCard.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 49 | -------------------------------------------------------------------------------- /src/components/ui/FieldWithValue.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /src/components/ui/PageTitle.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | -------------------------------------------------------------------------------- /src/components/ui/RememberMe.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | -------------------------------------------------------------------------------- /src/components/ui/input/InputNoValidation.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 41 | -------------------------------------------------------------------------------- /src/components/ui/input/InputWithValidation.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 54 | -------------------------------------------------------------------------------- /src/components/ui/paging/Paging.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 49 | -------------------------------------------------------------------------------- /src/components/ui/paging/PagingConfig.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | -------------------------------------------------------------------------------- /src/components/ui/paging/PagingNavigator.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 40 | -------------------------------------------------------------------------------- /src/components/ui/social/SocialShare.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 66 | -------------------------------------------------------------------------------- /src/layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Application Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /src/layouts/error.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | -------------------------------------------------------------------------------- /src/middleware/clear-messages.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from '@nuxt/types' 2 | import { StoreConfig } from '~/types' 3 | 4 | const clearMessageMiddleware: Middleware = async ({ store }) => { 5 | if (store.state.notification.notificationMessage) { 6 | await store.dispatch(StoreConfig.notification.clearNotificationMessage) 7 | console.log('Messages cleared') 8 | } 9 | } 10 | 11 | export default clearMessageMiddleware 12 | -------------------------------------------------------------------------------- /src/middleware/router-auth.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from '@nuxt/types' 2 | import { AuthUser } from 'types-module' 3 | import { QueryParameters, Routes } from '~/types' 4 | import { authenticatedAllowed, authenticatedNotAllowed, getUserRoute } from '~/service/global-service' 5 | 6 | const routerAuthMiddleware: Middleware = ({ store, redirect, route }) => { 7 | console.log('routerAuthMiddleware fullPath', route.fullPath) 8 | const authUser = store.state.auth?.authUser as AuthUser 9 | if (authUser) { 10 | if (authenticatedNotAllowed(route)) { 11 | redirect(getUserRoute(Routes.PROFILE_DYNAMIC, authUser.username)) 12 | } 13 | console.log('let it go') 14 | } else if (authenticatedAllowed(route)) { 15 | redirect({ 16 | ...Routes.LOGIN, 17 | query: { 18 | [QueryParameters.NEXT]: route.fullPath 19 | } 20 | }) 21 | } 22 | } 23 | 24 | export default routerAuthMiddleware 25 | -------------------------------------------------------------------------------- /src/middleware/user-private.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from '@nuxt/types' 2 | import { AuthUser } from 'types-module' 3 | import { RouteParameters, Routes } from '~/types' 4 | 5 | const userPrivateMiddleware: Middleware = ({ store, redirect, route }) => { 6 | const authUser = store.state.auth?.authUser as AuthUser 7 | const usernameParam = route.params[RouteParameters.USERNAME] 8 | 9 | // The user not logged in or tries to go to the other user's private page 10 | if (!authUser || !usernameParam || (authUser.username !== usernameParam)) { 11 | console.log('The page is private') 12 | redirect(Routes.HOME) 13 | } 14 | } 15 | 16 | export default userPrivateMiddleware 17 | -------------------------------------------------------------------------------- /src/mixin/BaseModule.ts: -------------------------------------------------------------------------------- 1 | import { Component, Prop, Vue } from 'nuxt-property-decorator' 2 | import { AuthUser, User } from 'types-module' 3 | 4 | @Component({ 5 | components: {} 6 | }) 7 | export default class BaseModule extends Vue { 8 | @Prop({ type: Boolean, required: true }) isMyProfile: boolean 9 | @Prop({ required: true }) authUser: AuthUser 10 | @Prop({ type: Object, required: true }) user: User 11 | } 12 | -------------------------------------------------------------------------------- /src/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "nuxt.config.ts", 6 | "use": "@nuxtjs/now-builder", 7 | "config": {} 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/auth/action.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 96 | -------------------------------------------------------------------------------- /src/pages/auth/forget-password.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /src/pages/auth/reset-password.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 55 | -------------------------------------------------------------------------------- /src/pages/crop.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 35 | -------------------------------------------------------------------------------- /src/pages/login.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 52 | -------------------------------------------------------------------------------- /src/pages/privacy-policy.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /src/pages/profile/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 58 | -------------------------------------------------------------------------------- /src/pages/register.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 59 | -------------------------------------------------------------------------------- /src/pages/terms.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /src/pages/u/_username/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 95 | -------------------------------------------------------------------------------- /src/plugins/axios-plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '@nuxt/types' 2 | import { configureAxiosObservable } from '~/service/rx-service' 3 | import { configureAxiosDefaults } from '~/service/api-service' 4 | 5 | const axiosPlugin: Plugin = ({ app }) => { 6 | configureAxiosObservable.asObservable().subscribe(() => { 7 | configureAxiosDefaults(app.$axios) 8 | }) 9 | configureAxiosDefaults(app.$axios) 10 | } 11 | 12 | export default axiosPlugin 13 | -------------------------------------------------------------------------------- /src/plugins/buefy-plugin.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Buefy from 'buefy' 3 | 4 | import '@fortawesome/fontawesome-free/css/all.min.css' 5 | import '@mdi/font/css/materialdesignicons.min.css' 6 | import 'bulma-helpers/css/bulma-helpers.min.css' 7 | import 'bulma-badge/dist/css/bulma-badge.min.css' 8 | import 'buefy/dist/buefy.css' 9 | 10 | Vue.use(Buefy) 11 | -------------------------------------------------------------------------------- /src/plugins/fire-init-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as firebase from 'firebase/app' 2 | import 'firebase/auth' 3 | import 'firebase/firestore' 4 | import 'firebase/storage' 5 | import { ProviderType } from 'types-module' 6 | 7 | if (!firebase.apps.length) { 8 | const config = { 9 | apiKey: process.env.FIREBASE_API_KEY, 10 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 11 | databaseURL: process.env.FIREBASE_DATABASE_URL, 12 | projectId: process.env.FIREBASE_PROJECT_ID, 13 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET, 14 | messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, 15 | appId: process.env.FIREBASE_APP_ID, 16 | measurementId: process.env.FIREBASE_MEASUREMENT_ID 17 | } 18 | 19 | firebase.initializeApp(config) 20 | } 21 | 22 | export const getAuthProvider = (providerType: ProviderType) => { 23 | switch (providerType) { 24 | case ProviderType.GOOGLE: 25 | return new firebase.auth.GoogleAuthProvider() 26 | 27 | case ProviderType.TWITTER: 28 | return new firebase.auth.TwitterAuthProvider() 29 | 30 | case ProviderType.FACEBOOK: 31 | return new firebase.auth.FacebookAuthProvider() 32 | 33 | default: 34 | throw new Error( 35 | `No social auth provider for provider type ${providerType}` 36 | ) 37 | } 38 | } 39 | 40 | export const auth: firebase.auth.Auth = firebase.auth() 41 | export const firestore: firebase.firestore.Firestore = firebase.firestore() 42 | export const storage: firebase.storage.Storage = firebase.storage() 43 | 44 | export const TaskEvent = firebase.storage.TaskEvent 45 | export const TaskState = firebase.storage.TaskState 46 | export const FieldValue = firebase.firestore.FieldValue 47 | 48 | export default firebase 49 | -------------------------------------------------------------------------------- /src/plugins/notification-plugin.ts: -------------------------------------------------------------------------------- 1 | import { NuxtAppOptions, Plugin } from '@nuxt/types' 2 | import { PushNotification, PushNotificationStatus } from 'types-module' 3 | import firebase, { auth } from './fire-init-plugin' 4 | import { configureFcmObservable, loadNotificationObservable, sendNotificationObservable } from '~/service/rx-service' 5 | import { LocalStorageKey } from '~/types' 6 | import { getAlreadyExistPushNotification, savePushNotification } from '~/service/firebase/firestore' 7 | import 'firebase/messaging' 8 | import { saveUserDevice } from '~/service/firebase/firestore/user-device-collection' 9 | import { notificationNotify } from '~/service/api-service' 10 | 11 | let messagingLoaded = false 12 | 13 | const configureFcm = () => { 14 | if (!firebase.messaging.isSupported()) { 15 | console.log('Push Notification is not supported') 16 | return 17 | } 18 | const messaging = firebase.messaging() 19 | if (!messagingLoaded) { 20 | messaging.usePublicVapidKey(process.env.FIREBASE_MESSAGING_VAP_ID) 21 | messaging.onTokenRefresh((token) => { 22 | console.log('messaging.onTokenRefresh token', token) 23 | }, (error) => { 24 | console.log('messaging.onTokenRefresh', error) 25 | }) 26 | messagingLoaded = true 27 | } 28 | 29 | Notification.requestPermission().then((permission) => { 30 | console.log('permission: ', permission, auth.currentUser.uid) 31 | messaging.getToken() 32 | .then((token) => { 33 | const user = auth.currentUser 34 | if (token && user) { 35 | saveUserDevice({ 36 | userId: user.uid, 37 | deviceToken: token 38 | }) 39 | .catch(() => { 40 | }) 41 | 42 | messaging.onMessage((payload) => { 43 | console.log('FCM payload', payload) 44 | loadNotificationObservable.next() 45 | }) 46 | 47 | localStorage.setItem(LocalStorageKey.FCM_TOKEN, token) 48 | } 49 | }) 50 | }).catch((error: Error) => console.log(error)) 51 | 52 | console.log('configureFcm done') 53 | } 54 | 55 | const sendNotification = (app: NuxtAppOptions, notification: PushNotification) => { 56 | getAlreadyExistPushNotification(notification) 57 | .then((existNotifications) => { 58 | console.log('existNotifications', existNotifications) 59 | existNotifications 60 | .forEach((existNotification) => { 61 | existNotification.status = PushNotificationStatus.IGNORED 62 | savePushNotification(existNotification) 63 | .catch((error: Error) => console.log(error)) 64 | }) 65 | }) 66 | .then(() => { 67 | savePushNotification(notification) 68 | .then((saveNotification) => { 69 | notificationNotify(app.$axios, saveNotification.id) 70 | .catch((error: Error) => console.log(error)) 71 | }) 72 | }) 73 | .catch((error: Error) => console.log(error)) 74 | } 75 | 76 | const notificationPlugin: Plugin = ({ app }) => { 77 | sendNotificationObservable 78 | .asObservable() 79 | .subscribe((notification: PushNotification) => { 80 | console.log('sendNotificationObservable called') 81 | sendNotification(app, notification) 82 | }) 83 | configureFcmObservable.asObservable().subscribe(() => { 84 | configureFcm() 85 | }) 86 | } 87 | 88 | export default notificationPlugin 89 | -------------------------------------------------------------------------------- /src/plugins/rxjs-plugin.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRx from 'vue-rx' 3 | 4 | Vue.use(VueRx) 5 | -------------------------------------------------------------------------------- /src/plugins/vee-validate-plugin.ts: -------------------------------------------------------------------------------- 1 | import { NuxtAppOptions, Plugin } from '@nuxt/types' 2 | import { configure, extend } from 'vee-validate' 3 | import * as rules from 'vee-validate/dist/rules' 4 | import { QueryParameters, SupportedLanguages } from '~/types' 5 | import { getUserByUsername } from '~/service/firebase/firestore' 6 | 7 | for (const [rule, validation] of Object.entries(rules)) { 8 | // @ts-ignore 9 | extend(rule, { 10 | ...validation 11 | }) 12 | } 13 | 14 | const setLanguageFromQuery = (langQuery: string, app: NuxtAppOptions) => { 15 | if (!langQuery) { 16 | return 17 | } 18 | 19 | const supportedLanguage = SupportedLanguages.find( 20 | language => language.code === langQuery 21 | ) 22 | 23 | if (supportedLanguage) { 24 | app.i18n.setLocale(supportedLanguage.code) 25 | } 26 | } 27 | 28 | const veeValidatePlugin: Plugin = ({ app, query }) => { 29 | // beforeLanguageSwitch called right before setting a new locale 30 | // app.i18n.beforeLanguageSwitch = (oldLocale, newLocale) => { 31 | // console.log('beforeLanguageSwitch', oldLocale, newLocale) 32 | // }; 33 | // // onLanguageSwitched called right after a new locale has been set 34 | // app.i18n.onLanguageSwitched = (oldLocale, newLocale) => { 35 | // console.log('onLanguageSwitched', oldLocale, newLocale) 36 | // }; 37 | 38 | const langQuery = query[QueryParameters.LANG] + '' 39 | setLanguageFromQuery(langQuery, app) 40 | 41 | // CUSTOM RULES 42 | extend('username', { 43 | message: field => app.i18n.t('validation.username', { field }) + '', 44 | validate: (value, params: any) => getUserByUsername(value) 45 | .then((user) => { 46 | return user ? user.id === params[0] : true 47 | }) 48 | }) 49 | 50 | configure({ 51 | // @ts-ignore 52 | defaultMessage: (field: string, values) => { 53 | values._field_ = app.i18n.t(`${field}`) 54 | return app.i18n.t(`validation.${values._rule_}`, values) 55 | } 56 | }) 57 | } 58 | 59 | export default veeValidatePlugin 60 | -------------------------------------------------------------------------------- /src/plugins/vue-lazyload-plugin.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueLazyload from 'vue-lazyload' 3 | 4 | Vue.use(VueLazyload, { 5 | preLoad: 1.3 6 | // attempt: 1, 7 | // loading: '/img/default-profile.svg' 8 | }) 9 | -------------------------------------------------------------------------------- /src/server/api.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from 'express' 2 | import cookieParser from 'cookie-parser' 3 | import { json } from 'body-parser' 4 | import cors from 'cors' 5 | import { ApiConfig } from 'types-module' 6 | import { 7 | claimsHandler, 8 | extractHeaderHandler, 9 | HandlerConfig, 10 | healthyHandler, 11 | notifyHandler, 12 | tokenHandler, 13 | verifyHandler 14 | } from 'handlers-module' 15 | 16 | HandlerConfig.setCredentials(require('./config/firebase-admin-credentials.json')) 17 | 18 | const router = Router() 19 | router.get(ApiConfig.healthy, healthyHandler) 20 | router.get(ApiConfig.auth.verify, extractHeaderHandler, tokenHandler, verifyHandler) 21 | router.post(ApiConfig.auth.claims, extractHeaderHandler, tokenHandler, claimsHandler) 22 | router.post(ApiConfig.notification.notify.context, extractHeaderHandler, tokenHandler, notifyHandler) 23 | 24 | const app = express() 25 | app.disable('x-powered-by') 26 | app.use(json()) 27 | app.use(cookieParser()) 28 | app.use(cors({ origin: true })) 29 | app.use(router) 30 | 31 | export default { 32 | path: '/api', 33 | handler: app 34 | } 35 | -------------------------------------------------------------------------------- /src/server/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rslvn/nuxt-typescript-ssr-firebase-auth/4288cb24e284a93bcb0202cbc38b44d214920b1d/src/server/config/.gitkeep -------------------------------------------------------------------------------- /src/server/config/README.md: -------------------------------------------------------------------------------- 1 | add a firebase-admin-credentials.json here to use serverMiddleware 2 | -------------------------------------------------------------------------------- /src/server/sitemap.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from 'express' 2 | import { sitemapHandler } from 'handlers-module' 3 | 4 | const app = express() 5 | app.disable('x-powered-by') 6 | 7 | const router = Router() 8 | 9 | router.get('/sitemap.xml', sitemapHandler) 10 | app.use(router) 11 | 12 | export default { 13 | handler: app 14 | } 15 | -------------------------------------------------------------------------------- /src/service/api-service.ts: -------------------------------------------------------------------------------- 1 | import { NuxtAxiosInstance } from '@nuxtjs/axios' 2 | import { AxiosResponse } from 'axios' 3 | import { ApiConfig, AppHeader, AuthUser } from 'types-module' 4 | import { generateUuid } from '~/service/global-service' 5 | 6 | let sessionId: string|null = null 7 | 8 | export const configureAxiosDefaults = (axios: NuxtAxiosInstance) => { 9 | sessionId = generateUuid() 10 | axios.onRequest((config) => { 11 | config.headers.common[AppHeader.SESSION_ID] = sessionId 12 | config.headers.common[AppHeader.REQUEST_ID] = generateUuid() 13 | }) 14 | } 15 | 16 | export const authVerify = (axios: NuxtAxiosInstance) => { 17 | return axios 18 | .get(ApiConfig.auth.verify) 19 | .then((response: AxiosResponse) => { 20 | return response.data 21 | }) 22 | } 23 | 24 | export const authClaims = (axios: NuxtAxiosInstance, username: string) => { 25 | const claims = { username } 26 | return axios.post(ApiConfig.auth.claims, { claims }) 27 | } 28 | 29 | export const notificationNotify = (axios: NuxtAxiosInstance, notificationId: string) => { 30 | const notify = ApiConfig.notification.notify 31 | return axios.post(notify.context.replace(notify.params.notificationId, notificationId)) 32 | } 33 | -------------------------------------------------------------------------------- /src/service/error-service.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'vuex' 2 | import { errorToNotificationMessage, sendNotification } from '~/service/notification-service' 3 | import { NotificationMessage } from '~/types' 4 | 5 | const sendErrorNotification = async (dispatch: Dispatch, error: Error) => { 6 | await sendNotification(dispatch, errorToNotificationMessage(error)) 7 | } 8 | 9 | export const handleError = async (dispatch: Dispatch, error: Error, notificationMessage ?: NotificationMessage) => { 10 | console.error(error) 11 | notificationMessage 12 | ? await sendNotification(dispatch, notificationMessage) 13 | : await sendErrorNotification(dispatch, error) 14 | } 15 | -------------------------------------------------------------------------------- /src/service/firebase/firebase-service.ts: -------------------------------------------------------------------------------- 1 | import { User, UserInfo } from 'firebase' 2 | import jwtDecode from 'jwt-decode' 3 | import { AuthUser, FirebaseClaimKey, Image, ProviderData, ProviderType } from 'types-module' 4 | import { auth } from '~/plugins/fire-init-plugin' 5 | import { DefaultProfilePhoto } from '~/types' 6 | 7 | export const getProviderData = (userInfo: UserInfo|null|undefined): ProviderData|null => { 8 | return userInfo 9 | ? { 10 | providerType: userInfo.providerId as ProviderType, 11 | displayName: userInfo.displayName, 12 | email: userInfo.email, 13 | phoneNumber: userInfo.phoneNumber, 14 | photoURL: userInfo.photoURL, 15 | uid: userInfo.uid 16 | } 17 | : null 18 | } 19 | 20 | export const getAuthUser = (firebaseUser: User): AuthUser => { 21 | return { 22 | name: firebaseUser.displayName, 23 | email: firebaseUser.email, 24 | profilePhoto: { 25 | src: firebaseUser.photoURL || DefaultProfilePhoto.src, 26 | alt: firebaseUser.displayName ? 'Picture of ' + firebaseUser.displayName : DefaultProfilePhoto.alt 27 | }, 28 | userId: firebaseUser.uid, 29 | username: '', 30 | verified: firebaseUser.emailVerified, 31 | providers: firebaseUser.providerData?.filter(value => !!value).map(value => getProviderData(value)) 32 | } 33 | } 34 | 35 | export const getProviderOption = (provider: ProviderType) => { 36 | return { 37 | provider: provider.replace('.com', '') 38 | } 39 | } 40 | 41 | export const decodeToken = (token: string): AuthUser => { 42 | const decodedToken: any = jwtDecode(token) 43 | const name = decodedToken[FirebaseClaimKey.NAME] 44 | const profilePhoto: Image = { 45 | src: decodedToken[FirebaseClaimKey.PICTURE], 46 | alt: `Cover photo of ${name}` 47 | } 48 | const providers: ProviderData[] = Object.keys(decodedToken[FirebaseClaimKey.FIREBASE]?.identities) 49 | .map((provider: string) => { 50 | return { 51 | providerType: provider as ProviderType 52 | } 53 | }) 54 | 55 | return { 56 | userId: decodedToken[FirebaseClaimKey.USER_ID], 57 | username: decodedToken[FirebaseClaimKey.USERNAME], 58 | name, 59 | email: decodedToken[FirebaseClaimKey.EMAIL], 60 | verified: decodedToken[FirebaseClaimKey.EMAIL_VERIFIED], 61 | profilePhoto, 62 | providers 63 | } 64 | } 65 | 66 | export const refreshToken = async () => { 67 | await auth.currentUser?.getIdToken(true) 68 | } 69 | 70 | export const updateProfileName = async (displayName: string) => { 71 | await auth.currentUser?.updateProfile({ displayName }) 72 | } 73 | 74 | export const updateProfilePhotoUrl = async (photoURL: string) => { 75 | await auth.currentUser?.updateProfile({ photoURL }) 76 | } 77 | 78 | export const updateProfile = async (displayName: string, photoURL: string) => { 79 | await auth.currentUser?.updateProfile({ displayName, photoURL }) 80 | } 81 | -------------------------------------------------------------------------------- /src/service/firebase/firestore/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-collection' 2 | export * from './following-collection' 3 | export * from './push-notification-collection' 4 | -------------------------------------------------------------------------------- /src/service/firebase/firestore/user-device-collection.ts: -------------------------------------------------------------------------------- 1 | import { collection, CollectionField, FirebaseQueryOperator, UserDevice, WhereClause } from 'types-module' 2 | import { deleteModel, getModelsByWhereClauses, saveModel } from '~/service/firebase/firestore/collection-base-service' 3 | 4 | export const saveUserDevice = async (userDevice: UserDevice): Promise => { 5 | return await deleteUserDeviceByToken(userDevice.deviceToken) 6 | .then(async () => { 7 | return await saveModel(collection.USER_DEVICE, userDevice) 8 | }) 9 | } 10 | 11 | export const deleteUserDeviceByToken = (deviceToken: string) => { 12 | return getUserDevicesByDeviceToken(deviceToken) 13 | .then((userDevices) => { 14 | console.log(`found ${userDevices.length} userDevices for the token`) 15 | if (userDevices.length === 0) { 16 | return 17 | } 18 | userDevices.forEach(async (userDevice) => { 19 | await deleteModel(collection.USER_DEVICE, userDevice) 20 | }) 21 | }) 22 | } 23 | 24 | export const getUserDevicesByDeviceToken = (deviceToken: string) => { 25 | const deviceTokenWhereClause: WhereClause = { 26 | field: CollectionField.USER_DEVICE.deviceToken, 27 | operator: FirebaseQueryOperator.EQ, 28 | value: deviceToken 29 | } 30 | return getModelsByWhereClauses(collection.USER_DEVICE, deviceTokenWhereClause) 31 | } 32 | -------------------------------------------------------------------------------- /src/service/global-service.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import slug from 'slug' 3 | import { v4 as uuidv4 } from 'uuid' 4 | import { Route } from 'vue-router' 5 | import { QueryParameters, Routes, RouteType } from '../types' 6 | 7 | const timestampFormat: string = 'MM/DD/YYYY HH:mm:ss.SSS' 8 | const slugDelimiter = '-' 9 | 10 | export const dashAllRegex = /-/g 11 | // new RegExp('-', 'g') 12 | 13 | export const generateUuid = () => { 14 | return uuidv4().replace(dashAllRegex, '') 15 | } 16 | 17 | export const generateNumericByLength = (length: number) => { 18 | return Math.random().toString(9).substr(2, length) 19 | } 20 | 21 | export const log = (...anyMessages: any[]) => { 22 | console.log(`${moment().format(timestampFormat)} | `, ...anyMessages) 23 | } 24 | 25 | /** 26 | * 27 | * @param fileName 28 | * @returns {*} 29 | */ 30 | export const getNewFileName = (fileName: string): string => { 31 | if (fileName) { 32 | const baseNameSlug = slugify(basename(fileName)) 33 | const extension = fileName.split('.').pop() 34 | return `${baseNameSlug}${slugDelimiter}${generateUuid()}.${extension}` 35 | } 36 | return fileName 37 | } 38 | 39 | /** 40 | * 41 | * @param fileName 42 | * @returns {*|void|string|never} 43 | */ 44 | export const basename = (fileName: string): string => { 45 | return fileName.split('.').slice(0, -1).join('.') 46 | } 47 | 48 | export const slugify = (text: string): string => { 49 | return slug(text, slugDelimiter) 50 | } 51 | 52 | export const toBoolean = (value: string|boolean): boolean => { 53 | return typeof value === 'boolean' ? value : JSON.parse(value) 54 | } 55 | 56 | export const authenticatedAllowed = (route: Route): boolean => { 57 | return route.matched.some(record => 58 | record.path.startsWith(Routes.PROFILE_DYNAMIC.path) || 59 | record.path.startsWith(Routes.PROFILE.path)) 60 | } 61 | 62 | export const authenticatedNotAllowed = (route: Route): boolean => { 63 | return ( 64 | route.path === Routes.LOGIN.path || 65 | route.path === Routes.REGISTER.path || 66 | route.path === Routes.FORGET_PASSWORD.path || 67 | route.path === Routes.RESET_PASSWORD.path 68 | ) 69 | } 70 | 71 | export const getUserRoute = (routeType: RouteType, username: string) => { 72 | return { 73 | name: routeType.name, 74 | params: { 75 | username 76 | } 77 | } 78 | } 79 | 80 | export const getPageRouteWithQuery = (routeType: RouteType, query: string) => { 81 | return { 82 | path: routeType.path, 83 | query: { 84 | [QueryParameters.QUERY]: query 85 | } 86 | } 87 | } 88 | 89 | export const generateUsername = (username: string) => { 90 | const usernamePrefix = username.replace(/@.*$/, '') 91 | return slugify(usernamePrefix).replace(dashAllRegex, '') 92 | } 93 | -------------------------------------------------------------------------------- /src/service/notification-service.ts: -------------------------------------------------------------------------------- 1 | import { NotificationProgrammatic } from 'buefy' 2 | import { Dispatch } from 'vuex' 3 | import { NotificationMessage, NotificationType, StoreConfig } from '~/types' 4 | 5 | const hasIcon = true 6 | const position = 'is-top-right' 7 | 8 | const toasterConfig = { 9 | hasIcon, 10 | queue: false, 11 | duration: 3000 12 | } 13 | 14 | export const sendNotification = async ( 15 | dispatch: Dispatch, 16 | notificationMessage: NotificationMessage 17 | ) => { 18 | await dispatch( 19 | StoreConfig.notification.saveNotificationMessage, 20 | notificationMessage, 21 | { 22 | root: true 23 | } 24 | ) 25 | } 26 | 27 | export const sendSuccessNotification = async ( 28 | dispatch: Dispatch, 29 | message: any 30 | ) => { 31 | await sendNotification(dispatch, getSuccessNotificationMessage(message)) 32 | } 33 | 34 | export const sendDangerNotification = async ( 35 | dispatch: Dispatch, 36 | message: any 37 | ) => { 38 | await sendNotification(dispatch, getDangerNotificationMessage(message)) 39 | } 40 | 41 | export const sendWarningNotification = async ( 42 | dispatch: Dispatch, 43 | message: any 44 | ) => { 45 | await sendNotification(dispatch, getWarningNotificationMessage(message)) 46 | } 47 | 48 | export const sendInfoNotification = async ( 49 | dispatch: Dispatch, 50 | message: any 51 | ) => { 52 | await sendNotification(dispatch, getInfoNotificationMessage(message)) 53 | } 54 | 55 | export const errorToNotificationMessage = ( 56 | error: Error 57 | ): NotificationMessage => { 58 | return { 59 | type: NotificationType.DANGER, 60 | message: error.message, 61 | hasIcon 62 | } 63 | } 64 | 65 | export const getSuccessNotificationMessage = ( 66 | message: any 67 | ): NotificationMessage => { 68 | return { 69 | type: NotificationType.SUCCESS, 70 | message, 71 | hasIcon 72 | } 73 | } 74 | 75 | export const getDangerNotificationMessage = ( 76 | message: any 77 | ): NotificationMessage => { 78 | return { 79 | type: NotificationType.DANGER, 80 | message, 81 | hasIcon 82 | } 83 | } 84 | 85 | export const getWarningNotificationMessage = ( 86 | message: any 87 | ): NotificationMessage => { 88 | return { 89 | type: NotificationType.WARNING, 90 | message, 91 | hasIcon 92 | } 93 | } 94 | 95 | export const getInfoNotificationMessage = ( 96 | message: any 97 | ): NotificationMessage => { 98 | return { 99 | type: NotificationType.INFO, 100 | message, 101 | hasIcon 102 | } 103 | } 104 | 105 | export const showErrorToaster = (message: any): void => { 106 | NotificationProgrammatic.open({ 107 | type: 'is-danger', 108 | message, 109 | position, 110 | ...toasterConfig 111 | }) 112 | } 113 | 114 | export const showInfoToaster = (message: any): void => { 115 | NotificationProgrammatic.open({ 116 | type: 'is-info', 117 | message, 118 | position, 119 | ...toasterConfig 120 | }) 121 | } 122 | 123 | export const showWarningToaster = (message: any): void => { 124 | NotificationProgrammatic.open({ 125 | type: 'is-warning', 126 | message, 127 | position, 128 | ...toasterConfig 129 | }) 130 | } 131 | 132 | export const showSuccessToaster = (message: any): void => { 133 | NotificationProgrammatic.open({ 134 | type: 'is-success', 135 | message, 136 | position, 137 | ...toasterConfig 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /src/service/rx-service.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs' 2 | import { Image, PushNotification } from 'types-module' 3 | import { ModuleType, UpdatePushNotificationStatus } from '~/types' 4 | 5 | // Configuration 6 | export const configureFcmObservable = new Subject() 7 | export const configureAxiosObservable = new Subject() 8 | 9 | // Profile 10 | export const coverPhotoObservable = new Subject() 11 | export const profilePhotoObservable = new Subject() 12 | export const reauthenticateObservable = new Subject() 13 | export const reloadUserFromDatabase = new Subject() 14 | export const loadMoreSearchResult = new Subject() 15 | export const showProfileModule = new Subject() 16 | export const reloadFollowing = new Subject() 17 | 18 | // Notification 19 | export const sendNotificationObservable = new Subject() 20 | export const loadNotificationObservable = new Subject() 21 | export const updateNotificationStatusObservable = new Subject() 22 | -------------------------------------------------------------------------------- /src/service/seo-service.ts: -------------------------------------------------------------------------------- 1 | import { Vue } from 'nuxt-property-decorator' 2 | import { DefaultMeta, PageMeta, RouteType } from '~/types' 3 | 4 | const getHeadTitle = (routeType: RouteType, vue: Vue) => { 5 | return vue.$t(`page.${routeType.label}.title`) 6 | } 7 | 8 | export const getHeadByRouteType = (routeType: RouteType, vue: Vue) => { 9 | const pageMeta: PageMeta = { 10 | title: `${getHeadTitle(routeType, vue)} | ${DefaultMeta.title}`, 11 | url: `${DefaultMeta.url}${routeType.path}`, 12 | description: vue.$t(`page.${routeType.label}.description`) + '', 13 | image: DefaultMeta.image 14 | } 15 | return getHead(pageMeta) 16 | } 17 | 18 | export const getHead = (pageMeta: PageMeta) => { 19 | return { 20 | title: pageMeta.title, 21 | meta: [ 22 | { hid: 'description', name: 'description', content: pageMeta.description }, 23 | // Open Graph 24 | { name: 'og:title', content: pageMeta.title }, 25 | { name: 'og:description', content: pageMeta.description }, 26 | { name: 'og:type', content: 'website' }, 27 | { name: 'og:url', content: pageMeta.url }, 28 | { name: 'og:image', content: pageMeta.image.src }, 29 | 30 | { name: 'twitter:card', content: 'summary_large_image' }, 31 | { name: 'twitter:site', content: '@whereistango' }, 32 | { name: 'twitter:title', content: pageMeta.title }, 33 | { name: 'twitter:description', content: pageMeta.description }, 34 | { name: 'twitter:image', content: pageMeta.image.src }, 35 | { name: 'twitter:image:alt', content: pageMeta.image.alt }, 36 | 37 | // Google / Schema.org markup: 38 | { itemprop: 'name', content: pageMeta.title }, 39 | { itemprop: 'description', content: pageMeta.description }, 40 | { itemprop: 'image', content: pageMeta.image.src } 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/shim-ts.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | user: any; 4 | nsSessionId: string; 5 | nsRequestId: string; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | // @ts-ignore 3 | import Vue from 'vue' 4 | export default Vue 5 | } 6 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rslvn/nuxt-typescript-ssr-firebase-auth/4288cb24e284a93bcb0202cbc38b44d214920b1d/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rslvn/nuxt-typescript-ssr-firebase-auth/4288cb24e284a93bcb0202cbc38b44d214920b1d/src/static/icon.png -------------------------------------------------------------------------------- /src/static/img/default-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rslvn/nuxt-typescript-ssr-firebase-auth/4288cb24e284a93bcb0202cbc38b44d214920b1d/src/static/img/default-cover.jpg -------------------------------------------------------------------------------- /src/static/img/default-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rslvn/nuxt-typescript-ssr-firebase-auth/4288cb24e284a93bcb0202cbc38b44d214920b1d/src/static/img/default-profile.png -------------------------------------------------------------------------------- /src/static/img/flag/rectangular/tr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/static/img/flag/rectangular/uk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/static/img/flag/rounded/tr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/static/img/flag/rounded/uk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 24 | 26 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/static/img/flag/square/tr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/static/img/flag/square/uk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /admin 3 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree } from 'vuex' 2 | import { Context } from '@nuxt/types' 3 | import { AxiosError } from 'axios' 4 | import { AppCookie } from 'types-module' 5 | import { RootState, StoreConfig } from '../types' 6 | import { decodeToken } from '~/service/firebase/firebase-service' 7 | import { authVerify, configureAxiosDefaults } from '~/service/api-service' 8 | 9 | export const state = (): RootState => ({}) 10 | 11 | export const actions: ActionTree = { 12 | async nuxtServerInit ({ commit }, { route, app }: Context) { 13 | return await Promise.resolve() 14 | .then(async () => { 15 | console.log(`>>>>>>>>>> nuxtServerInit for path: ${route.path}`) 16 | 17 | configureAxiosDefaults(app.$axios) 18 | 19 | const token = app.$cookies.get(AppCookie.TOKEN) 20 | if (!token) { 21 | console.log('No token') 22 | return 23 | } 24 | 25 | console.log('Token FOUND') 26 | 27 | await authVerify(app.$axios) 28 | .then(authUser => commit(StoreConfig.auth.setAuthUser, authUser)) 29 | .catch((error: AxiosError) => { 30 | if (error?.response?.status === 401) { 31 | console.log('Token DECODED') 32 | commit(StoreConfig.auth.setAuthUser, decodeToken(token)) 33 | } else { 34 | console.log('Error: ', error) 35 | commit(StoreConfig.auth.forceLogout, true) 36 | app.$cookies.remove(AppCookie.TOKEN) 37 | } 38 | }) 39 | }) 40 | .catch((error: Error) => console.log(error)) 41 | .finally(() => commit(StoreConfig.loading.setLoading, false)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/store/loading.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree, GetterTree, MutationTree } from 'vuex' 2 | import { LoadingState, RootState } from '~/types' 3 | 4 | export const state = (): LoadingState => ({ 5 | loading: true 6 | }) 7 | 8 | export const getters: GetterTree = { 9 | loading: state => state.loading 10 | } 11 | 12 | export const mutations: MutationTree = { 13 | setLoading (state, loading: boolean) { 14 | state.loading = loading 15 | } 16 | } 17 | 18 | export const actions: ActionTree = { 19 | saveLoading ({ commit }, loading: boolean) { 20 | commit('setLoading', loading) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/store/notification.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree, GetterTree, MutationTree } from 'vuex' 2 | import { NotificationMessage, NotificationState, RootState } from '~/types' 3 | 4 | export const state = (): NotificationState => ({ 5 | notificationMessage: undefined 6 | }) 7 | 8 | export const getters: GetterTree = { 9 | notificationMessage: (state: NotificationState) => state.notificationMessage 10 | } 11 | 12 | export const mutations: MutationTree = { 13 | setNotificationMessage (state, notificationMessage: NotificationMessage) { 14 | state.notificationMessage = notificationMessage 15 | } 16 | } 17 | 18 | export const actions: ActionTree = { 19 | saveNotificationMessage ({ commit }, notificationMessage: NotificationMessage) { 20 | commit('setNotificationMessage', notificationMessage) 21 | }, 22 | clearNotificationMessage ({ commit }) { 23 | commit('setNotificationMessage', undefined) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/store/profile.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree, GetterTree, MutationTree } from 'vuex' 2 | import { Image, User } from 'types-module' 3 | import { auth } from '~/plugins/fire-init-plugin' 4 | import { handleError } from '~/service/error-service' 5 | import { saveUser } from '~/service/firebase/firestore' 6 | import { refreshToken, updateProfileName } from '~/service/firebase/firebase-service' 7 | import { authClaims } from '~/service/api-service' 8 | import { ProfileState, RootState } from '~/types' 9 | 10 | export const state = (): ProfileState => ({}) 11 | 12 | export const getters: GetterTree = {} 13 | 14 | export const mutations: MutationTree = {} 15 | 16 | export const actions: ActionTree = { 17 | async updateCoverPhoto ({ dispatch }, coverPhoto: Image) { 18 | return await saveUser({ 19 | id: auth.currentUser?.uid, 20 | coverPhoto 21 | }).catch((error: Error) => handleError(dispatch, error)) 22 | }, 23 | 24 | // eslint-disable-next-line no-empty-pattern 25 | async updateUser ({}, user: User) { 26 | return await saveUser(user) 27 | .then(async (savedUser: User) => { 28 | await updateProfileName(savedUser.name) 29 | .then(async () => await authClaims(this.$axios, savedUser.username)) 30 | .then(() => refreshToken()) 31 | return savedUser 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // add your custom config here 3 | // https://stylelint.io/user-guide/configuration 4 | rules: {} 5 | } 6 | -------------------------------------------------------------------------------- /src/tests/jest-mocks.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@vue/test-utils' 2 | config.mocks.$t = (key: string) => key 3 | config.showDeprecationWarnings = false 4 | config.silent = false 5 | -------------------------------------------------------------------------------- /src/tests/pages/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import Vue from 'vue' 3 | import Index from '~/pages/index.vue' 4 | 5 | Vue.config.ignoredElements = ['b-icon'] 6 | 7 | // const head = (): MetaInfo => { 8 | // return null 9 | // } 10 | 11 | describe('HomePage', () => { 12 | const wrapper = mount(Index) 13 | 14 | it('Home page loaded sucessfully', () => { 15 | expect(wrapper.vm).toBeTruthy() 16 | expect(wrapper.find('h1')) 17 | }) 18 | 19 | // it('check page title', () => { 20 | // // const spy = jest.spyOn(Index, 'head') 21 | // // @ts-ignore 22 | // expect(wrapper.vm.head()).toHaveBeenCalled() 23 | // }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "esnext", 8 | "esnext.asynciterable", 9 | "dom" 10 | ], 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "experimentalDecorators": true, 14 | "strictPropertyInitialization": false, 15 | "strictNullChecks": false, 16 | "allowJs": true, 17 | "sourceMap": true, 18 | "strict": true, 19 | "noEmit": true, 20 | "baseUrl": ".", 21 | "paths": { 22 | "~/*": [ 23 | "./*" 24 | ], 25 | "@/*": [ 26 | "./*" 27 | ] 28 | }, 29 | "types": [ 30 | "@types/node", 31 | "@nuxt/types", 32 | "@nuxt/vue-app", 33 | "@types/jest", 34 | "@nuxtjs/axios", 35 | "nuxt-i18n", 36 | "cookie-universal-nuxt", 37 | "firebase", 38 | "firebase/app", 39 | "nuxt-property-decorator", 40 | "types-module", 41 | "handlers-module" 42 | ] 43 | }, 44 | "exclude": [ 45 | "node_modules", 46 | ".nuxt", 47 | "dist" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /src/types/firebase-types.ts: -------------------------------------------------------------------------------- 1 | import { Image, PrivacyType, ProviderData, ProviderType } from 'types-module' 2 | 3 | export enum FirebaseAuthAction { 4 | VERIFY_EMAIL = 'verifyEmail', 5 | RECOVERY_EMAIL = 'recoverEmail', 6 | RESET_PASSWORD = 'resetPassword' 7 | } 8 | 9 | export enum FirebaseAuthActionParams { 10 | ACTION = 'mode', 11 | ACTION_CODE = 'oobCode', 12 | } 13 | 14 | export interface RegistrationCredentials { 15 | name: string, 16 | email: string, 17 | password: string 18 | } 19 | 20 | export interface LoginCredentials { 21 | email: string, 22 | password: string, 23 | rememberMe: boolean 24 | } 25 | 26 | export interface SocialLoginCredentials { 27 | providerType: ProviderType, 28 | rememberMe: boolean 29 | } 30 | 31 | export interface ProviderConfig { 32 | providerType: ProviderType, 33 | colorType: string, 34 | icon: string, 35 | iconPack: string 36 | } 37 | 38 | export const SupportedProviders: ProviderConfig[] = [ 39 | { 40 | providerType: ProviderType.PASSWORD, 41 | colorType: 'is-primary', 42 | icon: 'at', 43 | iconPack: 'fas' 44 | }, 45 | { 46 | providerType: ProviderType.GOOGLE, 47 | colorType: 'is-danger', 48 | icon: 'google', 49 | iconPack: 'fab' 50 | }, 51 | { 52 | providerType: ProviderType.TWITTER, 53 | colorType: 'is-info', 54 | icon: 'twitter', 55 | iconPack: 'fab' 56 | }, 57 | { 58 | providerType: ProviderType.FACEBOOK, 59 | colorType: 'is-link', 60 | icon: 'facebook', 61 | iconPack: 'fab' 62 | } 63 | ] 64 | 65 | export interface ProviderLink { 66 | providerConfig: ProviderConfig, 67 | providerData?: ProviderData, 68 | linked: boolean, 69 | linkMethod: () => void 70 | } 71 | 72 | export interface SearchData { 73 | name: string, 74 | username: string, 75 | profilePhoto: Image, 76 | coverPhoto: Image, 77 | privacy: PrivacyType 78 | } 79 | 80 | export interface PagingResponse { 81 | total: number, 82 | totalPages: number, 83 | data: T[], 84 | } 85 | 86 | /** 87 | Firebase Storage Types 88 | */ 89 | export interface StorageRef { 90 | folderRef: string, 91 | parameters?: any 92 | } 93 | 94 | export const ProfilePhotoStorageRef: StorageRef = { 95 | folderRef: 'user/:userId/profilePhoto/', 96 | parameters: { 97 | userId: ':userId' 98 | } 99 | } 100 | 101 | export const CoverPhotoStorageRef: StorageRef = { 102 | folderRef: 'user/:userId/coverPhoto/', 103 | parameters: { 104 | userId: ':userId' 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { CookieSerializeOptions } from 'cookie' 2 | import { Image, PrivacyType, PushNotification, User } from 'types-module' 3 | 4 | export * from './firebase-types' 5 | export * from './state-types' 6 | export * from './route-types' 7 | export * from './seo-types' 8 | export * from './rx-types' 9 | 10 | export const ProfilePhotoPlaceholder = '/img/default-profile.png' 11 | export const CoverPhotoPlaceholder = '/img/default-cover.jpg' 12 | 13 | export const DefaultProfilePhoto: Image = { 14 | src: ProfilePhotoPlaceholder, 15 | alt: 'default profile picture', 16 | default: true 17 | } 18 | 19 | export const DefaultCoverPhoto: Image = { 20 | src: CoverPhotoPlaceholder, 21 | alt: 'default cover picture', 22 | default: true 23 | } 24 | 25 | export interface SupportedLanguage { 26 | name: string, 27 | code: string, 28 | alias: string, 29 | flag: Image 30 | } 31 | 32 | export const SupportedLanguages: SupportedLanguage[] = [ 33 | { 34 | name: 'English', 35 | code: 'en', 36 | alias: 'EN', 37 | flag: { 38 | src: '/img/flag/rounded/uk.svg', 39 | alt: 'England flag', 40 | default: true 41 | } 42 | }, 43 | { 44 | name: 'Turkce', 45 | code: 'tr', 46 | alias: 'TR', 47 | flag: { 48 | src: '/img/flag/rounded/tr.svg', 49 | alt: 'Turkiye bayragi', 50 | default: true 51 | } 52 | } 53 | ] 54 | 55 | export enum LocalStorageKey { 56 | FCM_TOKEN = 'fcmToken' 57 | } 58 | 59 | export const cookieOptions: CookieSerializeOptions = { sameSite: 'lax', path: '/' } 60 | 61 | export const sessionCookieOptionsDev: CookieSerializeOptions = { sameSite: 'none', path: '/' } 62 | 63 | export const sessionCookieOptionsProd: CookieSerializeOptions = { sameSite: 'none', secure: true, path: '/' } 64 | 65 | export enum ModuleType { 66 | ABOUT_ME = 'profileAboutMe', 67 | LINKED_ACCOUNTS = 'linkedAccounts', 68 | SETTINGS = 'profileSettings', 69 | FOLLOWERS = 'profileFollowers', 70 | FOLLOWINGS = 'profileFollowings', 71 | } 72 | 73 | export enum SettingsType { 74 | GENERAL = 'general', 75 | PRIVACY = 'privacy', 76 | } 77 | 78 | export interface NavigatorConfig { 79 | type: T, 80 | icon: string, 81 | component: M, 82 | componentProperties?: Object, 83 | private: boolean 84 | } 85 | 86 | export interface ModuleTabConfig extends NavigatorConfig { 87 | 88 | } 89 | 90 | export interface SettingsMenuConfig extends NavigatorConfig { 91 | 92 | } 93 | 94 | export interface PushNotificationEnriched { 95 | fromUser: User, 96 | pushNotification: PushNotification 97 | } 98 | 99 | export interface PrivacyConfig { 100 | privacyType: PrivacyType, 101 | colorType: string, 102 | icon: string, 103 | type: string, 104 | iconPack?: string 105 | } 106 | 107 | export const PrivacyConfigList: PrivacyConfig[] = [ 108 | { 109 | privacyType: PrivacyType.PRIVATE, 110 | colorType: 'is-danger', 111 | icon: 'account-lock', 112 | type: 'is-danger' 113 | }, 114 | { 115 | privacyType: PrivacyType.PUBLIC, 116 | colorType: 'is-primary', 117 | icon: 'earth', 118 | type: 'is-primary' 119 | } 120 | ] 121 | -------------------------------------------------------------------------------- /src/types/route-types.ts: -------------------------------------------------------------------------------- 1 | export interface RouteType { 2 | label: string, 3 | name?: string, 4 | path: string 5 | } 6 | 7 | export const HOME: RouteType = { label: 'home', path: '/' } 8 | export const LOGIN: RouteType = { label: 'login', path: '/login' } 9 | export const REGISTER: RouteType = { label: 'register', path: '/register' } 10 | export const PROFILE: RouteType = { label: 'profile', path: '/profile' } 11 | export const PROFILE_DYNAMIC: RouteType = { label: 'profile', name: 'u-username', path: '/u/' } 12 | export const PROFILE_NOTIFICATION: RouteType = { 13 | label: 'profileNotification', 14 | name: 'u-username-notification', 15 | path: '/u/:username?/notification' 16 | } 17 | export const ACTION: RouteType = { label: 'action', path: '/auth/action' } 18 | export const FORGET_PASSWORD: RouteType = { label: 'forgetPassword', path: '/auth/forget-password' } 19 | export const RESET_PASSWORD: RouteType = { label: 'resetPassword', path: '/auth/reset-password' } 20 | export const CROP: RouteType = { label: 'crop', path: '/crop', name: 'crop' } 21 | export const IMAGES: RouteType = { label: 'images', path: '/images' } 22 | export const LIGHT_BOX: RouteType = { label: 'lightbox', path: '/lightbox' } 23 | export const TERMS: RouteType = { label: 'terms', path: '/terms' } 24 | export const PRIVACY_POLICY: RouteType = { label: 'privacyAndPolicy', path: '/privacy-policy' } 25 | export const SEARCH: RouteType = { label: 'search', path: '/search' } 26 | 27 | export const Routes = { 28 | HOME, 29 | LOGIN, 30 | REGISTER, 31 | PROFILE, 32 | PROFILE_DYNAMIC, 33 | PROFILE_NOTIFICATION, 34 | ACTION, 35 | FORGET_PASSWORD, 36 | RESET_PASSWORD, 37 | CROP, 38 | IMAGES, 39 | LIGHT_BOX, 40 | TERMS, 41 | PRIVACY_POLICY, 42 | SEARCH 43 | } 44 | 45 | export enum RouteParameters { 46 | ACTION_CODE = 'actionCode', 47 | USERNAME = 'username', 48 | } 49 | 50 | export enum QueryParameters { 51 | NEXT = 'next', 52 | LANG = 'lang', 53 | QUERY = 'query' 54 | } 55 | -------------------------------------------------------------------------------- /src/types/rx-types.ts: -------------------------------------------------------------------------------- 1 | import { PushNotificationStatus } from 'types-module' 2 | 3 | export interface UpdatePushNotificationStatus { 4 | id: string, 5 | status: PushNotificationStatus 6 | } 7 | -------------------------------------------------------------------------------- /src/types/seo-types.ts: -------------------------------------------------------------------------------- 1 | import { Image } from 'types-module' 2 | 3 | export enum SocialMetaType { 4 | FACEBOOK = 'is-facebook', 5 | TWITTER = 'is-twitter', 6 | WHATSAPP = 'is-whatsapp', 7 | EMAIL = 'is-email', 8 | SMS = 'is-sms', 9 | PINTEREST = 'is-pinterest' 10 | } 11 | 12 | export interface SocialMetaConfig { 13 | type: SocialMetaType, 14 | icon: string, 15 | shareLink: string 16 | } 17 | 18 | export const SocialMetaConfigs: SocialMetaConfig[] = [ 19 | { 20 | type: SocialMetaType.FACEBOOK, 21 | icon: 'facebook', 22 | shareLink: 'https://www.facebook.com/sharer/sharer.php?u=@u&title=@t&description=@d&hashtag=@h' 23 | }, 24 | { 25 | type: SocialMetaType.TWITTER, 26 | icon: 'twitter', 27 | shareLink: 'https://twitter.com/intent/tweet?text=@t&url=@u&hashtags=@h' 28 | }, 29 | { 30 | type: SocialMetaType.WHATSAPP, 31 | icon: 'whatsapp', 32 | shareLink: 'https://api.whatsapp.com/send?text=@t%0D%0A@u%0D%0A@d' 33 | }, 34 | { 35 | type: SocialMetaType.PINTEREST, 36 | icon: 'pinterest', 37 | shareLink: 'https://pinterest.com/pin/create/button/?url=@u&media=@m&description=@t' 38 | }, 39 | { 40 | type: SocialMetaType.EMAIL, 41 | icon: 'email-send', 42 | shareLink: 'mailto:?subject=@t&body=@u%0D%0A@d' 43 | }, 44 | { 45 | type: SocialMetaType.SMS, 46 | icon: 'message-text', 47 | shareLink: 'sms:?body=@t%0D%0A@u%0D%0A@d' 48 | } 49 | ] 50 | 51 | export interface PageMeta { 52 | url: string, 53 | title: string, 54 | description: string, 55 | image: Image, 56 | tags?: string 57 | } 58 | 59 | export const DefaultMeta: PageMeta = { 60 | title: 'Nuxt TS Firebase Auth SSR', 61 | description: 'The description of Nuxt TS Firebase Auth SSR web', 62 | image: { 63 | src: process.env.WEBSITE_URL + '/icon.png', 64 | alt: 'The image of the Nuxt TS Firebase Auth SSR web' 65 | }, 66 | url: process.env.WEBSITE_URL, 67 | tags: 'nuxtsocial,nuxt,ssr,firebase,firebaseauth,firebasefunctions,rslvn' 68 | } 69 | -------------------------------------------------------------------------------- /src/types/state-types.ts: -------------------------------------------------------------------------------- 1 | import { namespace } from 'vuex-class' 2 | import { AuthUser } from 'types-module' 3 | 4 | export enum NotificationType { 5 | DANGER = 'is-danger', 6 | WARNING = 'is-warning', 7 | INFO = 'is-info', 8 | SUCCESS = 'is-success' 9 | } 10 | 11 | export interface NotificationMessage { 12 | type: NotificationType, 13 | message: string, 14 | hasIcon?: boolean 15 | } 16 | 17 | export interface AuthState { 18 | authUser?: AuthUser, 19 | forceLogout: boolean, 20 | rememberMe: boolean 21 | } 22 | 23 | export interface ProfileState { 24 | } 25 | 26 | export interface NotificationState { 27 | notificationMessage?: NotificationMessage, 28 | } 29 | 30 | export interface LoadingState { 31 | loading: boolean 32 | } 33 | 34 | export interface RootState { 35 | auth?: AuthState, 36 | notification?: NotificationState, 37 | loading?: LoadingState, 38 | profile?: ProfileState 39 | } 40 | 41 | export const StateNamespace = { 42 | auth: namespace('auth'), 43 | notification: namespace('notification'), 44 | loading: namespace('loading'), 45 | profile: namespace('profile') 46 | } 47 | 48 | export const StoreConfig = { 49 | auth: { 50 | setAuthUser: 'auth/setAuthUser', 51 | forceLogout: 'auth/forceLogout', 52 | logout: 'auth/logout', 53 | saveRememberMe: 'auth/saveRememberMe' 54 | }, 55 | notification: { 56 | saveNotificationMessage: 'notification/saveNotificationMessage', 57 | clearNotificationMessage: 'notification/clearNotificationMessage' 58 | }, 59 | loading: { 60 | setLoading: 'loading/setLoading', 61 | saveLoading: 'loading/saveLoading' 62 | } 63 | } 64 | --------------------------------------------------------------------------------