├── static
└── .gitkeep
├── src
├── components
│ ├── HelloWorld.vue
│ └── UsersTable.vue
├── assets
│ ├── logo.png
│ └── gas.svg
├── store
│ ├── modules
│ │ ├── template.js
│ │ ├── users.js
│ │ └── auth.js
│ └── index.js
├── gas.config.js
├── main.js
├── views
│ ├── Users.vue
│ ├── Signin.vue
│ └── Home.vue
├── App.vue
├── utils.js
└── router
│ └── index.js
├── config
├── prod.env.js
├── dev.env.js
└── index.js
├── .clasp.json
├── .editorconfig
├── .gitignore
├── gas
├── appsscript.json
├── index.html
├── main.js
└── api.js
├── .babelrc
├── .postcssrc.js
├── index.html
├── LICENSE
├── README.md
├── gas.js
└── package.json
/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"'
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonfei/vuejs-gas-template/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/.clasp.json:
--------------------------------------------------------------------------------
1 | {"scriptId":"13Oy9WzAb8YHjUMph4pP78ATnRx-IbBdBzaPboRV-kte90Q0kxBjgwtpx","rootDir":"./gas"}
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vuejs-gas-template
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/gas/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | vuejs-gas-template
9 | != include('vue/app.css') ?>
10 |
11 |
12 |
13 |
14 | != include('vue/manifest.js') ?>
15 | != include('vue/vendor.js') ?>
16 | != include('vue/app.js') ?>
17 |
18 |
19 |
--------------------------------------------------------------------------------
/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/components/UsersTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ text }}
5 |
6 | Loading...
7 |
8 |
9 | {{ user[key] }}
10 |
11 |
12 |
13 |
14 |
15 |
34 |
35 |
--------------------------------------------------------------------------------
/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 | 
37 | 
38 | 
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 |
--------------------------------------------------------------------------------
/src/views/Users.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Home
7 |
11 | Users
12 |
13 |
14 |
15 |
16 |
17 |
18 |
72 |
73 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
42 |
43 |
83 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/gas.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/views/Signin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
37 |
38 |
39 |
40 |
75 |
76 |
--------------------------------------------------------------------------------
/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/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Home
6 |
7 |
11 | Users
12 |
13 |
14 |
Hello, {{ user.name }}, you are logged in as {{ user.role }}. Click to
16 | Sign out .
18 |
19 |
20 |
21 |
22 |
23 |
24 |
{{ msg }}
25 |
Essential Links
26 |
46 |
Ecosystem
47 |
65 |
Google Apps Script
66 |
67 | Apps Script
70 |
71 |
72 | Web Apps
77 |
78 |
Ashton Fei
79 |
80 | YouTube
81 |
82 |
83 | Twitter
84 |
85 |
86 | Github
87 |
88 |
89 |
90 |
91 |
92 |
114 |
115 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------