mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-05-25 02:25:01 +00:00
Merge branch 'master' into setup-documentation
This commit is contained in:
commit
6a3007562e
429 changed files with 8120 additions and 2717 deletions
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
collapseWhitespace: false
|
||||
collapseWhitespace: false,
|
||||
};
|
||||
|
|
1
dashboard/.prettierignore
Normal file
1
dashboard/.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
/dist
|
25
dashboard/package-lock.json
generated
25
dashboard/package-lock.json
generated
|
@ -9,9 +9,11 @@
|
|||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"highlight.js": "^9.15.10",
|
||||
"humanize-duration": "^3.27.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"marked": "^0.7.0",
|
||||
"modern-css-reset": "^1.0.4",
|
||||
"moment": "^2.29.1",
|
||||
"vue": "^2.6.10",
|
||||
"vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance",
|
||||
"vue-material-design-icons": "^4.1.0",
|
||||
|
@ -6038,6 +6040,11 @@
|
|||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/humanize-duration": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.0.tgz",
|
||||
"integrity": "sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ=="
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
|
@ -7164,6 +7171,14 @@
|
|||
"resolved": "https://registry.npmjs.org/modern-css-reset/-/modern-css-reset-1.4.0.tgz",
|
||||
"integrity": "sha512-0crZmSFmrxkI7159rvQWjpDhy0u4+Awg/iOycJdlVn0RSeft/a+6BrQHR3IqvmdK25sqt0o6Z5Ap7cWgUee2rw=="
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
@ -17836,6 +17851,11 @@
|
|||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||
"dev": true
|
||||
},
|
||||
"humanize-duration": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.0.tgz",
|
||||
"integrity": "sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ=="
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
|
@ -18685,6 +18705,11 @@
|
|||
"resolved": "https://registry.npmjs.org/modern-css-reset/-/modern-css-reset-1.4.0.tgz",
|
||||
"integrity": "sha512-0crZmSFmrxkI7159rvQWjpDhy0u4+Awg/iOycJdlVn0RSeft/a+6BrQHR3IqvmdK25sqt0o6Z5Ap7cWgUee2rw=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
|
|
@ -39,9 +39,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"highlight.js": "^9.15.10",
|
||||
"humanize-duration": "^3.27.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"marked": "^0.7.0",
|
||||
"modern-css-reset": "^1.0.4",
|
||||
"moment": "^2.29.1",
|
||||
"vue": "^2.6.10",
|
||||
"vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance",
|
||||
"vue-material-design-icons": "^4.1.0",
|
||||
|
|
|
@ -22,13 +22,13 @@ function buildQueryString(params: QueryParamObject) {
|
|||
return (
|
||||
"?" +
|
||||
Array.from(Object.entries(params))
|
||||
.map(pair => `${encodeURIComponent(pair[0])}=${encodeURIComponent(pair[1] || "")}`)
|
||||
.map((pair) => `${encodeURIComponent(pair[0])}=${encodeURIComponent(pair[1] || "")}`)
|
||||
.join("&")
|
||||
);
|
||||
}
|
||||
|
||||
export function request(resource, fetchOpts: RequestInit = {}) {
|
||||
return fetch(`${apiUrl}/${resource}`, fetchOpts).then(async res => {
|
||||
return fetch(`${apiUrl}/${resource}`, fetchOpts).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
RootStore.dispatch("auth/expiredLogin");
|
||||
|
|
|
@ -16,8 +16,8 @@ export const authGuard: NavigationGuard = async (to, from, next) => {
|
|||
|
||||
export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
|
||||
if (to.query.apiKey) {
|
||||
await RootStore.dispatch("auth/setApiKey", to.query.apiKey);
|
||||
next("/dashboard");
|
||||
await RootStore.dispatch("auth/setApiKey", { key: to.query.apiKey });
|
||||
window.location.href = "/dashboard";
|
||||
} else {
|
||||
window.location.href = `/?error=noAccess`;
|
||||
}
|
||||
|
|
|
@ -1,70 +1,182 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>Guild Access</h1>
|
||||
<h1>Dashboard access</h1>
|
||||
<p>
|
||||
<img class="inline-block w-16 mr-4" style="vertical-align: -20px" src="../../img/squint.png"> Or here
|
||||
On this page you can manage who has access to the server's Zeppelin dashboard.
|
||||
</p>
|
||||
<div v-for="permAssignment in permissionAssignments">
|
||||
<strong>{{ permAssignment.type }} {{ permAssignment.target_id }}</strong>
|
||||
<permission-tree :tree="permAssignment._permissionTree" :granted-permissions="permAssignment.permissions" :on-change="onTreeUpdate.bind(null, permAssignment)" />
|
||||
|
||||
<h2 class="mt-8">Roles</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Owner:</strong> All permissions. Managed automatically by the bot.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Bot manager:</strong> Can manage dashboard users (including other bot managers) and edit server configuration
|
||||
</li>
|
||||
<li>
|
||||
<strong>Bot operator:</strong> Can edit server configuration
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="mt-8">Dashboard users</h2>
|
||||
<div class="mt-4">
|
||||
<div v-if="permanentPermissionAssignments.length === 0">
|
||||
No dashboard users
|
||||
</div>
|
||||
<ul v-if="permanentPermissionAssignments.length">
|
||||
<li v-for="perm in permanentPermissionAssignments">
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<strong>{{ perm.target_id }}</strong>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<label class="block" v-if="isOwner(perm)">
|
||||
<input type="checkbox" checked="checked" disabled>
|
||||
Owner
|
||||
</label>
|
||||
<label class="block">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="hasPermission(perm, 'MANAGE_ACCESS')"
|
||||
@change="ev => setPermissionValue(perm, 'MANAGE_ACCESS', ev.target.checked)"
|
||||
:disabled="hasPermissionIndirectly(perm, 'MANAGE_ACCESS')"
|
||||
>
|
||||
Bot manager
|
||||
</label>
|
||||
<label class="block">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="hasPermission(perm, 'EDIT_CONFIG')"
|
||||
@change="ev => setPermissionValue(perm, 'EDIT_CONFIG', ev.target.checked)"
|
||||
:disabled="hasPermissionIndirectly(perm, 'EDIT_CONFIG')"
|
||||
>
|
||||
Bot operator
|
||||
</label>
|
||||
<a href="#" v-on:click="deletePermissionAssignment(perm)" v-if="!isOwner(perm)">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-2">
|
||||
<a href="#" v-on:click="addPermissionAssignment()">
|
||||
Add new user
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-8">Temporary dashboard users</h2>
|
||||
<p>
|
||||
You can add temporary dashboard users to e.g. request help from a person outside your organization.<br>
|
||||
Temporary users always have <strong>Bot operator</strong> permissions.
|
||||
</p>
|
||||
<div v-if="temporaryPermissionAssignments.length === 0">
|
||||
No temporary dashboard users
|
||||
</div>
|
||||
<ul v-if="temporaryPermissionAssignments.length">
|
||||
<li v-for="perm in temporaryPermissionAssignments">
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<strong>{{ perm.target_id }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Expires in {{ formatTimeRemaining(perm) }}
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" v-on:click="deletePermissionAssignment(perm)">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-2">
|
||||
<a href="#" v-on:click="addTemporaryPermissionAssignment()">
|
||||
Add temporary user for 1 hour
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ApiPermissions, permissionHierarchy } from "@shared/apiPermissions";
|
||||
import PermissionTree from "./PermissionTree.vue";
|
||||
import { applyStateToPermissionHierarchy } from "./permissionTreeUtils";
|
||||
import { mapState } from "vuex";
|
||||
import { GuildState } from "../../store/types";
|
||||
import { ApiPermissions, hasPermission } from "@shared/apiPermissions";
|
||||
import PermissionTree from "./PermissionTree.vue";
|
||||
import { mapState } from "vuex";
|
||||
import {
|
||||
GuildPermissionAssignment,
|
||||
GuildState,
|
||||
RootState
|
||||
} from "../../store/types";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import moment from "moment";
|
||||
|
||||
export default {
|
||||
export default {
|
||||
components: {PermissionTree},
|
||||
|
||||
data() {
|
||||
return {
|
||||
managerPermissions: new Set([ApiPermissions.ManageAccess]),
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
canManage(state: RootState): boolean {
|
||||
const guildPermissions = state.guilds.guildPermissionAssignments[this.$route.params.guildId] || [];
|
||||
const myPermissions = guildPermissions.find(p => p.type === "USER" && p.target_id === state.auth.userId) || null;
|
||||
return myPermissions && hasPermission(myPermissions.permissions, ApiPermissions.ManageAccess);
|
||||
},
|
||||
}),
|
||||
...mapState<GuildState>("guilds", {
|
||||
canManage(guilds) {
|
||||
return guilds.myPermissions[this.$route.params.guildId]?.[ApiPermissions.ManageAccess];
|
||||
permanentPermissionAssignments(guilds: GuildState): GuildPermissionAssignment[] {
|
||||
return (guilds.guildPermissionAssignments[this.$route.params.guildId] || []).filter(perm => perm.expires_at == null);
|
||||
},
|
||||
|
||||
permissionAssignments(guilds) {
|
||||
return (guilds.guildPermissionAssignments[this.$route.params.guildId] || []).map(permAssignment => {
|
||||
return {
|
||||
...permAssignment,
|
||||
_permissionTree: applyStateToPermissionHierarchy(permissionHierarchy, permAssignment.permissions, this.managerPermissions),
|
||||
};
|
||||
});
|
||||
temporaryPermissionAssignments(guilds: GuildState): GuildPermissionAssignment[] {
|
||||
return (guilds.guildPermissionAssignments[this.$route.params.guildId] || []).filter(perm => perm.expires_at != null);
|
||||
},
|
||||
}),
|
||||
},
|
||||
// beforeMount() {
|
||||
// this.tree = applyStateToPermissionHierarchy(permissionHierarchy, this.grantedPermissions, this.managerPermissions);
|
||||
// },
|
||||
|
||||
async mounted() {
|
||||
await this.$store.dispatch("guilds/checkPermission", {
|
||||
guildId: this.$route.params.guildId,
|
||||
permission: ApiPermissions.ManageAccess,
|
||||
});
|
||||
await this.$store.dispatch("guilds/loadGuildPermissionAssignments", this.$route.params.guildId).catch(() => {});
|
||||
|
||||
if (! this.canManage) {
|
||||
this.$router.push('/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$store.dispatch("guilds/loadGuildPermissionAssignments", this.$route.params.guildId);
|
||||
},
|
||||
methods: {
|
||||
// updateTreeState() {
|
||||
// this.tree = applyStateToPermissionHierarchy(permissionHierarchy, this.grantedPermissions, this.managerPermissions);
|
||||
// },
|
||||
//
|
||||
// onChange() {
|
||||
// this.updateTreeState();
|
||||
// }
|
||||
isOwner(perm: GuildPermissionAssignment) {
|
||||
return perm.permissions.has(ApiPermissions.Owner);
|
||||
},
|
||||
|
||||
hasPermission(perm: GuildPermissionAssignment, permissionName: ApiPermissions) {
|
||||
return hasPermission(perm.permissions, permissionName);
|
||||
},
|
||||
|
||||
hasPermissionIndirectly(perm: GuildPermissionAssignment, permissionName: ApiPermissions) {
|
||||
return hasPermission(perm.permissions, permissionName) && ! perm.permissions.has(permissionName);
|
||||
},
|
||||
|
||||
setPermissionValue(perm: GuildPermissionAssignment, permissionName: ApiPermissions, value) {
|
||||
if (value) {
|
||||
perm.permissions.add(permissionName);
|
||||
} else {
|
||||
perm.permissions.delete(permissionName);
|
||||
}
|
||||
|
||||
this.$store.dispatch("guilds/setTargetPermissions", {
|
||||
guildId: this.$route.params.guildId,
|
||||
type: perm.type,
|
||||
targetId: perm.target_id,
|
||||
permissions: Array.from(perm.permissions),
|
||||
expiresAt: null,
|
||||
});
|
||||
|
||||
this.$set(perm, "permissions", new Set(perm.permissions));
|
||||
},
|
||||
|
||||
onTreeUpdate(targetPermissions) {
|
||||
this.$store.dispatch("guilds/setTargetPermissions", {
|
||||
|
@ -73,7 +185,58 @@
|
|||
type: targetPermissions.type,
|
||||
permissions: targetPermissions.permissions,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
formatTimeRemaining(perm: GuildPermissionAssignment) {
|
||||
const ms = Math.max(moment.utc(perm.expires_at).valueOf() - Date.now(), 0);
|
||||
return humanizeDuration(ms, { largest: 2, round: true });
|
||||
},
|
||||
|
||||
addPermissionAssignment() {
|
||||
const userId = window.prompt("Enter user ID");
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.dispatch("guilds/setTargetPermissions", {
|
||||
guildId: this.$route.params.guildId,
|
||||
type: "USER",
|
||||
targetId: userId,
|
||||
permissions: [ApiPermissions.EditConfig],
|
||||
expiresAt: null,
|
||||
});
|
||||
},
|
||||
|
||||
addTemporaryPermissionAssignment() {
|
||||
const userId = window.prompt("Enter user ID");
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiresAt = moment.utc().add(1, "hour").format("YYYY-MM-DD HH:mm:ss");
|
||||
this.$store.dispatch("guilds/setTargetPermissions", {
|
||||
guildId: this.$route.params.guildId,
|
||||
type: "USER",
|
||||
targetId: userId,
|
||||
permissions: [ApiPermissions.EditConfig],
|
||||
expiresAt,
|
||||
});
|
||||
},
|
||||
|
||||
deletePermissionAssignment(perm: GuildPermissionAssignment) {
|
||||
const confirm = window.confirm(`Remove ${perm.target_id} from dashboard users?`);
|
||||
if (! confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.dispatch("guilds/setTargetPermissions", {
|
||||
guildId: this.$route.params.guildId,
|
||||
type: perm.type,
|
||||
targetId: perm.target_id,
|
||||
permissions: [],
|
||||
expiresAt: null,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
@init="editorInit"
|
||||
lang="yaml"
|
||||
theme="tomorrow_night"
|
||||
:width="editorWidth"
|
||||
:height="editorHeight"
|
||||
ref="aceEditor" />
|
||||
</div>
|
||||
|
@ -38,6 +37,7 @@
|
|||
import AceEditor from "vue2-ace-editor";
|
||||
|
||||
let editorKeybindListener;
|
||||
let windowResizeListener;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -69,6 +69,12 @@
|
|||
window.removeEventListener("keydown", editorKeybindListener);
|
||||
editorKeybindListener = null;
|
||||
}
|
||||
|
||||
if (windowResizeListener) {
|
||||
window.removeEventListener("resize", windowResizeListener);
|
||||
windowResizeListener = null;
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
data() {
|
||||
|
@ -105,6 +111,7 @@
|
|||
tabSize: 2
|
||||
});
|
||||
|
||||
// Add Ctrl+S/Cmd+S save shortcut
|
||||
const isMac = /mac/i.test(navigator.platform);
|
||||
const modKeyPressed = (ev: KeyboardEvent) => (isMac ? ev.metaKey : ev.ctrlKey);
|
||||
const nonModKeyPressed = (ev: KeyboardEvent) => (isMac ? ev.ctrlKey : ev.metaKey);
|
||||
|
@ -130,7 +137,24 @@
|
|||
};
|
||||
window.addEventListener("keydown", editorKeybindListener);
|
||||
|
||||
// Auto-fit editor to window
|
||||
this.fitEditorToWindow();
|
||||
|
||||
if (windowResizeListener) {
|
||||
window.removeEventListener("resize", windowResizeListener);
|
||||
}
|
||||
|
||||
let debounceTimeout;
|
||||
windowResizeListener = (ev: UIEvent) => {
|
||||
if (debounceTimeout) {
|
||||
clearTimeout(debounceTimeout);
|
||||
}
|
||||
|
||||
debounceTimeout = setTimeout(() => {
|
||||
this.fitEditorToWindow();
|
||||
}, 350);
|
||||
};
|
||||
window.addEventListener("resize", windowResizeListener);
|
||||
},
|
||||
fitEditorToWindow() {
|
||||
const mainContainer = document.querySelector('.dashboard');
|
||||
|
|
|
@ -17,9 +17,8 @@
|
|||
<div class="text-gray-600 text-sm leading-tight">{{ guild.id }}</div>
|
||||
</div>
|
||||
<div class="pt-1">
|
||||
<span class="inline-block bg-gray-700 rounded px-1 opacity-50 select-none">Info</span>
|
||||
<router-link class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/config'">Config</router-link>
|
||||
<span class="inline-block bg-gray-700 rounded px-1 opacity-50 select-none">Access</span>
|
||||
<router-link v-if="canManageAccess(guild.id)" class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/access'">Access</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -28,12 +27,15 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from "vuex";
|
||||
<script lang="ts">
|
||||
import { mapState } from "vuex";
|
||||
import { ApiPermissions, hasPermission } from "@shared/apiPermissions";
|
||||
import { AuthState, GuildState } from "../../store/types";
|
||||
|
||||
export default {
|
||||
async mounted() {
|
||||
await this.$store.dispatch("guilds/loadAvailableGuilds");
|
||||
await this.$store.dispatch("guilds/loadMyPermissionAssignments");
|
||||
this.loading = false;
|
||||
},
|
||||
data() {
|
||||
|
@ -41,7 +43,7 @@
|
|||
},
|
||||
computed: {
|
||||
...mapState('guilds', {
|
||||
guilds: state => {
|
||||
guilds: (state: GuildState) => {
|
||||
const guilds = Array.from(state.available.values());
|
||||
guilds.sort((a, b) => {
|
||||
if (a.name > b.name) return 1;
|
||||
|
@ -52,7 +54,20 @@
|
|||
});
|
||||
return guilds;
|
||||
},
|
||||
|
||||
guildPermissionAssignments: (state: GuildState) => state.guildPermissionAssignments,
|
||||
}),
|
||||
|
||||
...mapState('auth', {
|
||||
userId: (state: AuthState) => state.userId!,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
canManageAccess(guildId: string) {
|
||||
const guildPermissions = this.guildPermissionAssignments[guildId] || [];
|
||||
const myPermissions = guildPermissions.find(p => p.type === "USER" && p.target_id === this.userId) || null;
|
||||
return myPermissions && hasPermission(new Set(myPermissions.permissions), ApiPermissions.ManageAccess);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -19,7 +19,7 @@ Vue.directive("trim-indents", {
|
|||
|
||||
el.innerHTML = withoutStartEndWhitespace
|
||||
.split("\n")
|
||||
.map(line => line.slice(spacesToTrim))
|
||||
.map((line) => line.slice(spacesToTrim))
|
||||
.join("\n");
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Zeppelin - Moderation bot for Discord</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<h1>Zeppelin</h1>
|
||||
The Zeppelin website requires JavaScript to load.
|
||||
</noscript>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Zeppelin - Moderation bot for Discord</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<h1>Zeppelin</h1>
|
||||
The Zeppelin website requires JavaScript to load.
|
||||
</noscript>
|
||||
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div id="error"></div>
|
||||
<div class="wrapper">
|
||||
<div class="logo-column">
|
||||
<img class="logo" src="./img/logo.png" alt="Zeppelin Logo">
|
||||
<img class="logo" src="./img/logo.png" alt="Zeppelin Logo" />
|
||||
</div>
|
||||
<div class="info-column">
|
||||
<h1>Zeppelin</h1>
|
||||
|
|
|
@ -12,6 +12,7 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
apiKey: null,
|
||||
loadedInitialAuth: false,
|
||||
authRefreshInterval: null,
|
||||
userId: null,
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
@ -23,7 +24,7 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
try {
|
||||
const result = await post("auth/validate-key", { key: storedKey });
|
||||
if (result.valid) {
|
||||
await dispatch("setApiKey", storedKey);
|
||||
await dispatch("setApiKey", { key: storedKey, userId: result.userId });
|
||||
return;
|
||||
}
|
||||
} catch {} // tslint:disable-line
|
||||
|
@ -35,9 +36,9 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
commit("markInitialAuthLoaded");
|
||||
},
|
||||
|
||||
setApiKey({ commit, state, dispatch }, newKey: string) {
|
||||
localStorage.setItem("apiKey", newKey);
|
||||
commit("setApiKey", newKey);
|
||||
setApiKey({ commit, state, dispatch }, { key, userId }) {
|
||||
localStorage.setItem("apiKey", key);
|
||||
commit("setApiKey", { key, userId });
|
||||
|
||||
dispatch("startAuthAutoRefresh");
|
||||
},
|
||||
|
@ -64,7 +65,7 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
await dispatch("endAuthAutoRefresh");
|
||||
|
||||
localStorage.removeItem("apiKey");
|
||||
commit("setApiKey", null);
|
||||
commit("setApiKey", { key: null, userId: null });
|
||||
},
|
||||
|
||||
async logout({ dispatch }) {
|
||||
|
@ -79,8 +80,9 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
},
|
||||
|
||||
mutations: {
|
||||
setApiKey(state: AuthState, key) {
|
||||
setApiKey(state: AuthState, { key, userId }) {
|
||||
state.apiKey = key;
|
||||
state.userId = userId;
|
||||
},
|
||||
|
||||
setAuthRefreshInterval(state: AuthState, interval: IntervalType | null) {
|
||||
|
|
|
@ -11,7 +11,6 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
availableGuildsLoadStatus: LoadStatus.None,
|
||||
available: new Map(),
|
||||
configs: {},
|
||||
myPermissions: {},
|
||||
guildPermissionAssignments: {},
|
||||
},
|
||||
|
||||
|
@ -48,9 +47,14 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
await post(`guilds/${guildId}/config`, { config });
|
||||
},
|
||||
|
||||
async checkPermission({ commit }, { guildId, permission }) {
|
||||
const result = await post(`guilds/${guildId}/check-permission`, { permission });
|
||||
commit("setMyPermission", { guildId, permission, value: result.result });
|
||||
async loadMyPermissionAssignments({ commit }) {
|
||||
const myPermissionAssignments = await get(`guilds/my-permissions`);
|
||||
for (const permissionAssignment of myPermissionAssignments) {
|
||||
commit("setGuildPermissionAssignments", {
|
||||
guildId: permissionAssignment.guild_id,
|
||||
permissionAssignments: [permissionAssignment],
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async loadGuildPermissionAssignments({ commit }, guildId) {
|
||||
|
@ -58,8 +62,9 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
commit("setGuildPermissionAssignments", { guildId, permissionAssignments });
|
||||
},
|
||||
|
||||
async setTargetPermissions({ commit }, { guildId, targetId, type, permissions }) {
|
||||
commit("setTargetPermissions", { guildId, targetId, type, permissions });
|
||||
async setTargetPermissions({ commit }, { guildId, targetId, type, permissions, expiresAt }) {
|
||||
await post(`guilds/${guildId}/set-target-permissions`, { guildId, targetId, type, permissions, expiresAt });
|
||||
commit("setTargetPermissions", { guildId, targetId, type, permissions, expiresAt });
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -77,28 +82,44 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
Vue.set(state.configs, guildId, config);
|
||||
},
|
||||
|
||||
setMyPermission(state: GuildState, { guildId, permission, value }) {
|
||||
Vue.set(state.myPermissions, guildId, state.myPermissions[guildId] || {});
|
||||
Vue.set(state.myPermissions[guildId], permission, value);
|
||||
},
|
||||
|
||||
setGuildPermissionAssignments(state: GuildState, { guildId, permissionAssignments }) {
|
||||
if (!state.guildPermissionAssignments) {
|
||||
Vue.set(state, "guildPermissionAssignments", {});
|
||||
}
|
||||
|
||||
Vue.set(
|
||||
state.guildPermissionAssignments,
|
||||
guildId,
|
||||
permissionAssignments.map(p => ({
|
||||
permissionAssignments.map((p) => ({
|
||||
...p,
|
||||
permissions: new Set(p.permissions),
|
||||
})),
|
||||
);
|
||||
},
|
||||
|
||||
setTargetPermissions(state: GuildState, { guildId, targetId, type, permissions }) {
|
||||
setTargetPermissions(state: GuildState, { guildId, targetId, type, permissions, expiresAt }) {
|
||||
const guildPermissionAssignments = state.guildPermissionAssignments[guildId] || [];
|
||||
const itemToEdit = guildPermissionAssignments.find(p => p.target_id === targetId && p.type === type);
|
||||
if (!itemToEdit) return;
|
||||
if (permissions.length === 0) {
|
||||
// No permissions -> remove permission assignment
|
||||
guildPermissionAssignments.splice(
|
||||
guildPermissionAssignments.findIndex((p) => p.target_id === targetId && p.type === type),
|
||||
1,
|
||||
);
|
||||
} else {
|
||||
// Update/add permission assignment
|
||||
const itemToEdit = guildPermissionAssignments.find((p) => p.target_id === targetId && p.type === type);
|
||||
if (itemToEdit) {
|
||||
itemToEdit.permissions = new Set(permissions);
|
||||
} else {
|
||||
state.guildPermissionAssignments[guildId].push({
|
||||
type,
|
||||
target_id: targetId,
|
||||
permissions: new Set(permissions),
|
||||
expires_at: expiresAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
itemToEdit.permissions = permissions;
|
||||
state.guildPermissionAssignments = { ...state.guildPermissionAssignments };
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { ApiPermissions } from "@shared/apiPermissions";
|
||||
import { ApiPermissionTypes } from "../../../backend/src/data/ApiPermissionAssignments";
|
||||
|
||||
export enum LoadStatus {
|
||||
None = 1,
|
||||
|
@ -14,6 +13,14 @@ export interface AuthState {
|
|||
apiKey: string | null;
|
||||
loadedInitialAuth: boolean;
|
||||
authRefreshInterval: IntervalType | null;
|
||||
userId: string | null;
|
||||
}
|
||||
|
||||
export interface GuildPermissionAssignment {
|
||||
type: string;
|
||||
target_id: string;
|
||||
permissions: Set<ApiPermissions>;
|
||||
expires_at: string | null;
|
||||
}
|
||||
|
||||
export interface GuildState {
|
||||
|
@ -29,17 +36,8 @@ export interface GuildState {
|
|||
configs: {
|
||||
[key: string]: string;
|
||||
};
|
||||
myPermissions: {
|
||||
[guildId: string]: {
|
||||
[K in ApiPermissions]?: boolean;
|
||||
};
|
||||
};
|
||||
guildPermissionAssignments: {
|
||||
[guildId: string]: Array<{
|
||||
target_id: string;
|
||||
type: ApiPermissionTypes;
|
||||
permissions: Set<ApiPermissions>;
|
||||
}>;
|
||||
[guildId: string]: GuildPermissionAssignment[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,22 +3,22 @@ module.exports = {
|
|||
theme: {
|
||||
extend: {
|
||||
lineHeight: {
|
||||
zero: '0'
|
||||
zero: "0",
|
||||
},
|
||||
flex: {
|
||||
full: '0 0 100%',
|
||||
flexible: '1 1 0'
|
||||
}
|
||||
full: "0 0 100%",
|
||||
flexible: "1 1 0",
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
'until-lg': { max: '1023px' },
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1536px'
|
||||
}
|
||||
sm: "640px",
|
||||
md: "768px",
|
||||
"until-lg": { max: "1023px" },
|
||||
lg: "1024px",
|
||||
xl: "1280px",
|
||||
"2xl": "1536px",
|
||||
},
|
||||
},
|
||||
variants: {},
|
||||
plugins: []
|
||||
}
|
||||
plugins: [],
|
||||
};
|
||||
|
|
|
@ -9,10 +9,7 @@
|
|||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"strict": false,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["esnext", "dom"],
|
||||
"baseUrl": ".",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
|
@ -1,42 +1,40 @@
|
|||
require('dotenv').config();
|
||||
require("dotenv").config();
|
||||
|
||||
const path = require('path');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const DotenvPlugin = require('dotenv-webpack');
|
||||
const merge = require('webpack-merge');
|
||||
const path = require("path");
|
||||
const VueLoaderPlugin = require("vue-loader/lib/plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const DotenvPlugin = require("dotenv-webpack");
|
||||
const merge = require("webpack-merge");
|
||||
|
||||
const targetDir = path.normalize(path.join(__dirname, 'dist'));
|
||||
const targetDir = path.normalize(path.join(__dirname, "dist"));
|
||||
|
||||
if (! process.env.NODE_ENV) {
|
||||
console.error('Please set NODE_ENV');
|
||||
if (!process.env.NODE_ENV) {
|
||||
console.error("Please set NODE_ENV");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const babelOpts = {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
],
|
||||
presets: ["@babel/preset-env"],
|
||||
};
|
||||
|
||||
const tsconfig = require('./tsconfig.json');
|
||||
const tsconfig = require("./tsconfig.json");
|
||||
const pathAliases = Object.entries(tsconfig.compilerOptions.paths || []).reduce((aliases, pair) => {
|
||||
let alias = pair[0];
|
||||
if (alias.endsWith('/*')) alias = alias.slice(0, -2);
|
||||
if (alias.endsWith("/*")) alias = alias.slice(0, -2);
|
||||
|
||||
let aliasPath = pair[1][0];
|
||||
if (aliasPath.endsWith('/*')) aliasPath = aliasPath.slice(0, -2);
|
||||
if (aliasPath.endsWith("/*")) aliasPath = aliasPath.slice(0, -2);
|
||||
|
||||
aliases[alias] = path.resolve(__dirname, aliasPath);
|
||||
return aliases;
|
||||
}, {});
|
||||
|
||||
let config = {
|
||||
entry: './src/main.ts',
|
||||
entry: "./src/main.ts",
|
||||
output: {
|
||||
filename: '[name].[hash].js',
|
||||
filename: "[name].[hash].js",
|
||||
path: targetDir,
|
||||
publicPath: '/',
|
||||
publicPath: "/",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
|
@ -50,11 +48,11 @@ let config = {
|
|||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
loader: "babel-loader",
|
||||
options: babelOpts,
|
||||
},
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
appendTsSuffixTo: [/\.vue$/],
|
||||
},
|
||||
|
@ -65,7 +63,7 @@ let config = {
|
|||
test: /\.m?js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
loader: "babel-loader",
|
||||
options: babelOpts,
|
||||
},
|
||||
},
|
||||
|
@ -90,26 +88,23 @@ let config = {
|
|||
loader: "postcss-loader",
|
||||
options: {
|
||||
ident: "postcss",
|
||||
plugins: loader => {
|
||||
plugins: (loader) => {
|
||||
const plugins = [
|
||||
require('postcss-import')({
|
||||
require("postcss-import")({
|
||||
resolve(id, base, options) {
|
||||
// Since WebStorm doesn't resolve imports from node_modules without a tilde (~) prefix,
|
||||
// strip the tilde here to get the best of both worlds (webstorm support + postcss-import support)
|
||||
if (id[0] === '~') id = id.slice(1);
|
||||
if (id[0] === "~") id = id.slice(1);
|
||||
// Call the original resolver after stripping the tilde
|
||||
return require('postcss-import/lib/resolve-id')(id, base, options);
|
||||
return require("postcss-import/lib/resolve-id")(id, base, options);
|
||||
},
|
||||
}),
|
||||
require('postcss-nesting')(),
|
||||
require('tailwindcss')(),
|
||||
require("postcss-nesting")(),
|
||||
require("tailwindcss")(),
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
plugins.push(
|
||||
require('postcss-preset-env')(),
|
||||
require('cssnano')(),
|
||||
);
|
||||
plugins.push(require("postcss-preset-env")(), require("cssnano")());
|
||||
}
|
||||
|
||||
return plugins;
|
||||
|
@ -137,9 +132,9 @@ let config = {
|
|||
{
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
root: path.resolve(__dirname, 'src'),
|
||||
attrs: ['img:src', 'link:href'],
|
||||
...(process.env.NODE_ENV === 'production' && {
|
||||
root: path.resolve(__dirname, "src"),
|
||||
attrs: ["img:src", "link:href"],
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
minimize: true,
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
|
@ -153,29 +148,29 @@ let config = {
|
|||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/index.html',
|
||||
template: "src/index.html",
|
||||
files: {
|
||||
"css": ["./src/style/initial.pcss"],
|
||||
"js": ["./src/main.ts"],
|
||||
css: ["./src/style/initial.pcss"],
|
||||
js: ["./src/main.ts"],
|
||||
},
|
||||
}),
|
||||
new DotenvPlugin(),
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.mjs', '.vue'],
|
||||
extensions: [".ts", ".tsx", ".js", ".mjs", ".vue"],
|
||||
alias: pathAliases,
|
||||
},
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
config = merge(config, {
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
mode: "production",
|
||||
devtool: "source-map",
|
||||
});
|
||||
} else {
|
||||
config = merge(config, {
|
||||
mode: 'development',
|
||||
devtool: 'eval',
|
||||
mode: "development",
|
||||
devtool: "eval",
|
||||
devServer: {
|
||||
...(process.env.DEV_HOST ? { host: process.env.DEV_HOST } : undefined),
|
||||
historyApiFallback: true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue