Initial working version

This commit is contained in:
Logan Williams
2023-05-19 09:39:18 +02:00
parent 731d6d7d03
commit 70e6db30e1
19 changed files with 26609 additions and 4365 deletions

View File

@@ -0,0 +1,7 @@
index.html,1684481624309,77e9dacd8a347223c5beb3b555f99692ca0c4d6ca5f09b8787df763406de0c08
js/app.537ca05d.js,1684481624308,e7cc5519a0c32d10bde9da4dd8ac32a560c171f63bcec0349aa72d3b5a3f77a3
favicon.ico,1684481624308,1e71457865f706dc865b49a54a86e193818220d290b30226b6630a42faf1535d
js/app.537ca05d.js.map,1684481624309,a191afff69155541073af65e5bb1bae76bacc3b0fc07a3d5fc53947f8529f401
css/chunk-vendors.07681150.css,1684481624309,c7bd88012597ed0484687de4fc4645d327cb5b3511a983d726f5f147071c1ab9
js/chunk-vendors.18403580.js,1684481624309,4e4b4f9e77b250f08186024fa36f7b7ca76c39d9a0da0f55316638254df0c344
js/chunk-vendors.18403580.js.map,1684481624309,1fbadcea2893e3ec3db735bd837e34254ca6a2fedabbd4717fc0b64419a3dff0

5
.firebaserc Normal file
View File

@@ -0,0 +1,5 @@
{
"projects": {
"default": "bellingcat-auto-archiver-b85db"
}
}

16
firebase.json Normal file
View File

@@ -0,0 +1,16 @@
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}

21003
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,14 @@
}, },
"dependencies": { "dependencies": {
"core-js": "^3.8.3", "core-js": "^3.8.3",
"firebase": "^9.22.0",
"firebaseui": "^6.0.2",
"gapi-script": "^1.2.0",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-router": "^3.5.1" "vue-router": "^3.5.1",
"vuetify": "^2.6.15",
"vuex": "^3.6.2",
"vuex-easy-firestore": "^1.37.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.16", "@babel/core": "^7.12.16",

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head> </head>
<body> <body>
<noscript> <noscript>

View File

@@ -1,32 +1,21 @@
<template> <template>
<div id="app"> <v-app>
<nav> <NavBar />
<router-link to="/">Home</router-link> | <v-main>
<router-link to="/about">About</router-link> <router-view />
</nav> </v-main>
<router-view /> </v-app>
</div>
</template> </template>
<style> <script>
#app { import NavBar from "@/components/NavBar.vue";
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav { export default {
padding: 30px; name: "App",
} components: {
NavBar,
},
};
</script>
nav a { <style></style>
font-weight: bold;
color: #2c3e50;
}
nav a.router-link-exact-active {
color: #42b983;
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<v-card class="doc">
<v-card-title>{{ doc.name }}</v-card-title>
<v-card-text>
<div class="doc-timestamp">
Created: {{ new Date(doc.timestamp).toLocaleString() }}
</div>
<div class="doc-archived">
Last archived:
{{
doc.lastArchived
? new Date(doc.lastArchived).toLocaleString()
: "never"
}}
</div>
<div class="doc-id">ID: {{ doc.sheetId }}</div>
</v-card-text>
<v-card-actions>
<v-col>
<v-btn :href="doc.url" target="_blank"
>Open sheet
<v-icon>mdi-open-in-new</v-icon>
</v-btn>
</v-col>
<v-col>
<v-btn>Archive now</v-btn>
</v-col>
<v-dialog width="500" v-model="dialog" persistent :retain-focus="false">
<template v-slot:activator="{ on, attrs }">
<v-col class="text-right">
<v-btn color="red lighten-2" right v-bind="attrs" v-on="on"
>Stop archiving</v-btn
>
</v-col>
</template>
<v-card>
<v-card-title>Stop archiving "{{ doc.name }}"? </v-card-title>
<v-card-text>
This will stop archiving the sheet, but will not delete the sheet or
any of its data from your Google Drive.
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-btn @click="dialog = false" color="primary">Cancel</v-btn>
<v-spacer></v-spacer>
<v-btn color="red" text @click="remove"> Stop archiving </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card-actions>
</v-card>
</template>
<script>
export default {
name: "DocCard",
props: {
doc: Object,
},
data() {
return {
dialog: false,
};
},
methods: {
remove() {
this.dialog = false;
this.$store.dispatch("removeDoc", this.doc.id);
},
},
};
</script>

View File

@@ -0,0 +1,30 @@
<template>
<v-container>
<div class="text-h5 mt-5 mb-3" v-if="docs.length > 0">
Your auto-archiver documents
</div>
<v-row v-for="doc in docs" :key="doc.sheetId">
<v-col>
<DocCard :doc="doc" />
</v-col>
</v-row>
</v-container>
</template>
<script>
import DocCard from "@/components/DocCard.vue";
export default {
name: "DocList",
components: {
DocCard,
},
computed: {
docs() {
return this.$store.state.docs;
},
},
};
</script>
<style></style>

View File

@@ -1,122 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
>vue-cli documentation</a
>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
target="_blank"
rel="noopener"
>babel</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
target="_blank"
rel="noopener"
>router</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
target="_blank"
rel="noopener"
>eslint</a
>
</li>
</ul>
<h3>Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
>Forum</a
>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
>Community Chat</a
>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
>Twitter</a
>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
>vue-router</a
>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank"
rel="noopener"
>vue-devtools</a
>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vue-loader</a
>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
rel="noopener"
>awesome-vue</a
>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

28
src/components/NavBar.vue Normal file
View File

@@ -0,0 +1,28 @@
<template>
<v-app-bar style="flex-grow: 0" class="text-no-wrap">
<v-toolbar-title>Bellingcat auto archiver setup tool</v-toolbar-title>
<v-spacer></v-spacer>
<v-col class="nav-wrapper">
<v-btn v-if="!user" @click="$store.dispatch('signin')">Sign In</v-btn>
<span class="user" v-if="user">
{{ user.email }}
</span>
<v-btn v-if="user" href="#" @click="$store.dispatch('signout')"
>Sign Out</v-btn
>
</v-col>
</v-app-bar>
</template>
<script>
export default {
name: "NavBar",
computed: {
user() {
return this.$store.state.user;
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,49 @@
<template>
<div
class="card horizontal"
style="max-width: 400px; margin: 0 auto"
v-if="user"
>
<div class="card-image" style="margin-top: 25px; margin-left: 10px">
<img
:src="user.photoURL"
style="
width: 75px;
height: 75px;
border-radius: 50%;
border: 4px solid #333;
"
/>
</div>
<div class="card-stacked">
<div class="card-content">
<p>
name: <strong>{{ user.displayName }}</strong
><br />email:<strong>{{ user.email }}</strong
><br />uid: <strong>{{ user.uid }}</strong> <br />provider:
<strong class="teal-text">{{
user.providerData[0].providerId
}}</strong>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "ProfileView",
data() {
return {
items: [],
};
},
computed: {
user() {
return this.$store.state.user;
},
},
};
</script>
<style></style>

20
src/firebase.js Normal file
View File

@@ -0,0 +1,20 @@
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "AIzaSyBEawXAq9pajlVKQtLopWyd_ELDwoUlbDo",
authDomain: "bellingcat-auto-archiver-b85db.firebaseapp.com",
projectId: "bellingcat-auto-archiver-b85db",
storageBucket: "bellingcat-auto-archiver-b85db.appspot.com",
messagingSenderId: "406209235111",
appId: "1:406209235111:web:f27327bed2db7295a43382",
};
const firebaseApp = initializeApp(firebaseConfig);
const firebaseAuth = getAuth(firebaseApp);
const firebaseFirestore = getFirestore(firebaseApp);
export { firebaseApp, firebaseAuth, firebaseFirestore };

21
src/gapi.js Normal file
View File

@@ -0,0 +1,21 @@
import { gapi } from "gapi-script";
// import { GoogleAuthProvider, signInWithCredential } from "firebase/auth";
// import { firebaseAuth } from "@/firebase.js";
gapi.load("client:auth2", async () => {
gapi.client.init({
apiKey: "AIzaSyBEawXAq9pajlVKQtLopWyd_ELDwoUlbDo",
clientId:
"406209235111-r1mpkvkfaqc2jg5iqbvffl2b0rf4clbo.apps.googleusercontent.com",
scope: "https://www.googleapis.com/auth/drive.file",
discoveryDocs: [
"https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
"https://sheets.googleapis.com/$discovery/rest?version=v4",
],
});
gapi.client.load("drive", "v3", () => {});
gapi.client.load("sheets", "v4", () => {});
});
export { gapi };

View File

@@ -1,10 +1,17 @@
import Vue from "vue"; import Vue from "vue";
import Vuetify from "vuetify";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
import store from "./store";
import "vuetify/dist/vuetify.min.css";
Vue.use(Vuetify);
Vue.config.productionTip = false; Vue.config.productionTip = false;
new Vue({ new Vue({
router, router,
store,
vuetify: new Vuetify(),
render: (h) => h(App), render: (h) => h(App),
}).$mount("#app"); }).$mount("#app");

View File

@@ -10,15 +10,6 @@ const routes = [
name: "home", name: "home",
component: HomeView, component: HomeView,
}, },
{
path: "/about",
name: "about",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
},
]; ];
const router = new VueRouter({ const router = new VueRouter({

282
src/store/index.js Normal file
View File

@@ -0,0 +1,282 @@
import Vue from "vue";
import Vuex from "vuex";
/* eslint-disable */
// eslint-disable-next-line
import { gapi, client } from "@/gapi";
import {
signOut,
GoogleAuthProvider,
signInWithCredential,
} from "firebase/auth";
import {
collection,
addDoc,
query,
where,
getDocs,
doc,
deleteDoc,
} from "firebase/firestore";
import { firebaseAuth, firebaseFirestore } from "@/firebase.js";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
user: null,
docs: [],
loading: false,
},
mutations: {
setUser(state, user) {
state.user = user;
},
setDocs(state, docs) {
state.docs = docs;
},
setLoading(state, loading) {
state.loading = loading;
},
},
actions: {
async signin({ commit, dispatch }) {
async function callback(tokenResponse) {
let access_token = tokenResponse.access_token;
const credential = GoogleAuthProvider.credential(null, access_token);
const response = await signInWithCredential(firebaseAuth, credential);
commit("setUser", response.user);
dispatch("getDocs");
}
commit("setUser", null);
// eslint-disable-next-line
const client = google.accounts.oauth2.initTokenClient({
client_id:
"406209235111-r1mpkvkfaqc2jg5iqbvffl2b0rf4clbo.apps.googleusercontent.com",
scope: "https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/userinfo.profile",
callback,
});
client.requestAccessToken();
},
async signout({ commit }) {
console.log("sign out");
try {
await gapi.auth2.getAuthInstance().signOut();
console.log("User is signed out from gapi.");
await signOut(firebaseAuth);
console.log("User is signed out from firebase.");
// clean user from store
commit("setUser", null);
commit("setDocs", []);
} catch (error) {
console.error("signOutUser (firebase/auth.js): ", error);
}
},
async getDocs({ state, commit }) {
try {
// get documents where uid matches user
const q = query(
collection(firebaseFirestore, "sheets"),
where("uid", "==", state.user.uid)
);
const response = await getDocs(q);
const docs = response.docs.map((d) => ({ id: d.id, ...d.data() }));
commit("setDocs", docs);
commit("setLoading", false);
} catch (error) {
console.error("getDocs (firebase.js): ", error);
}
},
async removeDoc({ dispatch }, id) {
try {
await deleteDoc(doc(firebaseFirestore, "sheets", id));
dispatch("getDocs");
} catch (error) {
console.error("removeDocs (firebase.js): ", error);
}
},
async add({ state, dispatch, commit }, { name }) {
commit("setLoading", true);
try {
// create new sheet
const newSheet = await gapi.client.sheets.spreadsheets.create({
properties: {
title: name,
},
});
const spreadsheetId = newSheet.result.spreadsheetId;
const userEnteredFormat = {
textFormat: {
bold: true,
},
};
// add header row
await gapi.client.sheets.spreadsheets.batchUpdate(
{
spreadsheetId: spreadsheetId,
},
{
requests: [
{
updateCells: {
rows: [
{
values: [
{
userEnteredValue: {
stringValue: "Link",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Archive status",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Destination folder",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Archive location",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Archive date",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Thumbnail",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Upload timestamp",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Upload title",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Textual content",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Screenshot",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Hash",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "WACZ",
},
userEnteredFormat,
},
{
userEnteredValue: {
stringValue: "Replaywebpage",
},
userEnteredFormat,
},
],
},
],
fields:
"userEnteredValue.stringValue,userEnteredFormat.textFormat.bold",
start: {
sheetId: 0,
rowIndex: 0,
columnIndex: 0,
},
},
},
{
addProtectedRange: {
protectedRange: {
range: {
sheetId: 0,
startRowIndex: 0,
endRowIndex: 1,
startColumnIndex: 0,
endColumnIndex: 13,
},
description:
"Protecting header row (needed for auto-archiver)",
warningOnly: true,
},
},
},
],
}
);
// add permissions
await gapi.client.drive.permissions.create({
fileId: spreadsheetId,
resource: {
role: "writer",
type: "user",
emailAddress:
"test-auto-archiver@bc-auto-archiver.iam.gserviceaccount.com",
},
});
const col = await collection(firebaseFirestore, "sheets");
await addDoc(col, {
sheetId: spreadsheetId,
url: newSheet.result.spreadsheetUrl,
timestamp: Date.now(),
uid: state.user.uid,
lastArchived: null,
name: name,
});
dispatch("getDocs");
} catch (error) {
console.error("add (firebase.js): ", error);
}
},
},
});

View File

@@ -1,18 +1,64 @@
<template> <template>
<div class="home"> <v-container v-if="user">
<img alt="Vue logo" src="../assets/logo.png" /> <v-row>
<HelloWorld msg="Welcome to Your Vue.js App" /> <v-col>
</div> <v-card>
<v-card-title>Create a new auto archiver sheet</v-card-title>
<v-card-text>
<p>
This tool will configure a Google Sheet on your account for use
with Bellingcat's auto archiver. This sheet will be shared with
the service account necessary for Bellingcat's archiving server.
You can modify and share the Google Sheet subsequently, but do not
edit the column names in the header row or remove the service
account from the shared users. For more information about the auto
archiver and how to use it, see
<a href="https://github.com/bellingcat/auto-archiver"
>our Github repository</a
>
and the
<a
href="https://www.bellingcat.com/resources/2022/09/22/preserve-vital-online-content-with-bellingcats-auto-archiver-tool/"
>associated article</a
>.
</p>
<v-text-field
label="Document name"
v-model="docName"
></v-text-field>
<v-btn
@click="$store.dispatch('add', { name: docName })"
:loading="$store.state.loading"
>Add</v-btn
>
</v-card-text>
</v-card>
</v-col>
</v-row>
<DocList />
</v-container>
<v-container v-else>
<v-alert type="error">Sign in to set up an auto archiver</v-alert>
</v-container>
</template> </template>
<script> <script>
// @ is an alias to /src import DocList from "@/components/DocList.vue";
import HelloWorld from "@/components/HelloWorld.vue";
export default { export default {
name: "HomeView", name: "HomeView",
components: { components: {
HelloWorld, DocList,
},
data() {
return {
docName: "Auto archiver sheet",
};
},
computed: {
user() {
return this.$store.state.user;
},
}, },
}; };
</script> </script>

9187
yarn.lock

File diff suppressed because it is too large Load Diff