├── .babelrc ├── .clasp.json ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── LICENSE ├── README.md ├── build ├── build.js ├── check-versions.js ├── logo.png ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js └── prod.env.js ├── gas.js ├── gas ├── api.js ├── appsscript.json ├── index.html └── main.js ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── assets │ ├── gas.svg │ └── logo.png ├── components │ ├── HelloWorld.vue │ └── UsersTable.vue ├── gas.config.js ├── main.js ├── router │ └── index.js ├── store │ ├── index.js │ └── modules │ │ ├── auth.js │ │ ├── template.js │ │ └── users.js ├── utils.js └── views │ ├── Home.vue │ ├── Signin.vue │ └── Users.vue └── static └── .gitkeep /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.clasp.json: -------------------------------------------------------------------------------- 1 | {"scriptId":"13Oy9WzAb8YHjUMph4pP78ATnRx-IbBdBzaPboRV-kte90Q0kxBjgwtpx","rootDir":"./gas"} 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /gas/vue/ 2 | /logs 3 | 4 | .DS_Store 5 | node_modules/ 6 | /dist/ 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ashton Fei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuejs-gas-template 2 | 3 | > A vuejs template for building web application with google apps script 4 | 5 | ## Web App 6 | 7 | [vuejs-gas-template-app](https://script.google.com/macros/s/AKfycbwCJGuZb5gAntCPlQYAg9TJOXOL8ZLQ3_af-LQs9JyBxEueflVwCOaoQid9wGQsyE47TQ/exec) 8 | 9 | ## Build Setup 10 | 11 | ```bash 12 | # install dependencies 13 | npm install 14 | 15 | # serve with hot reload at localhost:8080 16 | npm run dev 17 | 18 | # build for production with minification 19 | npm run build 20 | 21 | # build for production and view the bundle analyzer report 22 | npm run build --report 23 | 24 | # build for web application with apps script 25 | npm run build-gas --build apps script 26 | 27 | # create a new Google Apps Script project 28 | clasp create --title 'project-name' --type standalone --rootDir ./gas 29 | 30 | # push scripts to the new project 31 | clasp push 32 | ``` 33 | 34 | ## Screenshots 35 | 36 | ![image](https://user-images.githubusercontent.com/16481229/122939470-3a3c6300-d3a6-11eb-86e5-49a07bac96f6.png) 37 | ![image](https://user-images.githubusercontent.com/16481229/122939681-6ce65b80-d3a6-11eb-9e26-f2ac48a97789.png) 38 | ![image](https://user-images.githubusercontent.com/16481229/122939824-88e9fd00-d3a6-11eb-8f83-e586c6de21ad.png) 39 | 40 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 41 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashtonfei/vuejs-gas-template/8069fa856215699c2bd187259fc6ea3d991ba0ef/build/logo.png -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | 12 | 13 | module.exports = { 14 | context: path.resolve(__dirname, '../'), 15 | entry: { 16 | app: './src/main.js' 17 | }, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: '[name].js', 21 | publicPath: process.env.NODE_ENV === 'production' 22 | ? config.build.assetsPublicPath 23 | : config.dev.assetsPublicPath 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.vue', '.json'], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.esm.js', 29 | '@': resolve('src'), 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.vue$/, 36 | loader: 'vue-loader', 37 | options: vueLoaderConfig 38 | }, 39 | { 40 | test: /\.js$/, 41 | loader: 'babel-loader', 42 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 43 | }, 44 | { 45 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 46 | loader: 'url-loader', 47 | options: { 48 | limit: 10000, 49 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 50 | } 51 | }, 52 | { 53 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 54 | loader: 'url-loader', 55 | options: { 56 | limit: 10000, 57 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 58 | } 59 | }, 60 | { 61 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 62 | loader: 'url-loader', 63 | options: { 64 | limit: 10000, 65 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 66 | } 67 | } 68 | ] 69 | }, 70 | node: { 71 | // prevent webpack from injecting useless setImmediate polyfill because Vue 72 | // source contains it (although only uses it if it's native). 73 | setImmediate: false, 74 | // prevent webpack from injecting mocks to Node native modules 75 | // that does not make sense for the client 76 | dgram: 'empty', 77 | fs: 'empty', 78 | net: 'empty', 79 | tls: 'empty', 80 | child_process: 'empty' 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].[contenthash].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: true, 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeAttributeQuotes: true 71 | // more options: 72 | // https://github.com/kangax/html-minifier#options-quick-reference 73 | }, 74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 75 | chunksSortMode: 'dependency' 76 | }), 77 | // keep module.id stable when vendor modules does not change 78 | new webpack.HashedModuleIdsPlugin(), 79 | // enable scope hoisting 80 | new webpack.optimize.ModuleConcatenationPlugin(), 81 | // split vendor js into its own file 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'vendor', 84 | minChunks (module) { 85 | // any required modules inside node_modules are extracted to vendor 86 | return ( 87 | module.resource && 88 | /\.js$/.test(module.resource) && 89 | module.resource.indexOf( 90 | path.join(__dirname, '../node_modules') 91 | ) === 0 92 | ) 93 | } 94 | }), 95 | // extract webpack runtime and module manifest to its own file in order to 96 | // prevent vendor hash from being updated whenever app bundle is updated 97 | new webpack.optimize.CommonsChunkPlugin({ 98 | name: 'manifest', 99 | minChunks: Infinity 100 | }), 101 | // This instance extracts shared chunks from code splitted chunks and bundles them 102 | // in a separate chunk, similar to the vendor chunk 103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 104 | new webpack.optimize.CommonsChunkPlugin({ 105 | name: 'app', 106 | async: 'vendor-async', 107 | children: true, 108 | minChunks: 3 109 | }), 110 | 111 | // copy custom static assets 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.resolve(__dirname, '../static'), 115 | to: config.build.assetsSubDirectory, 116 | ignore: ['.*'] 117 | } 118 | ]) 119 | ] 120 | }) 121 | 122 | if (config.build.productionGzip) { 123 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 124 | 125 | webpackConfig.plugins.push( 126 | new CompressionWebpackPlugin({ 127 | asset: '[path].gz[query]', 128 | algorithm: 'gzip', 129 | test: new RegExp( 130 | '\\.(' + 131 | config.build.productionGzipExtensions.join('|') + 132 | ')$' 133 | ), 134 | threshold: 10240, 135 | minRatio: 0.8 136 | }) 137 | ) 138 | } 139 | 140 | if (config.build.bundleAnalyzerReport) { 141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 143 | } 144 | 145 | module.exports = webpackConfig 146 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | 24 | /** 25 | * Source Maps 26 | */ 27 | 28 | // https://webpack.js.org/configuration/devtool/#development 29 | devtool: 'cheap-module-eval-source-map', 30 | 31 | // If you have problems debugging vue-files in devtools, 32 | // set this to false - it *may* help 33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 34 | cacheBusting: true, 35 | 36 | cssSourceMap: true 37 | }, 38 | 39 | build: { 40 | // Template for index.html 41 | index: path.resolve(__dirname, '../dist/index.html'), 42 | 43 | // Paths 44 | assetsRoot: path.resolve(__dirname, '../dist'), 45 | assetsSubDirectory: 'static', 46 | assetsPublicPath: '/', 47 | 48 | /** 49 | * Source Maps 50 | */ 51 | 52 | productionSourceMap: true, 53 | // https://webpack.js.org/configuration/devtool/#production 54 | devtool: '#source-map', 55 | 56 | // Gzip off by default as many popular static hosts such as 57 | // Surge or Netlify already gzip all static assets for you. 58 | // Before setting to `true`, make sure to: 59 | // npm install --save-dev compression-webpack-plugin 60 | productionGzip: false, 61 | productionGzipExtensions: ['js', 'css'], 62 | 63 | // Run the build command with an extra argument to 64 | // View the bundle analyzer report after build finishes: 65 | // `npm run build --report` 66 | // Set to `true` or `false` to always turn it on or off 67 | bundleAnalyzerReport: process.env.npm_config_report 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /gas.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const GAS_VUE_PATH = path.join(__dirname, 'gas', 'vue') 5 | const JS_PATH = path.join(__dirname, 'dist', 'static', 'js') 6 | const CSS_PATH = path.join(__dirname, 'dist', 'static', 'css') 7 | const TYPE_JS = ".js" 8 | const TYPE_CSS = ".css" 9 | 10 | function createGasFile(filePath, newFilePath, type) { 11 | let data = fs.readFileSync(filePath, 'utf-8') 12 | if (type === TYPE_JS) { 13 | data = `` 14 | } else { 15 | data = `` 16 | } 17 | fs.writeFileSync(newFilePath, data) 18 | console.log(`[${new Date().toLocaleTimeString()}] ${newFilePath.split(path.sep).pop()} has been created.`) 19 | } 20 | 21 | try { 22 | if (!fs.existsSync(GAS_VUE_PATH)) fs.mkdirSync(GAS_VUE_PATH) 23 | console.log(`[${new Date().toLocaleTimeString()}] Creating JS Files for GAS ...`) 24 | const jsFiles = fs.readdirSync(JS_PATH) 25 | jsFiles.forEach(fileName => { 26 | if (fileName.endsWith(TYPE_JS)) { 27 | filePath = path.join(JS_PATH, fileName) 28 | const newFilePath = path.join(GAS_VUE_PATH, fileName.split('.')[0] + TYPE_JS + '.html') 29 | createGasFile(filePath, newFilePath, TYPE_JS) 30 | } 31 | }) 32 | 33 | console.log(`[${new Date().toLocaleTimeString()}] Creating CSS Files for GAS ...`) 34 | const cssFiles = fs.readdirSync(CSS_PATH) 35 | cssFiles.forEach(fileName => { 36 | if (fileName.endsWith(TYPE_CSS)) { 37 | filePath = path.join(CSS_PATH, fileName) 38 | const newFilePath = path.join(GAS_VUE_PATH, fileName.split('.')[0] + TYPE_CSS + '.html') 39 | createGasFile(filePath, newFilePath, TYPE_CSS) 40 | } 41 | }) 42 | 43 | console.info(`[${new Date().toLocaleTimeString()}] Done!`) 44 | } catch (error) { 45 | console.error(`[${new Date().toLocaleTimeString()}] Error: ${error.message}`) 46 | } 47 | -------------------------------------------------------------------------------- /gas/api.js: -------------------------------------------------------------------------------- 1 | class JWT { 2 | constructor() { } 3 | /** 4 | * @param {object} payload - an json object 5 | */ 6 | static createToken(payload) { 7 | const header = Utilities.base64EncodeWebSafe(JSON.stringify({ alg: "HS256", typ: "JWT" })) 8 | payload = Utilities.base64EncodeWebSafe(JSON.stringify(payload)) 9 | const signature = Utilities.computeHmacSha256Signature(`${header}.${payload}`, SETTINGS.SECRECT) 10 | return `${header}.${payload}.${Utilities.base64EncodeWebSafe(signature)}` 11 | } 12 | 13 | /** 14 | * @param {string} token - header.payload.signature 15 | */ 16 | static isValidToken(token) { 17 | const [header, payload, signature] = token.split(".") 18 | const validSignature = Utilities.base64EncodeWebSafe(Utilities.computeHmacSha256Signature(`${header}.${payload}`, SETTINGS.SECRECT)) 19 | return signature === validSignature 20 | } 21 | } 22 | 23 | class Session { 24 | constructor() { 25 | } 26 | 27 | static generateSessionId() { 28 | return Utilities.base64Encode(Utilities.getUuid()) 29 | } 30 | 31 | static getSession(sid) { 32 | const session = CacheService.getScriptCache().get(sid) 33 | if (session === null) return session 34 | return this.createSession(sid, session) 35 | } 36 | 37 | static createSession(key, session) { 38 | CacheService.getScriptCache().put(key, session, SETTINGS.SESSION_EXPRATION_IN_SECONDS) 39 | return session 40 | } 41 | 42 | static deleteSession(sid) { 43 | CacheService.getScriptCache().remove(sid) 44 | } 45 | } 46 | 47 | 48 | class SSDB { 49 | constructor() { } 50 | /** 51 | * @description get all items from a table 52 | * @param {sheet} table worksheet object 53 | * e.g. SpreadsheetApp.getActive().getSheetByName() 54 | * @returns an array of objects 55 | */ 56 | static getAllItems(table) { 57 | let [keys, ...values] = table.getDataRange().getValues() 58 | keys = keys.map(v => v.toString().trim()) 59 | return values.map(v => { 60 | const item = {} 61 | keys.forEach((key, i) => { 62 | if (key) item[key] = v[i] 63 | }) 64 | return item 65 | }) 66 | } 67 | 68 | /** 69 | * @description get the first match item by filters from a table 70 | * @param {sheet} table worksheet object 71 | * e.g. SpreadsheetApp.getActive().getSheetByName() 72 | * 73 | * @param {object} filters an object of key:value pairs 74 | * default is null (get all data without any filters) 75 | * e.g. {name: "Ashton Fei", role: ["admin", "staff"]} 76 | * note: value can be an array for multiple values in one column 77 | * 78 | * @returns a single item found or undefined 79 | */ 80 | static getItemByFilters(table, filters) { 81 | const items = this.getAllItems(table) 82 | return items.find(item => { 83 | const results = Object.keys(filters).map(key => { 84 | const itemValue = item[key] 85 | const filterValue = filters[key] 86 | if (Array.isArray(filterValue)) return filterValue.includes(itemValue) 87 | return itemValue === filterValue 88 | }) 89 | return !results.includes(false) 90 | }) 91 | } 92 | 93 | /** 94 | * @description get items by filters from a table 95 | * @param {sheet} table worksheet object 96 | * e.g. SpreadsheetApp.getActive().getSheetByName() 97 | * 98 | * @param {object} filters an object of key:value pairs 99 | * default is null (get all data without any filters) 100 | * e.g. {name: "Ashton Fei", role: ["admin", "staff"]} 101 | * note: value can be an array for multiple values in one column 102 | * 103 | * @returns an array of items found or empty array 104 | */ 105 | static getItemsByFilters(table, filters) { 106 | const items = this.getAllItems(table) 107 | return items.filter(item => { 108 | const results = Object.keys(filters).map(key => { 109 | const itemValue = item[key] 110 | const filterValue = filters[key] 111 | if (Array.isArray(filterValue)) return filterValue.includes(itemValue) 112 | return itemValue === filterValue 113 | }) 114 | return !results.includes(false) 115 | }) 116 | } 117 | } 118 | 119 | class API { 120 | constructor() { 121 | } 122 | /** 123 | * @param {string} name - name of the table(tab) in the database(spreadsheet) 124 | */ 125 | static getTableByName(name) { 126 | let db 127 | const response = { success: true, message: `${name} is found in the database.`, table: null } 128 | try { 129 | db = SpreadsheetApp.openById(SETTINGS.SSDB_ID) 130 | } catch (e) { 131 | response.success = false 132 | response.message = `${e.message}` 133 | return response 134 | } 135 | const table = db.getSheetByName(name) 136 | if (!table) { 137 | response.success = false 138 | response.message = `${name} was not found in database.` 139 | } else { 140 | response.table = table 141 | } 142 | return response 143 | } 144 | 145 | /** 146 | * @param {string} tableName the name of table(tab) in the database(spreadsheet) 147 | * e.g. users 148 | * @param {object} filters an object of key:value pairs 149 | * default is null (get all data without any filters) 150 | * e.g. {name: "Ashton Fei", role: ["admin", "staff"]} 151 | * note: value can be an array for multiple values in one column 152 | * @returns an array of objects or empty array 153 | */ 154 | static get(tableName, filters = null) { 155 | const response = { 156 | success: true, 157 | message: "Items have been retrieved from the database.", 158 | data: null, 159 | } 160 | const { success, message, table } = this.getTableByName(tableName) 161 | if (!table) return { ...response, success, message } 162 | if (!filters) return { ...response, data: SSDB.getAllItems(table) } 163 | return { ...response, data: SSDB.getItemsByFilters(table, filters) } 164 | } 165 | 166 | /** 167 | * @param {string} tableName the name of table(tab) in the database(spreadsheet) 168 | * e.g. users 169 | * @param {object} filters an object of key:value pairs 170 | * default is null (get all data without any filters) 171 | * e.g. {name: "Ashton Fei", role: ["admin", "staff"]} 172 | * note: value can be an array for multiple values in one column 173 | */ 174 | static post(tableName, filters = []) { 175 | const { success, message, table } = this.getTableByName(tableName) 176 | if (!table) return { success, message } 177 | return { 178 | success, 179 | message: "Items have been posted from the database.", 180 | data: table.getDataRange().getValues() 181 | } 182 | } 183 | 184 | /** 185 | * @param {string} tableName the name of table(tab) in the database(spreadsheet) 186 | * e.g. users 187 | * @param {object} filters an object of key:value pairs 188 | * default is null (get all data without any filters) 189 | * e.g. {name: "Ashton Fei", role: ["admin", "staff"]} 190 | * note: value can be an array for multiple values in one column 191 | */ 192 | static delete(tableName, filters = []) { 193 | const { success, message, table } = this.getTableByName(tableName) 194 | if (!table) return { success, message } 195 | return { 196 | success, 197 | message: "Items have been deleted from the database.", 198 | data: table.getDataRange().getValues() 199 | } 200 | } 201 | } 202 | 203 | class Auth { 204 | constructor() { 205 | 206 | } 207 | 208 | static hashPassword(password) { 209 | const signature = Utilities.computeHmacSha256Signature(password, SETTINGS.SECRECT) 210 | return Utilities.base64EncodeWebSafe(signature) 211 | } 212 | 213 | static validatePassword(password, correctHashPassword) { 214 | return this.hashPassword(password) === correctHashPassword 215 | } 216 | 217 | static validateToken(token) { 218 | return JWT.isValidToken(token) ? { 219 | success: true, 220 | message: "Token is valid.", 221 | data: JSON.parse(Utilities.newBlob(Utilities.base64DecodeWebSafe(token.split(".")[1])).getDataAsString()), 222 | token, 223 | } : { 224 | success: false, 225 | message: "Token is invalid.", 226 | data: null, 227 | token: null, 228 | } 229 | } 230 | 231 | static singin(email, password) { 232 | const response = { 233 | success: true, 234 | message: 'You are signed in successfully.', 235 | data: null 236 | } 237 | 238 | // get users by email address 239 | const { success, message, data } = API.get(SETTINGS.AUTH_TABLE_NAME, { email }) 240 | if (!success) return { ...response, success, message } 241 | 242 | // if user is not found 243 | const user = data[0] 244 | if (!user) return { ...response, success: false, message: "Your credentials are not correct." } 245 | 246 | // if password is not valid 247 | const isPasswordValid = this.validatePassword(password, user.password) 248 | if (!isPasswordValid) return { ...response, success: false, message: "Your credentials are not correct." } 249 | 250 | user.token = JWT.createToken(user) 251 | return { ...response, data: user } 252 | } 253 | 254 | static signout(token) { 255 | return { success: true, message: "You have been signed out.", data: SETTINGS.ROOT_URL } 256 | } 257 | } -------------------------------------------------------------------------------- /gas/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "America/New_York", 3 | "dependencies": {}, 4 | "exceptionLogging": "STACKDRIVER", 5 | "runtimeVersion": "V8", 6 | "webapp": { 7 | "executeAs": "USER_DEPLOYING", 8 | "access": "ANYONE_ANONYMOUS" 9 | } 10 | } -------------------------------------------------------------------------------- /gas/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vuejs-gas-template 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /gas/main.js: -------------------------------------------------------------------------------- 1 | const SETTINGS = { 2 | ROOT_URL: "https://script.google.com/macros/s/AKfycbzJX6xTOZSu_hIywmZ1_LBxouEAgUUOuNnI9DH3KV1l/dev", 3 | SECRECT: "asdflkja;lskjdfi12;lkjafjslkdf", 4 | APP_NAME: "VueJS GAS Template", 5 | SSDB_ID: "15zF38SRtW9LjFFrwHYN9-VFROPH6_cn25lqrhlHtxpg", 6 | AUTH_TABLE_NAME: 'users', 7 | SESSION_EXPRATION_IN_SECONDS: 6 * 60 * 60, // Max 6 hours, min 1s 8 | } 9 | 10 | function include(filename) { 11 | return HtmlService.createTemplateFromFile(filename).evaluate().getContent() 12 | } 13 | 14 | function getUrl() { 15 | return ScriptApp.getService().getUrl() 16 | } 17 | 18 | function doGet(e) { 19 | const htmlOuput = HtmlService.createTemplateFromFile("index.html").evaluate() 20 | htmlOuput.setTitle(SETTINGS.APP_NAME) 21 | .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) 22 | .addMetaTag("viewport", "width=device-width,initial-scale=1") 23 | return htmlOuput 24 | } 25 | 26 | const request = (method, tableName, stringify_json, token) => { 27 | let response = { success: true, message: "Your request has been done successfully.", token } 28 | if (!JWT.isValidToken(token)) return { success: false, message: "Your session is not valid anymore, please sign in again!", token: null } 29 | const data = JSON.parse(stringify_json) 30 | if (method.toUpperCase() === "GET") response = API.get(tableName, data) 31 | if (method.toUpperCase() === "POST") response = API.post(tableName, data) 32 | if (method.toUpperCase() === "DELETE") response = API.delete(tableName, data) 33 | return JSON.stringify({ ...response, token }) 34 | } 35 | 36 | const validateToken = (token) => JSON.stringify(Auth.validateToken(token)) 37 | 38 | const signout = (token) => JSON.stringify(Auth.signout(token)) 39 | 40 | const signin = (data) => { 41 | const { email, password } = JSON.parse(data) 42 | return JSON.stringify(Auth.singin(email, password)) 43 | } 44 | 45 | const createHashPassword = () => { 46 | const password = '123456' 47 | console.log(Auth.hashPassword(password)) 48 | } 49 | 50 | const test = () => { 51 | const users = API.get(SETTINGS.AUTH_TABLE_NAME, { role: 'admin' }) 52 | console.log(users) 53 | } 54 | 55 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vuejs-gas-template 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-gas-template", 3 | "version": "1.0.0", 4 | "description": "A vuejs template for building web application with google apps script", 5 | "author": "Ashton Fei ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js", 11 | "build-gas": "npm run build && node gas.js" 12 | }, 13 | "dependencies": { 14 | "vue": "^2.5.2", 15 | "vue-router": "^3.0.1", 16 | "vuex": "^3.6.2" 17 | }, 18 | "devDependencies": { 19 | "@types/google-apps-script": "^1.0.34", 20 | "autoprefixer": "^7.1.2", 21 | "babel-core": "^6.22.1", 22 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 23 | "babel-loader": "^7.1.1", 24 | "babel-plugin-syntax-jsx": "^6.18.0", 25 | "babel-plugin-transform-runtime": "^6.22.0", 26 | "babel-plugin-transform-vue-jsx": "^3.5.0", 27 | "babel-preset-env": "^1.3.2", 28 | "babel-preset-stage-2": "^6.22.0", 29 | "chalk": "^2.0.1", 30 | "clasp": "^1.0.0", 31 | "copy-webpack-plugin": "^4.0.1", 32 | "css-loader": "^0.28.0", 33 | "extract-text-webpack-plugin": "^3.0.0", 34 | "file-loader": "^1.1.4", 35 | "friendly-errors-webpack-plugin": "^1.6.1", 36 | "html-webpack-plugin": "^2.30.1", 37 | "node-notifier": "^5.1.2", 38 | "optimize-css-assets-webpack-plugin": "^3.2.0", 39 | "ora": "^1.2.0", 40 | "portfinder": "^1.0.13", 41 | "postcss-import": "^11.0.0", 42 | "postcss-loader": "^2.0.8", 43 | "postcss-url": "^7.2.1", 44 | "rimraf": "^2.6.0", 45 | "semver": "^5.3.0", 46 | "shelljs": "^0.7.6", 47 | "uglifyjs-webpack-plugin": "^1.1.1", 48 | "url-loader": "^0.5.8", 49 | "vue-loader": "^13.3.0", 50 | "vue-style-loader": "^3.0.1", 51 | "vue-template-compiler": "^2.5.2", 52 | "webpack": "^3.6.0", 53 | "webpack-bundle-analyzer": "^2.9.0", 54 | "webpack-dev-server": "^2.9.1", 55 | "webpack-merge": "^4.1.0" 56 | }, 57 | "engines": { 58 | "node": ">= 6.0.0", 59 | "npm": ">= 3.0.0" 60 | }, 61 | "browserslist": [ 62 | "> 1%", 63 | "last 2 versions", 64 | "not ie <= 8" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 42 | 43 | 83 | -------------------------------------------------------------------------------- /src/assets/gas.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashtonfei/vuejs-gas-template/8069fa856215699c2bd187259fc6ea3d991ba0ef/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/UsersTable.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | 35 | -------------------------------------------------------------------------------- /src/gas.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | debug: true, 3 | googleNotDefined: 'google is not defined', 4 | authTokenKey: "gas_jwt_token", 5 | testToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQXNodG9uIEZlaSJ9.7TIf2tNTvbGTih25no2_9Q--4zf0lTPIEGxJzNcypXU=", 6 | testUser: { 7 | id: 'test', 8 | name: "Ashton Fei", 9 | role: 'admin', 10 | email: 'test@gamil.com', 11 | token: this.testToken, 12 | } 13 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import store from './store/index' 7 | 8 | Vue.config.productionTip = false 9 | 10 | /* eslint-disable no-new */ 11 | new Vue({ 12 | el: '#app', 13 | router, 14 | store, 15 | components: { App }, 16 | template: '' 17 | }) 18 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import { getToken } from '@/utils' 4 | 5 | import Home from '@/views/Home' 6 | import Signin from '@/views/Signin' 7 | import Users from '@/views/Users' 8 | import { cat } from 'shelljs' 9 | 10 | Vue.use(Router) 11 | 12 | const router = new Router({ 13 | mode: 'hash', 14 | routes: [ 15 | 16 | { 17 | path: '/', 18 | name: 'home', 19 | component: Home, 20 | meta: { 21 | requiresAuth: true, 22 | }, 23 | }, 24 | { 25 | path: '/signin', 26 | name: 'signin', 27 | component: Signin, 28 | meta: { 29 | requiresAuth: false, 30 | }, 31 | }, 32 | { 33 | path: '/users', 34 | name: 'users', 35 | component: Users, 36 | meta: { 37 | requiresAuth: true, 38 | }, 39 | } 40 | ] 41 | }) 42 | 43 | router.beforeEach((to, from, next) => { 44 | const token = getToken(); 45 | if (token === null) { 46 | if (to.name !== 'signin') next('/signin') 47 | else next() 48 | return 49 | } 50 | try { 51 | google.script.run 52 | .withSuccessHandler((response) => { 53 | const { success, message, data } = JSON.parse(response); 54 | console.log({ success, message, data }) 55 | if (to.name !== 'signin' && !success) next('/signin') 56 | else next() 57 | }) 58 | .withFailureHandler((err) => { 59 | console.log({ error: err.message }) 60 | if (to.name !== 'signin') next('/signin') 61 | else next() 62 | }) 63 | .validateToken(token); 64 | } catch (err) { 65 | if (to.name !== 'signin' && !token) next('/signin') 66 | else next() 67 | } 68 | }) 69 | router.afterEach((to, from) => { 70 | try { 71 | const stateObject = {} 72 | const params = {} 73 | google.script.history.push(stateObject, params, to.name) 74 | } catch (err) { 75 | //pass 76 | } 77 | }) 78 | export default router -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from "vuex" 3 | import auth from './modules/auth' 4 | import users from './modules/users' 5 | 6 | Vue.use(Vuex) 7 | 8 | const store = new Vuex.Store({ 9 | modules: { 10 | auth, 11 | users, 12 | }, 13 | }) 14 | 15 | export default store -------------------------------------------------------------------------------- /src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import { TEST_USER, getToken, setToken, removeToken, runApi } from '@/utils.js' 2 | import router from '@/router' 3 | 4 | const state = () => ({ 5 | user: null, 6 | token: null, 7 | defaultUser: { 8 | id: 1, 9 | name: "Ashton Fei", 10 | gender: "Male", 11 | email: "yunjia.fei@gmail.com", 12 | role: "admin", 13 | status: "active", 14 | token: null, 15 | }, 16 | default_token: 'ashton.fei@gmail.com.password', 17 | }) 18 | 19 | const getters = { 20 | } 21 | 22 | const actions = { 23 | checkUserAuth: ({ commit, state }) => { 24 | const token = getToken(); 25 | if (token === null || token === "null") return this.$router.push("/signin"); 26 | try { 27 | runApi("validateToken", token) 28 | .then(({ success, message, data }) => { 29 | if (!success) return alert(message); 30 | this.$store.commit("user/setUser", data); 31 | }) 32 | .catch(({ success, message }) => { 33 | console.err(message); 34 | }) 35 | // google.script.run 36 | // .withSuccessHandler((response) => { 37 | // const { success, message, data } = JSON.parse(response); 38 | // if (!success) return alert(message); 39 | // this.$store.commit("user/setUser", data); 40 | // }) 41 | // .withFailureHandler((err) => { 42 | // alert(err.message); 43 | // }) 44 | // .validateToken(token); 45 | } catch (err) { 46 | this.$store.commit("user/setUser", TEST_USER); 47 | } 48 | }, 49 | signin: ({ state, commit }, { email, password }) => { 50 | try { 51 | runApi("signin", { email, password }) 52 | .then(({ success, message, data }) => { 53 | if (!success) return console.error(message) 54 | commit('setUser', data) 55 | router.push('/') 56 | }) 57 | .catch(({ message }) => { 58 | console.error(message) 59 | }) 60 | } catch (err) { 61 | if (`${email}.${password}` !== state.default_token) { 62 | alert('Your credentials are not current.') 63 | return 64 | } 65 | console.log("You are signed in.") 66 | commit('setUser', TEST_USER) 67 | router.push('/') 68 | } 69 | }, 70 | signout: ({ commit }) => { 71 | commit('setUser', null) 72 | router.push('/signin') 73 | }, 74 | } 75 | 76 | const mutations = { 77 | setUser: (state, data) => { 78 | state.user = data 79 | if (data == null) return removeToken() 80 | if (data.token) setToken(data.token) 81 | }, 82 | } 83 | 84 | export default { 85 | namespaced: true, 86 | state, 87 | getters, 88 | actions, 89 | mutations, 90 | } -------------------------------------------------------------------------------- /src/store/modules/template.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | }) 3 | 4 | const getters = {} 5 | const actions = {} 6 | const mutations = {} 7 | 8 | export default { 9 | namespaced: true, 10 | state, 11 | getters, 12 | actions, 13 | mutations, 14 | } -------------------------------------------------------------------------------- /src/store/modules/users.js: -------------------------------------------------------------------------------- 1 | import { GOOGLE_NOT_DEFINED, getToken } from '@/utils' 2 | 3 | const state = () => ({ 4 | users: [], 5 | defaultUsers: [ 6 | { 7 | id: 1, 8 | name: "Ashton 1", 9 | gender: "Male", 10 | email: "yunjia.fei@gmail.com", 11 | role: "admin", 12 | status: "active", 13 | }, 14 | { 15 | id: 2, 16 | name: "Ashton 2", 17 | gender: "Male", 18 | email: "yunjia.fei@gmail.com", 19 | role: "staff", 20 | status: "active", 21 | }, 22 | { 23 | id: 3, 24 | name: "Ashton 3", 25 | gender: "Female", 26 | email: "yunjia.fei@gmail.com", 27 | role: "manager", 28 | status: "inactive", 29 | }, 30 | ], 31 | }) 32 | 33 | const getters = { 34 | activeUsers: state => state.users.filter(v => v.status.toLowerCase() === 'active'), 35 | activeUsersCount: (getters) => getters.activeUsers.length, 36 | } 37 | 38 | const actions = { 39 | getAllUsers: ({ commit, state }) => { 40 | const token = getToken() 41 | if (token === null || token === "null") return 42 | try { 43 | google.script.run 44 | .withSuccessHandler(response => { 45 | const { success, message, data } = JSON.parse(response) 46 | if (!success) alert(message) 47 | commit('setUsers', data) 48 | }) 49 | .withFailureHandler(err => { 50 | alert(err.message) 51 | }) 52 | .request("GET", "users", '{}', token) 53 | } catch (err) { 54 | commit('setUsers', state.defaultUsers) 55 | } 56 | } 57 | } 58 | 59 | const mutations = { 60 | setUsers: (state, data) => state.users = [...data], 61 | } 62 | 63 | export default { 64 | namespaced: true, 65 | state, 66 | getters, 67 | actions, 68 | mutations, 69 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const GOOGLE_NOT_DEFINED = 'google is not defined' 2 | const AUTH_TOKEN_KEY = "gas_jwt_token" 3 | const TEST_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQXNodG9uIEZlaSJ9.7TIf2tNTvbGTih25no2_9Q--4zf0lTPIEGxJzNcypXU=" 4 | const TEST_USER = { 5 | id: 'test', 6 | name: "Ashton Fei", 7 | role: 'admin', 8 | email: 'test@gamil.com', 9 | token: TEST_TOKEN, 10 | } 11 | 12 | const getLocalItem = (key) => { 13 | try { 14 | let token = localStorage.getItem(key) 15 | return token == 'null' ? null : token 16 | } catch (err) { 17 | return null 18 | } 19 | } 20 | 21 | const setLocalItem = (key, value) => { 22 | try { 23 | return localStorage.setItem(key, value) 24 | } catch (err) { 25 | //pass 26 | } 27 | } 28 | 29 | const removeLocalItem = (key) => { 30 | try { 31 | localStorage.removeItem(key) 32 | } catch (err) { 33 | //pass 34 | } 35 | } 36 | 37 | /** 38 | * 39 | * @returns get auth token from local storage 40 | */ 41 | const getToken = () => getLocalItem(AUTH_TOKEN_KEY) 42 | /** 43 | * 44 | * @param {string} token the token provided by the server side 45 | */ 46 | const setToken = (token) => setLocalItem(AUTH_TOKEN_KEY, token) 47 | const removeToken = () => removeLocalItem(AUTH_TOKEN_KEY) 48 | 49 | 50 | /** 51 | * 52 | * @param {string} functionName the function name google.script.run.{functionName} which is created in your apps script project 53 | * @param {object} params an object of parameters 54 | */ 55 | const runApi = (functionName, params) => { 56 | if (typeof params === 'object') params = JSON.stringify(params) 57 | return new Promise((resolve, reject) => { 58 | google.script.run 59 | .withSuccessHandler(reply => resolve(JSON.parse(reply))) 60 | .withFailureHandler(err => reject({ success: false, message: err.message }))[functionName](params) 61 | }) 62 | } 63 | 64 | 65 | export { 66 | GOOGLE_NOT_DEFINED, 67 | AUTH_TOKEN_KEY, 68 | TEST_USER, 69 | TEST_TOKEN, 70 | getLocalItem, 71 | setLocalItem, 72 | removeLocalItem, 73 | getToken, 74 | setToken, 75 | removeToken, 76 | runApi 77 | } 78 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 114 | 115 | -------------------------------------------------------------------------------- /src/views/Signin.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 75 | 76 | -------------------------------------------------------------------------------- /src/views/Users.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 72 | 73 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashtonfei/vuejs-gas-template/8069fa856215699c2bd187259fc6ea3d991ba0ef/static/.gitkeep --------------------------------------------------------------------------------