3
0
Fork 0
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:
Bluenix 2021-09-24 20:01:55 +02:00 committed by GitHub
commit 6a3007562e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
429 changed files with 8120 additions and 2717 deletions

View file

@ -1,3 +1,3 @@
module.exports = {
collapseWhitespace: false
collapseWhitespace: false,
};

View file

@ -0,0 +1 @@
/dist

View file

@ -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",

View file

@ -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",

View file

@ -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");

View file

@ -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`;
}

View file

@ -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>

View file

@ -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');

View file

@ -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>

View file

@ -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");
},
});

View file

@ -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>

View file

@ -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>

View file

@ -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) {

View file

@ -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 };
},
},

View file

@ -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[];
};
}

View file

@ -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: [],
};

View file

@ -9,10 +9,7 @@
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": false,
"lib": [
"esnext",
"dom"
],
"lib": ["esnext", "dom"],
"baseUrl": ".",
"resolveJsonModule": true,
"esModuleInterop": true,

View file

@ -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,