mirror of
https://github.com/bellingcat/auto-archiver-setup-tool.git
synced 2026-06-08 03:28:37 +03:00
introduces 1st batch of permissions and usage quota control
This commit is contained in:
18
src/App.vue
18
src/App.vue
@@ -4,7 +4,7 @@
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
<v-footer class="text-center">
|
||||
<v-footer class="text-center py-6">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="legal py-2">
|
||||
@@ -13,13 +13,14 @@
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
This tool uses <a href="https://github.com/bellingcat/auto-archiver">Bellingcat's Auto Archiver</a> to
|
||||
archive online content.
|
||||
<br/>For more information see
|
||||
<a href="https://github.com/bellingcat/auto-archiver">our Github repository</a>
|
||||
and the <a
|
||||
You can deploy your own version of this tool by hosting the <a href="https://github.com/bellingcat/auto-archiver-api">API</a> and the <a href="https://github.com/bellingcat/auto-archiver-api">UI</a>.
|
||||
<br/>
|
||||
This tool uses <a href="https://github.com/bellingcat/auto-archiver">Bellingcat's Auto Archiver</a> under the hood to archive online content.
|
||||
<br/>
|
||||
For more information about it see <a
|
||||
href="https://www.bellingcat.com/resources/2022/09/22/preserve-vital-online-content-with-bellingcats-auto-archiver-tool/">associated
|
||||
article</a>.
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -70,10 +71,13 @@ html {
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
footer, .v-footer, .v-footer div {
|
||||
footer,
|
||||
.v-footer,
|
||||
.v-footer div {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.v-footer {
|
||||
max-height: 125px;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<v-select v-model="group" label="Group" :items="availableGroups" required density="comfortable"></v-select>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="6" class="ma-0 py-0">
|
||||
<v-select v-model="frequency" label="Archive frequency" :items="availableFrequencies" required
|
||||
density="comfortable"></v-select>
|
||||
<v-select v-model="frequency" label="Archive frequency" :items="availableFrequencies"
|
||||
:disabled="!availableFrequencies?.length" required density="comfortable"></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" class="text-right pt-0">
|
||||
<small v-if="spreadsheetId">Detected Spreadsheet id: <code>{{ spreadsheetId }}</code></small>
|
||||
@@ -30,6 +30,28 @@
|
||||
<v-btn v-if="!actionIsCreate" color="teal" size="large" :disabled="!requiredDataExisting"
|
||||
@click="addExistingSheet">Add Existing Sheet</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" class="pt-0" v-if="group != 'please select'">
|
||||
<span>
|
||||
Quota and rules for group <code>{{ group }}</code>:
|
||||
<ul>
|
||||
<li>
|
||||
Active sheets:
|
||||
<strong>{{ groupUsage.total_sheets || 0 }}</strong> out of
|
||||
<strong>{{ displayPermissionValue(groupPermissions?.max_sheets, "")}}</strong>
|
||||
<v-chip v-if="maxedOutGroupQuota" label class="ml-2" color="red" density="comfortable" size="small">maxed out</v-chip>
|
||||
</li>
|
||||
<li>Monthly URLs: <strong>{{ groupUsage.monthly_urls || 0 }}</strong> out of <strong>{{
|
||||
displayPermissionValue(groupPermissions?.max_monthly_urls, " URLs") }}</strong></li>
|
||||
<li>Monthly MBs: <strong>{{ groupUsage.monthly_mbs || 0 }}</strong> out of <strong>{{
|
||||
displayPermissionValue(groupPermissions?.max_monthly_mbs, " MBs") }}</strong></li>
|
||||
<li>How long will we store these archives: <strong>{{
|
||||
displayPermissionValue(groupPermissions?.max_archive_lifespan_months, " months") }}</strong>
|
||||
</li>
|
||||
<li>You <strong>{{ groupPermissions?.manually_trigger_sheet?"can":"cannot" }}</strong> manually trigger sheeets in this group. </li>
|
||||
</ul>
|
||||
</span>
|
||||
</v-col>
|
||||
|
||||
|
||||
</v-row>
|
||||
|
||||
@@ -63,8 +85,7 @@ export default {
|
||||
|
||||
group: "please select",
|
||||
|
||||
availableFrequencies: ["daily", "hourly"].map(f => ({ title: f, value: f })),
|
||||
frequency: "daily",
|
||||
frequency: "please select group",
|
||||
|
||||
newSheetId: "",
|
||||
};
|
||||
@@ -74,14 +95,27 @@ export default {
|
||||
return this.$store.state.user;
|
||||
},
|
||||
requiredData() {
|
||||
return this.sheetName && this.availableGroups?.some(g => g.value === this.group) && this.availableFrequencies?.some(f => f.value === this.frequency);
|
||||
return this.sheetName && this.availableGroups?.some(g => g.value === this.group) && this.availableFrequencies?.some(f => f === this.frequency) && !this.maxedOutGroupQuota;
|
||||
},
|
||||
requiredDataExisting() {
|
||||
return this.sheetName && this.spreadsheetId && this.availableGroups?.some(g => g.value === this.group) && this.availableFrequencies?.some(f => f.value === this.frequency);
|
||||
return this.sheetName && this.spreadsheetId && this.availableGroups?.some(g => g.value === this.group) && this.availableFrequencies?.some(f => f === this.frequency) && !this.maxedOutGroupQuota;
|
||||
},
|
||||
availableGroups() {
|
||||
return (this.$store.state.user?.groups || []).map(g => ({ title: g, value: g }));
|
||||
},
|
||||
availableFrequencies() {
|
||||
return this.$store.state.user?.permissions?.[this.group]?.sheet_frequency || [];
|
||||
},
|
||||
groupPermissions() {
|
||||
return this.$store.state.user?.permissions?.[this.group] || {};
|
||||
},
|
||||
groupUsage() {
|
||||
return this.$store.state.user?.usage?.["groups"]?.[this.group] || {};
|
||||
},
|
||||
maxedOutGroupQuota(){
|
||||
if (this.groupPermissions.max_sheets === -1) return false;
|
||||
return this.groupUsage.total_sheets >= this.groupPermissions.max_sheets;
|
||||
},
|
||||
spreadsheetId() {
|
||||
if (
|
||||
this.sheetUrlId.startsWith("http") &&
|
||||
@@ -107,6 +141,7 @@ export default {
|
||||
this.loading = true;
|
||||
this.newSheetId = "";
|
||||
this.$store.dispatch("createSheet", this.sheetName).then((res) => {
|
||||
this.$store.dispatch("checkUserUsage");
|
||||
if (!res.success) throw new Error(res.result);
|
||||
this.newSheetId = res.result;
|
||||
this.addSheetToAPI(this.newSheetId);
|
||||
@@ -140,6 +175,7 @@ export default {
|
||||
if (response.status === 201) {
|
||||
this.showSnackbar(`Sheet created successfully!`, "green");
|
||||
this.$store.dispatch("getSheets");
|
||||
this.$store.dispatch("checkUserUsage");
|
||||
} else {
|
||||
throw new Error(JSON.stringify(j));
|
||||
}
|
||||
@@ -152,6 +188,10 @@ export default {
|
||||
this.sheetUrlId = "";
|
||||
this.group = "please select";
|
||||
});
|
||||
},
|
||||
displayPermissionValue(value, extraWord) {
|
||||
if (value === undefined) { return "not set"; }
|
||||
return value == -1 ? "no limit" : value + extraWord;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-container class="pane" v-if="user?.active" >
|
||||
<v-container class="pane" v-if="user?.active">
|
||||
<v-card :loading="loadingArchive">
|
||||
<v-card-title class="text-center">
|
||||
Archive a single URL
|
||||
@@ -21,7 +21,7 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" class="text-right">
|
||||
<v-btn @click="archiveUrl" color="teal"
|
||||
:disabled="!validUrl || loadingArchive || (!public && group == -1)">
|
||||
:disabled="!validUrl || loadingArchive || (!public && group == -1) || maxedOutMBs || maxedOutURLs">
|
||||
Archive
|
||||
</v-btn>
|
||||
</v-col>
|
||||
@@ -41,11 +41,37 @@
|
||||
</v-alert>
|
||||
<p v-if="validUrl">
|
||||
You can <strong v-if="archiveFailure">still</strong> <router-link
|
||||
:to="`/archives?url=${encodeURIComponent(url)}`" target="_blank"><v-icon>mdi-open-in-new</v-icon> search for
|
||||
:to="`/archives?url=${encodeURIComponent(url)}`" target="_blank"><v-icon>mdi-open-in-new</v-icon> search
|
||||
for
|
||||
archives</router-link> of
|
||||
this URL.
|
||||
</p>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" class="pt-0">
|
||||
<span>
|
||||
Quota and rules<span v-if="group != ''"> for group <code>{{ group }}</code></span>:
|
||||
<ul>
|
||||
<li>
|
||||
Monthly URLs: <strong>{{ groupUsage.monthly_urls || 0 }}</strong>
|
||||
out of
|
||||
<strong>{{ displayPermissionValue(groupPermissions?.max_monthly_urls, " URLs") }}</strong>
|
||||
<v-chip v-if="maxedOutURLs" label class="ml-2" color="red" density="comfortable" size="small">maxed
|
||||
out</v-chip>
|
||||
</li>
|
||||
<li>
|
||||
Monthly MBs:
|
||||
<strong>{{ groupUsage.monthly_mbs || 0 }}</strong>
|
||||
out of
|
||||
<strong>{{ displayPermissionValue(groupPermissions?.max_monthly_mbs, " MBs") }}</strong>
|
||||
<v-chip v-if="maxedOutMBs" label class="ml-2" color="red" density="comfortable" size="small">maxed
|
||||
out</v-chip>
|
||||
</li>
|
||||
<li>How long will we store these archives: <strong>{{
|
||||
displayPermissionValue(groupPermissions?.max_archive_lifespan_months, " months") }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -93,6 +119,29 @@ export default {
|
||||
validUrl() {
|
||||
return this.url && this.urlValidator(this.url) === true;
|
||||
},
|
||||
globalUsage() {
|
||||
return this.$store.state.user?.usage || {};
|
||||
},
|
||||
groupUsage() {
|
||||
if (this.group == "") {
|
||||
return this.$store.state.user?.usage || {};
|
||||
}
|
||||
return this.$store.state.user?.usage?.["groups"]?.[this.group] || {};
|
||||
},
|
||||
groupPermissions() {
|
||||
if (this.group == "") {
|
||||
return this.$store.state.user?.permissions?.["all"] || {};
|
||||
}
|
||||
return this.$store.state.user?.permissions?.[this.group] || {};
|
||||
},
|
||||
maxedOutMBs() {
|
||||
if (this.groupPermissions.max_monthly_mbs === -1) return false;
|
||||
return this.groupUsage.monthly_mbs >= this.groupPermissions.max_monthly_mbs;
|
||||
},
|
||||
maxedOutURLs() {
|
||||
if (this.groupPermissions.max_monthly_urls === -1) return false;
|
||||
return this.groupUsage.monthly_urls >= this.groupPermissions.max_monthly_urls;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
url(val) {
|
||||
@@ -178,6 +227,10 @@ export default {
|
||||
};
|
||||
poll();
|
||||
},
|
||||
displayPermissionValue(value, extraWord) {
|
||||
if (value === undefined) { return "not set"; }
|
||||
return value == -1 ? "no limit" : value + extraWord;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -10,7 +10,7 @@
|
||||
<v-data-table :headers="headers" item-key="name" no-data-text="No Active Sheets available" :items="items"
|
||||
:loading="loading" items-per-page="25" hover>
|
||||
<template v-slot:item.actions="{ item: data }">
|
||||
<v-btn color="teal-lighten-1" size="small" icon class="mx-2" :disabled="loading" rounded
|
||||
<v-btn :disabled="!canArchiveNow(data.group_id) || loading" color="teal-lighten-1" size="small" icon class="mx-2" rounded
|
||||
@click="archiveSheetNow(data.id)"><v-icon>mdi-archive-outline</v-icon>
|
||||
|
||||
<v-tooltip activator="parent" location="left">Archive Now!</v-tooltip>
|
||||
@@ -21,7 +21,7 @@
|
||||
<v-tooltip activator="parent" location="left">Open in new tab</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn color="red-lighten-2" size="small" icon class="mx-2" :disabled="loading" rounded
|
||||
@click="removeSheet(data.id)"><v-icon>mdi-delete-outline</v-icon>
|
||||
@click="removeSheet(data.id)"><v-icon>mdi-stop</v-icon>
|
||||
<v-tooltip activator="parent" location="left">Stop archiving, does not delete the spreadsheet
|
||||
itself.</v-tooltip>
|
||||
</v-btn>
|
||||
@@ -30,7 +30,8 @@
|
||||
<strong :title="data.id">{{ data.name }}</strong>
|
||||
</template>
|
||||
<template v-slot:item.frequency="{ item: data }">
|
||||
<v-chip :color="data.frequency=='daily'?'teal-darken-3':'orange-darken-3'" class="bg-white" prepend-icon="mdi-archive-clock-outline" variant="outlined">
|
||||
<v-chip :color="data.frequency == 'daily' ? 'teal-darken-3' : 'orange-darken-3'" class="bg-white"
|
||||
prepend-icon="mdi-archive-clock-outline" variant="outlined">
|
||||
{{ data.frequency }}
|
||||
</v-chip>
|
||||
</template>
|
||||
@@ -63,7 +64,7 @@ export default {
|
||||
{ title: "Group", value: "group_id", sortable: true },
|
||||
{ title: "Frequency", value: "frequency", sortable: true },
|
||||
{ title: "Created", value: "created_at", sortable: true },
|
||||
{ title: "Last Archived", value: "last_archived_at", sortable: true },
|
||||
{ title: "Last URL Archived", value: "last_url_archived_at", sortable: true },
|
||||
{ title: 'Actions', value: "actions", align: 'center' },
|
||||
],
|
||||
|
||||
@@ -83,6 +84,9 @@ export default {
|
||||
this.snackbarColor = color;
|
||||
this.snackbar = true;
|
||||
},
|
||||
canArchiveNow(group_id) {
|
||||
return this.$store.state.user?.permissions?.[group_id]?.manually_trigger_sheet || false;
|
||||
},
|
||||
archiveSheetNow(sheetId) {
|
||||
this.loading = true;
|
||||
fetch(`${this.$store.state.API_ENDPOINT}/sheet/${sheetId}/archive`, {
|
||||
@@ -119,6 +123,7 @@ export default {
|
||||
if (response.status === 200 && res.deleted) {
|
||||
this.showSnackbar(`Sheet ${sheetId} has been removed!`, "green");
|
||||
this.$store.dispatch("getSheets");
|
||||
this.$store.dispatch("checkUserUsage");
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export default createStore({
|
||||
sheets: [],
|
||||
loading: false,
|
||||
errorMessage: "",
|
||||
// # TODO: reenable production API endpoint
|
||||
// API_ENDPOINT: "https://auto-archiver-api.bellingcat.com"
|
||||
API_ENDPOINT: "http://localhost:8004"
|
||||
},
|
||||
@@ -56,8 +57,13 @@ export default createStore({
|
||||
setUserActiveState(state, active) {
|
||||
state.user.active = active;
|
||||
},
|
||||
setUserGroups(state, groups) {
|
||||
state.user.groups = groups;
|
||||
setUserPermissions(state, permissions) {
|
||||
state.user.permissions = permissions;
|
||||
state.user.groups = Object.keys(permissions).filter(key => key !== "all");
|
||||
saveToLocalStorage(state);
|
||||
},
|
||||
setUserUsage(state, usage) {
|
||||
state.user.usage = usage;
|
||||
saveToLocalStorage(state);
|
||||
},
|
||||
setSheets(state, sheets) {
|
||||
@@ -85,7 +91,8 @@ export default createStore({
|
||||
|
||||
commit("setUser", response.user);
|
||||
dispatch("checkActiveUser");
|
||||
dispatch("checkUserGroups");
|
||||
dispatch("checkUserPermissions");
|
||||
dispatch("checkUserUsage");
|
||||
}
|
||||
|
||||
commit("setUser", null);
|
||||
@@ -147,11 +154,11 @@ export default createStore({
|
||||
}
|
||||
},
|
||||
|
||||
async checkUserGroups({ state, commit }) {
|
||||
async checkUserPermissions({ state, commit }) {
|
||||
try {
|
||||
commit("setErrorMessage", "");
|
||||
const r = await fetch(
|
||||
`${state.API_ENDPOINT}/groups`,
|
||||
`${state.API_ENDPOINT}/user/permissions`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -161,10 +168,30 @@ export default createStore({
|
||||
}
|
||||
);
|
||||
const response = await r.json();
|
||||
commit("setUserGroups", response);
|
||||
commit("setUserPermissions", response);
|
||||
} catch (error) {
|
||||
console.error("checkUserGroups (firebase.js): ", error);
|
||||
commit("setErrorMessage", "Unable to fetch user groups from the API");
|
||||
console.error("checkUserPermissions (firebase.js): ", error);
|
||||
commit("setErrorMessage", "Unable to fetch user permissions from the API");
|
||||
}
|
||||
},
|
||||
async checkUserUsage({ state, commit }) {
|
||||
try {
|
||||
commit("setErrorMessage", "");
|
||||
const r = await fetch(
|
||||
`${state.API_ENDPOINT}/user/usage`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${state.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await r.json();
|
||||
commit("setUserUsage", response);
|
||||
} catch (error) {
|
||||
console.error("checkUserUsage (firebase.js): ", error);
|
||||
commit("setErrorMessage", "Unable to fetch user usage quota from the API");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -376,7 +403,8 @@ export default createStore({
|
||||
} else {
|
||||
//TODO: merge these into single endpoint in the future
|
||||
store.dispatch("checkActiveUser");
|
||||
store.dispatch("checkUserGroups");
|
||||
store.dispatch("checkUserPermissions");
|
||||
store.dispatch("checkUserUsage");
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error("Error checking token expiration:", error);
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
Search archives by URL
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
|
||||
<!-- TODO: toggle between all/and my latest, maybe with tabs like sheets -->
|
||||
<v-form>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
|
||||
Reference in New Issue
Block a user