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

@@ -1,32 +1,21 @@
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view />
</div>
<v-app>
<NavBar />
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
<script>
import NavBar from "@/components/NavBar.vue";
nav {
padding: 30px;
}
export default {
name: "App",
components: {
NavBar,
},
};
</script>
nav a {
font-weight: bold;
color: #2c3e50;
}
nav a.router-link-exact-active {
color: #42b983;
}
</style>
<style></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 Vuetify from "vuetify";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "vuetify/dist/vuetify.min.css";
Vue.use(Vuetify);
Vue.config.productionTip = false;
new Vue({
router,
store,
vuetify: new Vuetify(),
render: (h) => h(App),
}).$mount("#app");

View File

@@ -10,15 +10,6 @@ const routes = [
name: "home",
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({

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>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js App" />
</div>
<v-container v-if="user">
<v-row>
<v-col>
<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>
<script>
// @ is an alias to /src
import HelloWorld from "@/components/HelloWorld.vue";
import DocList from "@/components/DocList.vue";
export default {
name: "HomeView",
components: {
HelloWorld,
DocList,
},
data() {
return {
docName: "Auto archiver sheet",
};
},
computed: {
user() {
return this.$store.state.user;
},
},
};
</script>