mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-20 20:01:17 +00:00
Add channel editing interface
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
dc21293e35
commit
1a425416b8
6 changed files with 466 additions and 9 deletions
12
botEditor.go
12
botEditor.go
|
@ -3,21 +3,23 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getAuthorizationFromRequest(r *http.Request) (string, error) {
|
func getAuthorizationFromRequest(r *http.Request) (string, error) {
|
||||||
token := r.Header.Get("Authorization")
|
_, token, hadPrefix := strings.Cut(r.Header.Get("Authorization"), " ")
|
||||||
if token == "" {
|
if !hadPrefix {
|
||||||
return "", fmt.Errorf("no authorization provided")
|
return "", fmt.Errorf("no authorization provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, user, _, _, err := editorTokenService.ValidateLoginToken(token) //nolint:dogsled // Required at other places
|
_, user, _, _, err := editorTokenService.ValidateLoginToken(token) //nolint:dogsled // Required at other places
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("getting authorized user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = "API-User"
|
user = "API-User"
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, errors.Wrap(err, "getting authorized user")
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
220
src/components/channelOverview.vue
Normal file
220
src/components/channelOverview.vue
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
<template>
|
||||||
|
<div class="container my-3">
|
||||||
|
<div class="row justify-content-center mb-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fas fa-hashtag fa-fw me-1" />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model="inputAddChannel.text"
|
||||||
|
type="text"
|
||||||
|
:class="inputAddChannelClasses"
|
||||||
|
@keypress.enter="addChannel"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-success"
|
||||||
|
:disabled="!inputAddChannel.valid"
|
||||||
|
@click="addChannel"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus fa-fw me-1" />
|
||||||
|
{{ $t('channel.btnAdd') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t("channel.table.colChannel") }}</th>
|
||||||
|
<th>{{ $t("channel.table.colPermissions") }}</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="channel in channels"
|
||||||
|
:key="channel.name"
|
||||||
|
>
|
||||||
|
<td class="align-content-center">
|
||||||
|
<i class="fas fa-hashtag fa-fw me-1" />
|
||||||
|
{{ channel.name }}
|
||||||
|
</td>
|
||||||
|
<td class="align-content-center">
|
||||||
|
<i
|
||||||
|
v-if="channel.numScopesGranted === 0"
|
||||||
|
class="fas fa-triangle-exclamation fa-fw me-1 text-danger"
|
||||||
|
:title="$t('channel.table.titleNoPermissions')"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else-if="channel.numScopesGranted < numExtendedScopes"
|
||||||
|
class="fas fa-triangle-exclamation fa-fw me-1 text-warning"
|
||||||
|
:title="$t('channel.table.titlePartialPermissions')"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-circle-check fa-fw me-1 text-success"
|
||||||
|
:title="$t('channel.table.titleAllPermissions')"
|
||||||
|
/>
|
||||||
|
{{ $t('channel.table.textPermissions', {
|
||||||
|
avail: numExtendedScopes,
|
||||||
|
granted: channel.numScopesGranted
|
||||||
|
}) }}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name:'channelPermissions', params: { channel: channel.name } }"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
>
|
||||||
|
<i class="fas fa-pencil-alt fa-fw" />
|
||||||
|
</RouterLink>
|
||||||
|
<button
|
||||||
|
class="btn btn-danger"
|
||||||
|
@click="removeChannel(channel.name)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-minus fa-fw" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import BusEventTypes from '../helpers/busevents'
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { successToast } from '../helpers/toasts'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
channels(): Array<any> {
|
||||||
|
return this.generalConfig.channels?.map((name: string) => ({
|
||||||
|
name,
|
||||||
|
numScopesGranted: (this.generalConfig.channel_scopes[name] || [])
|
||||||
|
.filter((scope: string) => Object.keys(this.authURLs.available_extended_scopes).includes(scope))
|
||||||
|
.length,
|
||||||
|
}))
|
||||||
|
.sort((a: any, b: any) => a.name.localeCompare(b.name))
|
||||||
|
},
|
||||||
|
|
||||||
|
inputAddChannelClasses(): string {
|
||||||
|
const classes = ['form-control']
|
||||||
|
|
||||||
|
if (this.inputAddChannel.valid) {
|
||||||
|
classes.push('is-valid')
|
||||||
|
} else if (this.inputAddChannel.text) {
|
||||||
|
classes.push('is-invalid')
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
},
|
||||||
|
|
||||||
|
numExtendedScopes(): number {
|
||||||
|
return Object.keys(this.authURLs.available_extended_scopes || {}).length
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
authURLs: {} as any,
|
||||||
|
generalConfig: {} as any,
|
||||||
|
inputAddChannel: {
|
||||||
|
text: '',
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Adds the channel entered into the input field to the list
|
||||||
|
*/
|
||||||
|
addChannel(): Promise<void> | undefined {
|
||||||
|
if (!this.inputAddChannel.valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = this.inputAddChannel.text
|
||||||
|
|
||||||
|
return this.updateGeneralConfig({
|
||||||
|
...this.generalConfig,
|
||||||
|
channels: [
|
||||||
|
...this.generalConfig.channels.filter((chan: string) => chan !== channel),
|
||||||
|
channel,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
?.then(() => {
|
||||||
|
this.inputAddChannel.text = ''
|
||||||
|
this.bus.emit(BusEventTypes.Toast, successToast(this.$t('channel.toastChannelAdded')))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches auth-URLs from the backend
|
||||||
|
*/
|
||||||
|
fetchAuthURLs(): Promise<void> | undefined {
|
||||||
|
return this.$root?.fetchJSON('config-editor/auth-urls')
|
||||||
|
.then((data: any) => {
|
||||||
|
this.authURLs = data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the general config object from the backend
|
||||||
|
*/
|
||||||
|
fetchGeneralConfig(): Promise<void> | undefined {
|
||||||
|
return this.$root?.fetchJSON('config-editor/general')
|
||||||
|
.then((data: any) => {
|
||||||
|
this.generalConfig = data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells backend to remove a channel
|
||||||
|
*/
|
||||||
|
removeChannel(channel: string): Promise<void> | undefined {
|
||||||
|
return this.updateGeneralConfig({
|
||||||
|
...this.generalConfig,
|
||||||
|
channels: this.generalConfig.channels.filter((chan: string) => chan !== channel),
|
||||||
|
})
|
||||||
|
?.then(() => this.bus.emit(BusEventTypes.Toast, successToast(this.$t('channel.toastChannelRemoved'))))
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes general config back to backend
|
||||||
|
*
|
||||||
|
* @param config Configuration object to write (MUST contain all config)
|
||||||
|
*/
|
||||||
|
updateGeneralConfig(config: any): Promise<void> | undefined {
|
||||||
|
return this.$root?.fetchJSON('config-editor/general', {
|
||||||
|
body: JSON.stringify(config),
|
||||||
|
method: 'PUT',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// Reload config after it changed
|
||||||
|
this.bus.on(BusEventTypes.ConfigReload, () => this.fetchGeneralConfig())
|
||||||
|
|
||||||
|
// Do initial fetches
|
||||||
|
this.fetchAuthURLs()
|
||||||
|
this.fetchGeneralConfig()
|
||||||
|
},
|
||||||
|
|
||||||
|
name: 'TwitchBotEditorChannelOverview',
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'inputAddChannel.text'(to) {
|
||||||
|
this.inputAddChannel.valid = to.match(/^[a-zA-Z0-9_]{4,25}$/)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
201
src/components/channelPermissions.vue
Normal file
201
src/components/channelPermissions.vue
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
<template>
|
||||||
|
<div class="container my-3">
|
||||||
|
<div class="row justify-content-center mb-3">
|
||||||
|
<div class="col-8">
|
||||||
|
<p v-html="$t('channel.permissionStart', { channel })" />
|
||||||
|
<div
|
||||||
|
v-for="perm in permissions"
|
||||||
|
:key="perm.scope"
|
||||||
|
class="form-check form-switch"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="`switch${perm.scope}`"
|
||||||
|
v-model="granted[perm.scope]"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
:for="`switch${perm.scope}`"
|
||||||
|
>{{ perm.description }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch mt-2">
|
||||||
|
<input
|
||||||
|
id="switch_all"
|
||||||
|
v-model="allPermissions"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="switch_all"
|
||||||
|
>{{ $t('channel.permissionsAll') }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mt-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:value="permissionsURL || ''"
|
||||||
|
:disabled="!permissionsURL"
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
ref="copyBtn"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="!authURLs?.update_bot_token"
|
||||||
|
@click="copyAuthURL"
|
||||||
|
>
|
||||||
|
<i class="fas fa-clipboard fa-fw" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="fas fa-circle-info fa-fw me-1" />
|
||||||
|
{{ $t('channel.permissionInfoHeader') }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p v-html="$t('channel.permissionIntro')" />
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
v-for="(bpt, idx) in $tm('channel.permissionIntroBullets')"
|
||||||
|
:key="`idx${idx}`"
|
||||||
|
>
|
||||||
|
{{ bpt }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import BusEventTypes from '../helpers/busevents'
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
allPermissions: {
|
||||||
|
get(): boolean {
|
||||||
|
return this.extendedScopeNames
|
||||||
|
.filter((scope: string) => !this.granted[scope])
|
||||||
|
.length === 0
|
||||||
|
},
|
||||||
|
|
||||||
|
set(all: boolean): void {
|
||||||
|
this.granted = Object.fromEntries(this.extendedScopeNames
|
||||||
|
.map((scope: string) => [scope, all]))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
extendedScopeNames(): Array<string> {
|
||||||
|
return Object.entries(this.authURLs.available_extended_scopes || {})
|
||||||
|
.map(e => e[0])
|
||||||
|
},
|
||||||
|
|
||||||
|
permissions(): Array<any> {
|
||||||
|
return Object.entries(this.authURLs.available_extended_scopes || {}).map(e => ({
|
||||||
|
description: e[1],
|
||||||
|
scope: e[0],
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
|
permissionsURL(): string {
|
||||||
|
if (!this.authURLs.update_channel_scopes) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopes = Object.entries(this.granted).filter(e => e[1])
|
||||||
|
.map(e => e[0])
|
||||||
|
|
||||||
|
const u = new URL(this.authURLs.update_channel_scopes)
|
||||||
|
u.searchParams.set('scope', scopes.join(' '))
|
||||||
|
return u.toString()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
authURLs: {} as any,
|
||||||
|
generalConfig: {} as any,
|
||||||
|
granted: {} as any,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Copies auth-url for the bot into clipboard and gives user feedback
|
||||||
|
* by colorizing copy-button for a short moment
|
||||||
|
*/
|
||||||
|
copyAuthURL(): void {
|
||||||
|
navigator.clipboard.writeText(this.permissionsURL)
|
||||||
|
.then(() => {
|
||||||
|
const btn = this.$refs.copyBtn as Element
|
||||||
|
btn.classList.replace('btn-primary', 'btn-success')
|
||||||
|
window.setTimeout(() => btn.classList.replace('btn-success', 'btn-primary'), 2500)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches auth-URLs from the backend
|
||||||
|
*/
|
||||||
|
fetchAuthURLs(): Promise<void> | undefined {
|
||||||
|
return this.$root?.fetchJSON('config-editor/auth-urls')
|
||||||
|
.then((data: any) => {
|
||||||
|
this.authURLs = data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the general config object from the backend
|
||||||
|
*/
|
||||||
|
fetchGeneralConfig(): Promise<void> | undefined {
|
||||||
|
return this.$root?.fetchJSON('config-editor/general')
|
||||||
|
.then((data: any) => {
|
||||||
|
this.generalConfig = data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the granted scopes into the object for easier display
|
||||||
|
* of the permission switches
|
||||||
|
*/
|
||||||
|
loadScopes(): void {
|
||||||
|
this.granted = Object.fromEntries((this.generalConfig.channel_scopes[this.channel] || []).map((scope: string) => [scope, true]))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// Reload config after it changed
|
||||||
|
this.bus.on(BusEventTypes.ConfigReload, () => this.fetchGeneralConfig()?.then(() => this.loadScopes()))
|
||||||
|
|
||||||
|
// Socket-reconnect could mean we need new auth-urls as the state
|
||||||
|
// may have changed due to bot-restart
|
||||||
|
this.bus.on(BusEventTypes.NotifySocketConnected, () => this.fetchAuthURLs())
|
||||||
|
|
||||||
|
// Do initial fetches
|
||||||
|
this.fetchAuthURLs()
|
||||||
|
this.fetchGeneralConfig()?.then(() => this.loadScopes())
|
||||||
|
},
|
||||||
|
|
||||||
|
name: 'TwitchBotEditorChannelPermissions',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
channel: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
channel() {
|
||||||
|
this.loadScopes()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -9,6 +9,29 @@
|
||||||
],
|
],
|
||||||
"heading": "Updating Bot-Authorization"
|
"heading": "Updating Bot-Authorization"
|
||||||
},
|
},
|
||||||
|
"channel": {
|
||||||
|
"btnAdd": "Add Channel",
|
||||||
|
"permissionsAll": "…do all of the above!",
|
||||||
|
"permissionInfoHeader": "Explanation",
|
||||||
|
"permissionIntro": "In order to access non-public information as channel-point redemptions or take actions limited to the channel owner the bot needs additional permissions. The <strong>owner</strong> of the channel needs to grant those!",
|
||||||
|
"permissionIntroBullets": [
|
||||||
|
"Select permissions on the left side",
|
||||||
|
"Copy the URL provided below",
|
||||||
|
"Pass the URL to the channel owner and tell them to open it with their personal account logged in",
|
||||||
|
"The bot will display a message containing the updated account"
|
||||||
|
],
|
||||||
|
"permissionStart": "For <strong>#{channel}</strong> the bot should be able to…",
|
||||||
|
"table": {
|
||||||
|
"colChannel": "Channel",
|
||||||
|
"colPermissions": "Permissions",
|
||||||
|
"textPermissions": "{granted} of {avail} granted",
|
||||||
|
"titleAllPermissions": "Bot can use all available extra features",
|
||||||
|
"titleNoPermissions": "Bot can not use features aside of chatting",
|
||||||
|
"titlePartialPermissions": "Bot can use some extra features"
|
||||||
|
},
|
||||||
|
"toastChannelAdded": "Channel added",
|
||||||
|
"toastChannelRemoved": "Channel removed"
|
||||||
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"activeRaffles": {
|
"activeRaffles": {
|
||||||
"caption": "Active",
|
"caption": "Active",
|
||||||
|
|
|
@ -114,6 +114,14 @@ const app = createApp({
|
||||||
|
|
||||||
parseResponseFromJSON(resp: Response): Promise<any> {
|
parseResponseFromJSON(resp: Response): Promise<any> {
|
||||||
this.check403(resp)
|
this.check403(resp)
|
||||||
|
|
||||||
|
if (resp.status === 204) {
|
||||||
|
// We can't expect content here
|
||||||
|
return new Promise(resolve => {
|
||||||
|
resolve({})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return resp.json()
|
return resp.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router'
|
import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
import BothAuth from './components/botauth.vue'
|
import BothAuth from './components/botauth.vue'
|
||||||
|
import ChannelOverview from './components/channelOverview.vue'
|
||||||
|
import ChannelPermissions from './components/channelPermissions.vue'
|
||||||
import Dashboard from './components/dashboard.vue'
|
import Dashboard from './components/dashboard.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
@ -8,23 +10,24 @@ const routes = [
|
||||||
|
|
||||||
// General settings
|
// General settings
|
||||||
{ component: BothAuth, name: 'botAuth', path: '/bot-auth' },
|
{ component: BothAuth, name: 'botAuth', path: '/bot-auth' },
|
||||||
{ component: {}, name: 'channels', path: '/channels' },
|
{ component: ChannelOverview, name: 'channels', path: '/channels' },
|
||||||
|
{ component: ChannelPermissions, name: 'channelPermissions', path: '/channels/:channel/permissions', props: true },
|
||||||
{ component: {}, name: 'editors', path: '/editors' },
|
{ component: {}, name: 'editors', path: '/editors' },
|
||||||
{ component: {}, name: 'tokens', path: '/tokens' },
|
{ component: {}, name: 'tokens', path: '/tokens' },
|
||||||
|
|
||||||
// Auto-Messages
|
// Auto-Messages
|
||||||
{ component: {}, name: 'autoMessagesList', path: '/auto-messages' },
|
{ component: {}, name: 'autoMessagesList', path: '/auto-messages' },
|
||||||
{ component: {}, name: 'autoMessageEdit', path: '/auto-messages/edit/{id}' },
|
{ component: {}, name: 'autoMessageEdit', path: '/auto-messages/edit/:id' },
|
||||||
{ component: {}, name: 'autoMessageNew', path: '/auto-messages/new' },
|
{ component: {}, name: 'autoMessageNew', path: '/auto-messages/new' },
|
||||||
|
|
||||||
// Rules
|
// Rules
|
||||||
{ component: {}, name: 'rulesList', path: '/rules' },
|
{ component: {}, name: 'rulesList', path: '/rules' },
|
||||||
{ component: {}, name: 'rulesEdit', path: '/rules/edit/{id}' },
|
{ component: {}, name: 'rulesEdit', path: '/rules/edit/:id' },
|
||||||
{ component: {}, name: 'rulesNew', path: '/rules/new' },
|
{ component: {}, name: 'rulesNew', path: '/rules/new' },
|
||||||
|
|
||||||
// Raffles
|
// Raffles
|
||||||
{ component: {}, name: 'rafflesList', path: '/raffles' },
|
{ component: {}, name: 'rafflesList', path: '/raffles' },
|
||||||
{ component: {}, name: 'rafflesEdit', path: '/raffles/edit/{id}' },
|
{ component: {}, name: 'rafflesEdit', path: '/raffles/edit/:id' },
|
||||||
{ component: {}, name: 'rafflesNew', path: '/raffles/new' },
|
{ component: {}, name: 'rafflesNew', path: '/raffles/new' },
|
||||||
] as RouteRecordRaw[]
|
] as RouteRecordRaw[]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue