├── components ├── emailStats.vue ├── mods │ └── monaco.js ├── footer.vue ├── appbar.vue ├── emailBox.vue ├── fileCard.vue ├── embed.vue └── templateCard.vue ├── assets ├── logo.png ├── global.scss └── variables.scss ├── static └── favicon.ico ├── api ├── assets │ └── trackerImage.png ├── controllers │ ├── tracker.controller.js │ ├── handlebars.controller.js │ ├── puppeteer.controller.js │ ├── excel.controller.js │ ├── bucket.controller.js │ ├── fileHandler.controller.js │ ├── gmail.controller.js │ └── db.controller.js ├── models │ ├── user.model.js │ ├── files.model.js │ ├── template.model.js │ └── email.model.js ├── routes │ ├── tracker.routes.js │ ├── basic.routes.js │ ├── fileHandler.routes.js │ ├── template.routes.js │ └── emails.routes.js └── index.js ├── plugins ├── prisma-highlight.js └── editor.js ├── jsconfig.json ├── .editorconfig ├── middleware └── auth.js ├── .eslintrc.js ├── store ├── README.md ├── systemConfig.js └── auth.js ├── pages ├── templates │ ├── rules.vue │ ├── index.vue │ ├── upload.vue │ └── create │ │ ├── _id.vue │ │ └── index.vue ├── home.vue ├── emails │ ├── _id.vue │ ├── index.vue │ └── send │ │ └── _id.vue ├── assets │ ├── upload.vue │ └── index.vue └── index.vue ├── layouts ├── error.vue ├── default.vue └── dashboard.vue ├── content ├── hello.md └── rules.md ├── package.json ├── README.md ├── .gitignore ├── Instructions.md └── nuxt.config.js /components/emailStats.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/mods/monaco.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/footer.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iresharma/G-Notify/HEAD/assets/logo.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iresharma/G-Notify/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /api/assets/trackerImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iresharma/G-Notify/HEAD/api/assets/trackerImage.png -------------------------------------------------------------------------------- /assets/global.scss: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: "Open Sans", sans-serif; 3 | } 4 | 5 | .logo { 6 | font-family: "Prata", serif; 7 | } 8 | -------------------------------------------------------------------------------- /plugins/prisma-highlight.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VuePrism from 'vue-prism' 3 | import 'prismjs/themes/prism.css' 4 | 5 | Vue.use(VuePrism) 6 | -------------------------------------------------------------------------------- /assets/variables.scss: -------------------------------------------------------------------------------- 1 | // Ref: https://github.com/nuxt-community/vuetify-module#customvariables 2 | // 3 | // The variables you want to modify 4 | // $font-size-root: 20px; 5 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./*"], 6 | "@/*": ["./*"], 7 | "~~/*": ["./*"], 8 | "@@/*": ["./*"] 9 | } 10 | }, 11 | "exclude": ["node_modules", ".nuxt", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /plugins/editor.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import MonacoEditor from 'gg-vue-monaco-editor' 3 | 4 | import VueApexCharts from 'vue-apexcharts' 5 | Vue.use(VueApexCharts) 6 | 7 | Vue.component('Apexchart', VueApexCharts) 8 | 9 | Vue.component('MonacoEditor', MonacoEditor) 10 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | export default function ({ app }) { 2 | // If the user is not authenticated 3 | app.router.beforeResolve((to, from, next) => { 4 | if (app.store.getters['auth/isLogged']) { 5 | next() 6 | } else { 7 | next('/index') 8 | } 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /api/controllers/tracker.controller.js: -------------------------------------------------------------------------------- 1 | // Tracking functions 2 | import { addEmailRead } from './db.controller' 3 | 4 | const addReadEntry = (id, email) => { 5 | return new Promise((resolve, reject) => { 6 | try { 7 | addEmailRead(id, email) 8 | } catch (err) { return reject(err) } 9 | return resolve() 10 | }) 11 | } 12 | 13 | module.exports = { addReadEntry } 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: '@babel/eslint-parser', 9 | requireConfigFile: false 10 | }, 11 | extends: [ 12 | '@nuxtjs', 13 | 'plugin:nuxt/recommended' 14 | ], 15 | plugins: [ 16 | ], 17 | // add your custom rules here 18 | rules: {} 19 | } 20 | -------------------------------------------------------------------------------- /api/models/user.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const userSchema = new mongoose.Schema( 3 | { 4 | _id: mongoose.Schema.Types.ObjectId, 5 | token: Object, 6 | user: Object, 7 | emails: [{ 8 | type: mongoose.Schema.Types.ObjectId, ref: 'emails' 9 | }] 10 | }, 11 | { database: 'g-notify', collection: 'users' } 12 | ) 13 | module.exports = mongoose.model('User', userSchema) 14 | -------------------------------------------------------------------------------- /api/models/files.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const fileSchema = new mongoose.Schema( 3 | { 4 | _id: mongoose.Schema.Types.ObjectId, 5 | name: String, 6 | path: String, 7 | size: Number, 8 | mimeType: String, 9 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'users' } 10 | }, 11 | { database: 'g-notify', collection: 'files' } 12 | ) 13 | module.exports = mongoose.model('Files', fileSchema) 14 | -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 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 Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /pages/templates/rules.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /api/models/template.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const templateSchema = new mongoose.Schema( 3 | { 4 | _id: mongoose.Schema.Types.ObjectId, 5 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 6 | content: String, 7 | likes: Number, 8 | plainText: String, 9 | name: String, 10 | public: Boolean 11 | }, 12 | { database: 'g-notify', collection: 'templates' } 13 | ) 14 | module.exports = mongoose.model('Template', templateSchema) 15 | -------------------------------------------------------------------------------- /api/models/email.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const emailsSchema = new mongoose.Schema( 3 | { 4 | _id: mongoose.Schema.Types.ObjectId, 5 | template: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'Template' 8 | }, 9 | user: Object, 10 | recipients: [{ 11 | type: Object 12 | }], 13 | subject: String 14 | }, 15 | { database: 'g-notify', collection: 'emails' } 16 | ) 17 | module.exports = mongoose.model('Emails', emailsSchema) 18 | -------------------------------------------------------------------------------- /api/controllers/handlebars.controller.js: -------------------------------------------------------------------------------- 1 | const HandleBars = require('handlebars') 2 | 3 | HandleBars.registerHelper('loud', (string) => { 4 | return string.toUpperCase() 5 | }) 6 | 7 | HandleBars.registerHelper('timeStampTime', (timeStamp) => { 8 | const date = new Date(timeStamp * 1000) 9 | return `${date.getHours()}:${date.getMinutes()}` 10 | }) 11 | 12 | HandleBars.registerHelper('timeStampDate', (timeStamp) => { 13 | const date = new Date(timeStamp * 1000) 14 | return `${date.getDate()}/${date.getMonth()}/${date.getFullYear()}` 15 | }) 16 | -------------------------------------------------------------------------------- /store/systemConfig.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | snackbar: { 3 | show: false, 4 | type: '', 5 | text: '' 6 | }, 7 | drawer: false // initialized as null because vuetify takes null to default it to closed on mobile and open on desktop 8 | }) 9 | 10 | export const getters = { 11 | snackbar: state => state.snackbar, 12 | drawer: state => state.drawer 13 | } 14 | 15 | export const mutations = { 16 | SNACKBAR: (state, payload) => (state.snackbar = payload), 17 | TOGGLE_DRAWER: state => (state.drawer = !state.drawer) 18 | } 19 | -------------------------------------------------------------------------------- /api/controllers/puppeteer.controller.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer') 2 | 3 | const templateScreenshot = async (templateHTML, quality) => { 4 | const browser = await puppeteer.launch() 5 | const page = await browser.newPage() 6 | await page.setContent(templateHTML) 7 | await page.waitForTimeout(1000) 8 | const body = await page.$('body') 9 | const screenshot = await body.screenshot({ 10 | type: 'jpeg', 11 | quality: quality === 'low' ? 30 : 100, 12 | fullPage: !(quality === 'low') 13 | }) 14 | await page.close() 15 | await browser.close() 16 | 17 | return screenshot 18 | } 19 | 20 | module.exports = { templateScreenshot } 21 | -------------------------------------------------------------------------------- /pages/home.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | -------------------------------------------------------------------------------- /api/routes/tracker.routes.js: -------------------------------------------------------------------------------- 1 | // Tracking routes 2 | 3 | const router = require('express').Router() 4 | const trackerHandler = require('../controllers/tracker.controller') 5 | const fileHandler = require('../controllers/fileHandler.controller') 6 | 7 | router.get('/:templateId/:email', async (req, res) => { 8 | const { templateId, email } = req.params 9 | if (!templateId) { 10 | return res.status(400).send('Template id not found') 11 | } if (!email) { 12 | return res.status(400).send('Email not found') 13 | } 14 | try { 15 | await trackerHandler.addReadEntry(templateId, email) 16 | return res.status(200).sendFile(fileHandler.trackerFilePath()) 17 | } catch (err) { 18 | console.log(err) 19 | return res.status(500).send(err) 20 | } 21 | }) 22 | 23 | module.exports = router 24 | -------------------------------------------------------------------------------- /layouts/error.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /store/auth.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | user: { 3 | name: null, 4 | photoUrl: null, 5 | _id: null 6 | } 7 | }) 8 | 9 | //* Getters 10 | export const getters = { 11 | isLogged: state => Boolean(state.user.email), 12 | user: state => state.user 13 | } 14 | 15 | //* mutations 16 | export const mutations = { 17 | SET_USER (state, payload, field = null) { 18 | if (field) { 19 | state.user[field] = payload 20 | } else { 21 | state.user = payload.user 22 | if (!payload.user.name) { state.user.name = payload.user.email.split('@')[0] } 23 | } 24 | localStorage.setItem('user', JSON.stringify(payload)) 25 | }, 26 | LOAD_USER (state) { 27 | if (localStorage.getItem('user')) { 28 | const user = JSON.parse(localStorage.getItem('user')) 29 | state.user = { ...user.user, id: user._id } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 43 | -------------------------------------------------------------------------------- /content/hello.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | description: 'Empower your NuxtJS application with @nuxt/content module: write in a content/ directory and fetch your Markdown, JSON, YAML and CSV files through a MongoDB like API, acting as a Git-based Headless CMS.' 4 | --- 5 | 6 | Empower your NuxtJS application with `@nuxtjs/content` module: write in a `content/` directory and fetch your Markdown, JSON, YAML and CSV files through a MongoDB like API, acting as a **Git-based Headless CMS**. 7 | 8 | ## Writing content 9 | 10 | Learn how to write your `content/`, supporting Markdown, YAML, CSV and JSON: https://content.nuxtjs.org/writing. 11 | 12 | ## Fetching content 13 | 14 | Learn how to fetch your content with `$content`: https://content.nuxtjs.org/fetching. 15 | 16 | ## Displaying content 17 | 18 | Learn how to display your Markdown content with the `` component directly in your template: https://content.nuxtjs.org/displaying. -------------------------------------------------------------------------------- /pages/emails/_id.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 32 | 33 | 36 | -------------------------------------------------------------------------------- /api/controllers/excel.controller.js: -------------------------------------------------------------------------------- 1 | const readXlsxFile = require('read-excel-file/node') 2 | 3 | const excelToList = (path) => { 4 | const VALID_COLUMNS = ['email', 5 | 'email address', 6 | 'contact email', 7 | 'personal email', 8 | 'contact_email', 9 | 'personal_email', 10 | 'email_address', 11 | 'mail', 12 | 'mail id', 13 | 'mail_address', 14 | 'mail address', 15 | 'mailing address', 16 | 'mailing_address' 17 | ] 18 | return new Promise((resolve, reject) => { 19 | console.log(path) 20 | readXlsxFile(path).then((rows) => { 21 | rows[0].forEach((row, index) => { 22 | if (row !== null && typeof row !== 'undefined') { 23 | if (VALID_COLUMNS.includes(row.toLowerCase())) { 24 | resolve(rows.map(row => row[index])) 25 | } 26 | } 27 | }) 28 | reject(Error(`Invalid columns:\n one of the following should be available: ${VALID_COLUMNS.join(',')}`)) 29 | }) 30 | }) 31 | } 32 | 33 | module.exports = { excelToList } 34 | -------------------------------------------------------------------------------- /components/appbar.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 50 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const logger = require('morgan') 3 | const mongoose = require('mongoose') 4 | const cors = require('cors') 5 | const basicRouter = require('./routes/basic.routes') 6 | const templateRouter = require('./routes/template.routes') 7 | const emailRouter = require('./routes/emails.routes') 8 | const trackerRouter = require('./routes/tracker.routes') 9 | const fileManagerRouter = require('./routes/fileHandler.routes') 10 | 11 | const app = express() 12 | 13 | app.use(cors()) 14 | 15 | app.use(logger('dev')) 16 | app.use(express.urlencoded({ extended: true })) 17 | app.use(express.json()) 18 | 19 | const dbURI = process.env.dbURI || '' 20 | 21 | mongoose 22 | .connect(dbURI, { 23 | useNewUrlParser: true 24 | }) 25 | .then(() => console.log('Database Connected')) 26 | .catch(err => console.log(err)) 27 | 28 | mongoose.Promise = global.Promise 29 | 30 | app.use('/', basicRouter) 31 | app.use('/templates', templateRouter) 32 | app.use('/emails', emailRouter) 33 | app.use('/tracking', trackerRouter) 34 | app.use('/files', fileManagerRouter) 35 | 36 | export default { 37 | path: '/api', 38 | handler: app 39 | } 40 | -------------------------------------------------------------------------------- /api/controllers/bucket.controller.js: -------------------------------------------------------------------------------- 1 | // GCP bucket handler functions for asset management 2 | 3 | const { Storage } = require('@google-cloud/storage') 4 | const storage = new Storage({ keyFilename: 'bucket_key.json' }) 5 | const bucket = storage.bucket(process.env.BUCKET_NAME) 6 | 7 | const uploadFile = (filePath, userId) => { 8 | return new Promise((resolve, reject) => { 9 | const options = { 10 | destination: `${userId}/${filePath.split('/').pop().replace(' ', '+')}` 11 | } 12 | bucket.upload(filePath, options).then(resolve).catch(reject) 13 | }) 14 | } 15 | 16 | const getSignedURL = (path) => { 17 | return new Promise((resolve, reject) => { 18 | const file = bucket.file(path) 19 | const config = { 20 | action: 'read', 21 | expires: (+new Date()) + 86400000 22 | } 23 | file.getSignedUrl(config).then(resolve).catch(reject) 24 | }) 25 | } 26 | 27 | const getEmbedLink = (path, days) => { 28 | return new Promise((resolve, reject) => { 29 | const file = bucket.file(path) 30 | const config = { 31 | action: 'read', 32 | expires: (+new Date()) + (86400000 * days) 33 | } 34 | file.getSignedUrl(config).then(resolve).catch(reject) 35 | }) 36 | } 37 | 38 | module.exports = { uploadFile, getSignedURL, getEmbedLink } 39 | -------------------------------------------------------------------------------- /api/controllers/fileHandler.controller.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const dir = path.join(path.resolve(path.dirname('')), 'api') 4 | const excel = require('./excel.controller') 5 | const bucketHandler = require('./bucket.controller') 6 | 7 | const readTemplate = (file) => { 8 | return new Promise((resolve, reject) => { 9 | fs.readFile(`${dir}/upload/${file}`, 'utf8', (err, data) => { 10 | if (err) { 11 | reject(err) 12 | } 13 | fs.unlinkSync(`${dir}/upload/${file}`) 14 | resolve(data) 15 | }) 16 | }) 17 | } 18 | 19 | const readExcel = (file) => { 20 | return new Promise((resolve, reject) => { 21 | excel.excelToList(`${dir}/upload/${file}`).then((data) => { 22 | fs.unlinkSync(`${dir}/upload/${file}`) 23 | resolve(data) 24 | }).catch((err) => { 25 | fs.unlinkSync(`${dir}/upload/${file}`) 26 | reject(err) 27 | }) 28 | }) 29 | } 30 | 31 | const uploadFile = (filePath, userID) => { 32 | return new Promise((resolve, reject) => { 33 | bucketHandler.uploadFile(filePath, userID).then(resolve).catch(reject) 34 | }) 35 | } 36 | 37 | const unlinkFile = (file) => { 38 | fs.unlink(`${dir}/upload/${file}`, (_, __) => console.info) 39 | } 40 | 41 | const trackerFilePath = () => { 42 | return `${dir}/assets/trackerImage.png` 43 | } 44 | 45 | module.exports = { readTemplate, readExcel, uploadFile, unlinkFile, trackerFilePath } 46 | -------------------------------------------------------------------------------- /pages/assets/upload.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 62 | 63 | 66 | -------------------------------------------------------------------------------- /api/routes/basic.routes.js: -------------------------------------------------------------------------------- 1 | // Routes go here and 2 | 3 | const router = require('express').Router() 4 | const dbFunction = require('../controllers/db.controller') 5 | const gmail = require('../controllers/gmail.controller') 6 | 7 | router.get('/', (req, res) => { 8 | return res.send('Hello World') 9 | }) 10 | 11 | router.get('/auth', async (req, res) => { 12 | let link 13 | try { 14 | link = await gmail.getCredLink() 15 | } catch (err) { 16 | console.error(err) 17 | return res.status(500).send(err) 18 | } 19 | return res.send(link) 20 | }) 21 | 22 | router.post('/auth', async (req, res) => { 23 | const code = req.body.code 24 | let token 25 | try { 26 | token = await gmail.getToken(code) 27 | } catch (err) { 28 | console.error(err) 29 | return res.status(500).send(err) 30 | } 31 | try { 32 | const userInfo = await gmail.loadUser(JSON.stringify(token)) 33 | // console.log(userInfo) 34 | const userData = await dbFunction.getUserData(userInfo.data.emailAddress, JSON.stringify(token)) 35 | // console.log(userData) 36 | if (userData) { 37 | return res.status(200).send({ new: false, userData }) 38 | } else { 39 | const user = await dbFunction.createUser({ email: userInfo.data.emailAddress }, token) 40 | // console.log({ new: true, ...user }) 41 | return res.status(200).send({ new: true, userData: user }) 42 | } 43 | } catch (err) { 44 | console.error(err) 45 | return res.status(500).send(err) 46 | } 47 | }) 48 | 49 | module.exports = router 50 | -------------------------------------------------------------------------------- /pages/emails/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 52 | 53 | 62 | -------------------------------------------------------------------------------- /pages/assets/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 58 | 59 | 62 | -------------------------------------------------------------------------------- /api/routes/fileHandler.routes.js: -------------------------------------------------------------------------------- 1 | // file handler controller 2 | 3 | const path = require('path') 4 | const dir = path.join(path.resolve(path.dirname('')), 'api') 5 | const router = require('express').Router() 6 | const multer = require('multer') 7 | const dbFunction = require('../controllers/db.controller') 8 | const bucketHandler = require('../controllers/bucket.controller') 9 | 10 | const storage = multer.diskStorage({ 11 | destination: `${dir}/upload`, 12 | // overwriting the filename with an inline function to replace spaces to '-', making it better for URLs 13 | filename: (_, file, cb) => { 14 | cb(null, file.originalname.replace(' ', '-')) 15 | } 16 | }) 17 | 18 | const upload = multer({ 19 | storage 20 | }) 21 | 22 | router.get('/list', async (req, res) => { 23 | const userId = req.query.user 24 | const files = await dbFunction.getFileByUserId(userId) 25 | return res.status(200).send(files) 26 | }) 27 | 28 | router.post('/upload', upload.any('files'), async (req, res) => { 29 | const files = req.files 30 | const resp = await dbFunction.addFiles(req.query.user, files) 31 | return res.status(200).send(resp) 32 | }) 33 | 34 | router.get('/cdn', async (req, res) => { 35 | const path = req.query.path 36 | const resp = await bucketHandler.getSignedURL(path) 37 | return res.status(200).send(resp) 38 | }) 39 | 40 | router.get('/get-embed-link', async (req, res) => { 41 | const path = req.query.path 42 | const days = req.query.days 43 | const resp = await bucketHandler.getEmbedLink(path, days) 44 | return res.status(200).send(resp) 45 | }) 46 | 47 | module.exports = router 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "G-Notify", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build", 8 | "start": "nuxt start", 9 | "generate": "nuxt generate", 10 | "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", 11 | "lint": "npm run lint:js" 12 | }, 13 | "dependencies": { 14 | "@google-cloud/storage": "^5.18.1", 15 | "@nuxt/content": "^1.14.0", 16 | "@nuxtjs/axios": "^5.13.6", 17 | "@nuxtjs/pwa": "^3.3.5", 18 | "apexcharts": "^3.40.0", 19 | "core-js": "^3.15.1", 20 | "cors": "^2.8.5", 21 | "exceljs": "^4.3.0", 22 | "express": "^4.17.1", 23 | "express-fileupload": "^1.2.1", 24 | "gg-vue-monaco-editor": "^1.0.1", 25 | "googleapis": "^39.2.0", 26 | "handlebars": "^4.7.7", 27 | "mimetext": "^2.0.6", 28 | "mongoose": "^6.0.5", 29 | "morgan": "^1.10.0", 30 | "multer": "^1.4.3", 31 | "nuxt": "^2.15.7", 32 | "nuxt-highlightjs": "^1.0.1", 33 | "puppeteer": "^10.4.0", 34 | "read-excel-file": "^5.2.25", 35 | "uuid": "^8.3.2", 36 | "vue-apexcharts": "^1.6.2", 37 | "vue-highlightjs": "^1.3.3", 38 | "vue-lottie": "^0.2.1", 39 | "vuetify": "^2.5.5" 40 | }, 41 | "devDependencies": { 42 | "@babel/eslint-parser": "^7.14.7", 43 | "@nuxtjs/eslint-config": "^6.0.1", 44 | "@nuxtjs/eslint-module": "^3.0.2", 45 | "@nuxtjs/vuetify": "^1.12.1", 46 | "eslint": "^7.29.0", 47 | "eslint-plugin-nuxt": "^2.0.0", 48 | "eslint-plugin-vue": "^7.12.1", 49 | "vue-prism": "^1.0.5", 50 | "vue2-ace-editor": "0.0.15" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # G-notify 2 | 3 | G-notify is a mass mailer written in nuxt snd express without any third party libraries like (nodemailer). 4 | We directly contact GAPIs to send the mail. 5 | 6 | ### Deplyment Status 7 | 8 | [landing](https://g-notify-landing.netlify.app) [![Netlify Status](https://api.netlify.com/api/v1/badges/6605941a-8232-4d5b-9869-8722f91ca31d/deploy-status)](https://app.netlify.com/sites/g-notify-landing/deploys) 9 | 10 | [G-notify](https://g-notify.netlify.app) [![Netlify Status](https://api.netlify.com/api/v1/badges/fff5718f-e1d1-49ca-94e1-f9bf0b2499b4/deploy-status)](https://app.netlify.com/sites/g-notify/deploys) 11 | 12 | ** The above hosted version on netify is not supported anymore ** 13 | 14 | ## Features 15 | - [x] Template collection 16 | - [x] create template - 17 | - [x] upload template 18 | - [x] send single template 19 | - [x] send multiple template 20 | - [~] asset management for people to upload files for hosting, basically a cdn mainly for images but also supports other things (untested after new gcp configs) 21 | - [x] tracking 22 | - [ ] stats 23 | ## how to start using 24 | - create a .env file with the following variables 25 | ```bash 26 | GMAIL_CLIENT_ID="your client id" 27 | GMAIL_CLIENT_SECRET="your client secret" 28 | GMAIL_REDIRECT_URL="your redirect url" 29 | dbURI="your mongodb url" 30 | BASEURL="http://localhost:3000" 31 | BUCKET_NAME="your gcp bucket name" 32 | DOMAIN="your server domain/IP address" 33 | ``` 34 | 35 | **you'd also need a service account json named `bucket_key.json` with suitable permissions and saved in the root folder of the project** -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | 92 | # Uploads 93 | src/api/uploads/* 94 | 95 | bucket_key.json 96 | creds.json -------------------------------------------------------------------------------- /components/emailBox.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 99 | 100 | 103 | -------------------------------------------------------------------------------- /api/routes/template.routes.js: -------------------------------------------------------------------------------- 1 | // Template related routes go here 2 | const path = require('path') 3 | const dir = path.join(path.resolve(path.dirname('')), 'api') 4 | const router = require('express').Router() 5 | const multer = require('multer') 6 | const dbFunction = require('../controllers/db.controller') 7 | const puppeteerController = require('../controllers/puppeteer.controller') 8 | const fileHandler = require('../controllers/fileHandler.controller') 9 | 10 | const storage = multer.diskStorage({ 11 | destination: `${dir}/upload`, 12 | // overwriting the filename with an inline function to replace spaces to '-', making it better for URLs 13 | filename: (_, file, cb) => { 14 | cb(null, file.originalname.replace(' ', '-')) 15 | } 16 | }) 17 | 18 | const upload = multer({ 19 | storage 20 | }) 21 | 22 | router.get('/', async (req, res) => { 23 | const templates = await dbFunction.getTemplates(req.query.page) 24 | return res.status(200).send({ templates }) 25 | }) 26 | 27 | router.get('/count', async (_, res) => { 28 | const count = await dbFunction.getTemplateCount() 29 | return res.status(200).send({ count }) 30 | }) 31 | 32 | router.get('/renderTemplate', async (req, res) => { 33 | const template = await dbFunction.getTemplate(req.query.id) 34 | const image = await puppeteerController.templateScreenshot(template.content, req.query.quality) 35 | res.writeHead(200, { 36 | 'Content-Type': 'image/png', 37 | 'Content-Length': image.length 38 | }) 39 | res.end(image) 40 | }) 41 | 42 | router.post('/readTemplate', upload.single('file'), async (req, res) => { 43 | if (!req.file) { 44 | return res.status(400).send({ error: 'No file was uploaded' }) 45 | } 46 | try { 47 | const template = await fileHandler.readTemplate(req.file.path.split('/').pop()) 48 | return res.status(200).send({ template }) 49 | } catch (err) { 50 | console.error(err) 51 | return res.status(500).send({ err }) 52 | } 53 | }) 54 | 55 | router.post('/createTemplate', async (req, res) => { 56 | const data = req.body 57 | try { 58 | const template = await dbFunction.createTemplate(data) 59 | return res.status(200).send({ template }) 60 | } catch (err) { 61 | console.error(err) 62 | return res.status(500).send({ err }) 63 | } 64 | }) 65 | 66 | router.get('/:id', async (req, res) => { 67 | const template = await dbFunction.getTemplate(req.params.id) 68 | return res.status(200).send({ template }) 69 | }) 70 | 71 | router.get('/:id/like', async (req, res) => { 72 | const likes = await dbFunction.addLike(req.params.id) 73 | return res.status(200).send({ likes }) 74 | }) 75 | 76 | module.exports = router 77 | -------------------------------------------------------------------------------- /layouts/dashboard.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 103 | -------------------------------------------------------------------------------- /Instructions.md: -------------------------------------------------------------------------------- 1 | # G-Notify 2 | 3 | ## Build Setup 4 | 5 | ```bash 6 | # install dependencies 7 | $ npm install 8 | 9 | # serve with hot reload at localhost:3000 10 | $ npm run dev 11 | 12 | # build for production and launch server 13 | $ npm run build 14 | $ npm run start 15 | 16 | # generate static project 17 | $ npm run generate 18 | ``` 19 | 20 | For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org). 21 | 22 | ## Special Directories 23 | 24 | You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality. 25 | 26 | ### `assets` 27 | 28 | The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts. 29 | 30 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets). 31 | 32 | ### `components` 33 | 34 | The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components. 35 | 36 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components). 37 | 38 | ### `layouts` 39 | 40 | Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop. 41 | 42 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts). 43 | 44 | 45 | ### `pages` 46 | 47 | This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically. 48 | 49 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing). 50 | 51 | ### `plugins` 52 | 53 | The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`. 54 | 55 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins). 56 | 57 | ### `static` 58 | 59 | This directory contains your static files. Each file inside this directory is mapped to `/`. 60 | 61 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 62 | 63 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static). 64 | 65 | ### `store` 66 | 67 | This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex. 68 | 69 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store). 70 | -------------------------------------------------------------------------------- /content/rules.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Templating Rules 3 | description: HTML based email template rules. 4 | --- 5 |
6 | This reference assumes you’re a front-end web designer with an intermediate working knowledge of HTML and CSS. You’re comfortable coding in and using HTML tables for structure, and working in coding standards that were state-of-the-art in 1999. Feel free to crank up a little TLC or Goo Goo Dolls to get into the spirit of thing 7 | 8 |
9 |
10 | 11 | 12 | 13 | #### Best Practices: Design 14 | 15 | There are a few things to keep in mind when designing HTML email campaigns. 16 | 17 |
18 | 19 | - Emails should be 600-800 pixels maximum width. This will make them behave better within the preview-pane size provided by many clients. 20 | - Design for simplicity. Use grid-based layers and avoid complicated elements that require HTML floats or positioning. 21 | - Assume images will be initially blocked by email clients, or that certain images—background images, for example—will completely fail to load. 22 | - Don’t design an email that’s essentially one large, sliced-up image. While these kinds of emails look pretty, they perform poorly. 23 | - Use basic, cross-platform fonts such as Arial, Verdana, Georgia, and Times New Roman. 24 | - Avoid elements that require Flash or JavaScript. If you need motion in an email, a .gif is your best bet. 25 | - Don’t forget about the mobile experience! Is your email readable at arm’s length on a small screen? Will the images slow its load time on a mobile device? Are your links easy to press with a thumb? 26 | 27 |
28 |
29 | 30 | #### Best Practices: Development 31 | 32 | Much like with design, there are best practices to follow when coding HTML email. 33 | 34 | 35 | - Code all structure using the table element. For more complicated layouts, you should nest tables to build complex structures. 36 | - Use element attributes (such as cellpadding, valign, and width) to set ta`ble dimensions. This forces a box-model structure. 37 | - Keep your CSS simple. Avoid compound style declarations (IE: “font:#000 12px Arial, Helvetica, sans-serif;”), shorthand code (IE: #000 instead of #000000), CSS layout properties (IE: slot, position, clear, visibility, etc.), complex selectors (IE: descendant, child or sibling selectors, and pseudo-elements) 38 | - Inline all CSS before sending. (Mailchimp will do this for you automatically.) 39 | - Use only absolute links for images, and host those images on a reliable server. (Mailchimp provides free image hosting.) 40 | - Don’t bother with JavaScript or Flash—those technologies are largely unsupported by email clients. 41 | - Account for mobile-friendliness, if possible. Use media queries to increase text sizes on small screens, provide thumb-sized (~46x46px) hit areas for links. Make an email responsive if the design allows for it. 42 | - Test, test, test. Create email accounts across various services, and send emails to yourself. Do this in conjunction with services such as [Litmus](https://www.litmus.com/). 43 | 44 | [^1]: Sourece [MailChimp](https://templates.mailchimp.com/getting-started/html-email-basics/) 45 | 46 | -------------------------------------------------------------------------------- /components/fileCard.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 115 | -------------------------------------------------------------------------------- /pages/templates/index.vue: -------------------------------------------------------------------------------- 1 | 67 | 111 | 112 | 118 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 112 | 113 | 139 | -------------------------------------------------------------------------------- /pages/templates/upload.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 121 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line nuxt/no-cjs-in-config 2 | import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin' 3 | 4 | export default { 5 | // Global page headers: https://go.nuxtjs.dev/config-head 6 | head: { 7 | titleTemplate: '%s - G-Notify', 8 | title: 'G-Notify', 9 | meta: [ 10 | { charset: 'utf-8' }, 11 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 12 | { hid: 'description', name: 'description', content: '' }, 13 | { name: 'format-detection', content: 'telephone=no' } 14 | ], 15 | link: [ 16 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, 17 | { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, 18 | { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOriginIsolated: true }, 19 | { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&family=Prata&display=swap' } 20 | ] 21 | }, 22 | 23 | // router config 24 | router: { 25 | middleware: ['auth'] 26 | }, 27 | 28 | // Express server settings 29 | serverMiddleware: { 30 | '/api': '~/api' 31 | }, 32 | 33 | // Global CSS: https://go.nuxtjs.dev/config-css 34 | css: [ 35 | '@/assets/global.scss' 36 | ], 37 | 38 | // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins 39 | plugins: [{ src: '~plugins/editor', mode: 'client' }, { src: '~plugins/prisma-highlight', mode: 'client' }], 40 | 41 | // Auto import components: https://go.nuxtjs.dev/config-components 42 | components: true, 43 | 44 | // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules 45 | buildModules: [ 46 | // https://go.nuxtjs.dev/eslint 47 | '@nuxtjs/eslint-module', 48 | // https://go.nuxtjs.dev/vuetify 49 | '@nuxtjs/vuetify' 50 | ], 51 | 52 | // Modules: https://go.nuxtjs.dev/config-modules 53 | modules: [ 54 | // https://go.nuxtjs.dev/axios 55 | '@nuxtjs/axios', 56 | // https://go.nuxtjs.dev/pwa 57 | '@nuxtjs/pwa', 58 | // https://go.nuxtjs.dev/content 59 | '@nuxt/content', 60 | [ 61 | 'nuxt-highlightjs', 62 | { 63 | style: 'base16/zenburn' 64 | } 65 | ] 66 | ], 67 | 68 | // Axios module configuration: https://go.nuxtjs.dev/config-axios 69 | axios: { 70 | baseURL: process.env.BASEURL 71 | }, 72 | 73 | // PWA module configuration: https://go.nuxtjs.dev/pwa 74 | pwa: { 75 | manifest: { 76 | lang: 'en' 77 | } 78 | }, 79 | 80 | // Content module configuration: https://go.nuxtjs.dev/config-content 81 | content: {}, 82 | 83 | // Vuetify module configuration: https://go.nuxtjs.dev/config-vuetify 84 | // Toggle Theme 85 | vuetify: { 86 | customVariables: ['~/assets/variables.scss'], 87 | theme: { 88 | dark: true, 89 | themes: { 90 | dark: { 91 | primary: '#4285F4', 92 | accent: '#42A5F5', 93 | secondary: '#2196F3', 94 | info: '#81C784', 95 | warning: '#FBBC04', 96 | error: '#EA4335', 97 | success: '#34A853' 98 | }, 99 | light: { 100 | primary: '#4285F4', 101 | accent: '#42A5F5', 102 | secondary: '#2196F3', 103 | info: '#81C784', 104 | warning: '#FBBC04', 105 | error: '#EA4335', 106 | success: '#34A853' 107 | } 108 | } 109 | } 110 | }, 111 | 112 | // Build Configuration: https://go.nuxtjs.dev/config-build 113 | build: { 114 | plugins: [new MonacoWebpackPlugin()] 115 | }, 116 | 117 | watch: ['~/api/*.js', '~/api/*/*.js'] 118 | } 119 | -------------------------------------------------------------------------------- /components/embed.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 134 | 135 | 138 | -------------------------------------------------------------------------------- /api/routes/emails.routes.js: -------------------------------------------------------------------------------- 1 | // Emailing related routes go here 2 | 3 | const path = require('path') 4 | const dir = path.join(path.resolve(path.dirname('')), 'api') 5 | const router = require('express').Router() 6 | const multer = require('multer') 7 | const dbFunction = require('../controllers/db.controller') 8 | const emailFunction = require('../controllers/gmail.controller') 9 | const fileHandler = require('../controllers/fileHandler.controller') 10 | 11 | const storage = multer.diskStorage({ 12 | destination: `${dir}/upload`, 13 | // overwriting the filename with an inline function to replace spaces to '-', making it better for URLs 14 | filename: (_, file, cb) => { 15 | cb(null, file.originalname.replace(' ', '-')) 16 | } 17 | }) 18 | 19 | const upload = multer({ 20 | storage 21 | }) 22 | 23 | router.get('/sendTest', async (req, res) => { 24 | const userId = req.query.user 25 | const templateId = req.query.template 26 | const user = await dbFunction.getUserDataById(userId) 27 | // console.log(user) 28 | const template = await dbFunction.getTemplate(templateId) 29 | try { 30 | emailFunction.sendSingleMessage( 31 | JSON.stringify(user.token), 32 | template.content, 33 | user.user.email, 34 | user.user.email, 35 | 'Test Email from G-Notify', 36 | 'plainText' 37 | ) 38 | } catch (err) { 39 | // console.log(err) 40 | } 41 | res.send('Email sent') 42 | }) 43 | 44 | router.post('/importExcel', upload.single('excel'), async (req, res) => { 45 | try { 46 | const list = await fileHandler.readExcel( 47 | req.file.path 48 | .split('/') 49 | .pop() 50 | .replace(' ', '+') 51 | ) 52 | return res.status(200).send({ list }) 53 | } catch (err) { 54 | console.log(err) 55 | return res.status(500).send({ err }) 56 | } 57 | }) 58 | 59 | router.post('/sendEmails', async (req, res) => { 60 | const { templateId, subject, emails, tracking } = req.body 61 | const template = await dbFunction.getTemplate(templateId) 62 | const user = await dbFunction.getUserDataById(req.query.userId) 63 | const email = await dbFunction.createEmail( 64 | templateId, 65 | user.user.email, 66 | emails, 67 | subject 68 | ) 69 | if (tracking) { 70 | try { 71 | const promises = emails.map(emailId => 72 | emailFunction.sendSingleMessage( 73 | JSON.stringify(user.token), 74 | template.content + 75 | ``, 76 | emailId, 77 | user.email, 78 | subject, 79 | template.plainText 80 | ) 81 | ) 82 | await Promise.all(promises) 83 | return res 84 | .status(200) 85 | .send({ message: 'Emails sent with tracking', emailId: email._id }) 86 | } catch (err) { 87 | console.log(err) 88 | return res.status(500).send({ err }) 89 | } 90 | } 91 | try { 92 | await emailFunction.sendMultipleMails( 93 | JSON.stringify(user.token), 94 | template.content, 95 | emails, 96 | user.user.email, 97 | subject, 98 | template.plainText 99 | ) 100 | return res.status(200).send({ message: 'Emails sent', emailId: email._id }) 101 | } catch (err) { 102 | console.log(err) 103 | return res.status(500).send({ err }) 104 | } 105 | }) 106 | 107 | router.get('/list/:user', async (req, res) => { 108 | const emails = await dbFunction.getEmailsByUser(req.params.user) 109 | return res.status(200).send({ emails }) 110 | }) 111 | 112 | router.get('/:id', async (req, res) => { 113 | const email = await dbFunction.getEmailById(req.params.id) 114 | return res.status(200).send({ email }) 115 | }) 116 | 117 | router.get('/stats/all', async (req, res) => { 118 | const totalCount = await dbFunction.getTotalEmailStats() 119 | const readCount = await dbFunction.getReadEmailStats() 120 | return res.status(200).send({ totalCount, readCount }) 121 | }) 122 | 123 | module.exports = router 124 | -------------------------------------------------------------------------------- /components/templateCard.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 141 | 142 | 155 | -------------------------------------------------------------------------------- /pages/templates/create/_id.vue: -------------------------------------------------------------------------------- 1 |