mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-13 18:53:22 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96b7a9604c | ||
|
|
21f1c3d328 | ||
|
|
84ae933cfb | ||
|
|
b929100d30 |
11
.github/codecov.yml
vendored
Normal file
11
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
threshold: 5%
|
||||||
|
patch: off
|
||||||
|
comment:
|
||||||
|
layout: diff
|
||||||
|
parsers:
|
||||||
|
go:
|
||||||
|
partials_as_hits: true
|
||||||
9
Makefile
9
Makefile
@@ -48,6 +48,15 @@ dev:
|
|||||||
go run . fake-data
|
go run . fake-data
|
||||||
go run . serve
|
go run . serve
|
||||||
|
|
||||||
|
.PHONY: dev-10000
|
||||||
|
dev-10000:
|
||||||
|
@echo "Running..."
|
||||||
|
rm -rf catalyst_data
|
||||||
|
go run . admin create admin@catalyst-soar.com 1234567890
|
||||||
|
go run . set-feature-flags dev
|
||||||
|
go run . fake-data --users 100 --tickets 10000
|
||||||
|
go run . serve
|
||||||
|
|
||||||
.PHONY: dev-ui
|
.PHONY: dev-ui
|
||||||
serve-ui:
|
serve-ui:
|
||||||
cd ui && bun dev --port 3000
|
cd ui && bun dev --port 3000
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func ticketRecords(dao *daos.Dao, users, types []*models.Record, count int) []*m
|
|||||||
record.Set("description", fakeTicketDescription())
|
record.Set("description", fakeTicketDescription())
|
||||||
record.Set("open", gofakeit.Bool())
|
record.Set("open", gofakeit.Bool())
|
||||||
record.Set("schema", `{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`)
|
record.Set("schema", `{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`)
|
||||||
record.Set("state", `{"tlp":"AMBER"}`)
|
record.Set("state", `{"severity":"Medium"}`)
|
||||||
record.Set("owner", random(users).GetId())
|
record.Set("owner", random(users).GetId())
|
||||||
|
|
||||||
records = append(records, record)
|
records = append(records, record)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/daos"
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
@@ -33,7 +35,16 @@ func typeRecords(dao *daos.Dao) []*models.Record {
|
|||||||
record.Set("singular", "Incident")
|
record.Set("singular", "Incident")
|
||||||
record.Set("plural", "Incidents")
|
record.Set("plural", "Incidents")
|
||||||
record.Set("icon", "Flame")
|
record.Set("icon", "Flame")
|
||||||
record.Set("schema", `{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`)
|
record.Set("schema", s(map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"severity": map[string]any{
|
||||||
|
"title": "Severity",
|
||||||
|
"enum": []string{"Low", "Medium", "High"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"severity"},
|
||||||
|
}))
|
||||||
|
|
||||||
records = append(records, record)
|
records = append(records, record)
|
||||||
|
|
||||||
@@ -42,9 +53,24 @@ func typeRecords(dao *daos.Dao) []*models.Record {
|
|||||||
record.Set("singular", "Alert")
|
record.Set("singular", "Alert")
|
||||||
record.Set("plural", "Alerts")
|
record.Set("plural", "Alerts")
|
||||||
record.Set("icon", "AlertTriangle")
|
record.Set("icon", "AlertTriangle")
|
||||||
record.Set("schema", `{"type":"object","properties":{"severity":{"title":"Severity","type":"string"}},"required": ["severity"]}`)
|
record.Set("schema", s(map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"severity": map[string]any{
|
||||||
|
"title": "Severity",
|
||||||
|
"enum": []string{"Low", "Medium", "High"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"severity"},
|
||||||
|
}))
|
||||||
|
|
||||||
records = append(records, record)
|
records = append(records, record)
|
||||||
|
|
||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func s(m map[string]any) string {
|
||||||
|
b, _ := json.Marshal(m) //nolint:errchkjson
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|||||||
49
migrations/7_search_view.go
Normal file
49
migrations/7_search_view.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchViewName = "ticket_search"
|
||||||
|
|
||||||
|
const searchViewQuery = `
|
||||||
|
SELECT
|
||||||
|
tickets.id,
|
||||||
|
tickets.name,
|
||||||
|
tickets.created,
|
||||||
|
tickets.description,
|
||||||
|
tickets.open,
|
||||||
|
tickets.type,
|
||||||
|
tickets.state,
|
||||||
|
users.name as owner_name,
|
||||||
|
group_concat(comments.message) as comment_messages,
|
||||||
|
group_concat(files.name) as file_names,
|
||||||
|
group_concat(links.name) as link_names,
|
||||||
|
group_concat(links.url) as link_urls,
|
||||||
|
group_concat(tasks.name) as task_names,
|
||||||
|
group_concat(timeline.message) as timeline_messages
|
||||||
|
FROM tickets
|
||||||
|
LEFT JOIN comments ON comments.ticket = tickets.id
|
||||||
|
LEFT JOIN files ON files.ticket = tickets.id
|
||||||
|
LEFT JOIN links ON links.ticket = tickets.id
|
||||||
|
LEFT JOIN tasks ON tasks.ticket = tickets.id
|
||||||
|
LEFT JOIN timeline ON timeline.ticket = tickets.id
|
||||||
|
LEFT JOIN users ON users.id = tickets.owner
|
||||||
|
GROUP BY tickets.id
|
||||||
|
`
|
||||||
|
|
||||||
|
func searchViewUp(db dbx.Builder) error {
|
||||||
|
return daos.New(db).SaveCollection(internalView(searchViewName, searchViewQuery))
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchViewDown(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db)
|
||||||
|
|
||||||
|
id, err := dao.FindCollectionByNameOrId(searchViewName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dao.DeleteCollection(id)
|
||||||
|
}
|
||||||
@@ -11,4 +11,5 @@ func Register() {
|
|||||||
migrations.Register(viewsUp, viewsDown, "1700000004_views.go")
|
migrations.Register(viewsUp, viewsDown, "1700000004_views.go")
|
||||||
migrations.Register(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
migrations.Register(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
||||||
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.go")
|
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.go")
|
||||||
|
migrations.Register(searchViewUp, searchViewDown, "1700000007_search_view.go")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build --force",
|
"type-check": "vue-tsc --build --force",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path ../.gitignore",
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
16
ui/src/components/common/CatalystLogo.vue
Normal file
16
ui/src/components/common/CatalystLogo.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img src="@/assets/flask.svg" alt="Catalyst Logo" :class="cn('dark:hidden', props.class)" />
|
||||||
|
<img
|
||||||
|
src="@/assets/flask_white.svg"
|
||||||
|
alt="Catalyst Logo"
|
||||||
|
:class="cn('hidden dark:flex', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,24 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { type HTMLAttributes, onMounted, ref } from 'vue'
|
import { type HTMLAttributes, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes['class']
|
class?: HTMLAttributes['class']
|
||||||
defaultValue?: string | number
|
|
||||||
modelValue?: string | number
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const modelValue = defineModel<string>({
|
||||||
(e: 'update:modelValue', payload: string | number): void
|
default: ''
|
||||||
}>()
|
|
||||||
|
|
||||||
const modelValue = useVModel(props, 'modelValue', emits, {
|
|
||||||
passive: true,
|
|
||||||
defaultValue: props.defaultValue
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const textarea = ref<HTMLElement | null>(null)
|
const textarea = ref<HTMLElement | null>(null)
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
@@ -34,6 +42,26 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(property, key) in schema.properties" :key="key">
|
<div v-for="(property, key) in schema.properties" :key="key">
|
||||||
|
<FormField v-if="property.enum" :name="key" v-slot="{ componentField }" v-model="formdata[key]">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel :for="key" class="text-right">
|
||||||
|
{{ property.title }}
|
||||||
|
</FormLabel>
|
||||||
|
<Select :id="key" class="col-span-3" v-bind="componentField">
|
||||||
|
<SelectTrigger class="font-medium">
|
||||||
|
<SelectValue :placeholder="'Select a ' + property.title" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem v-for="option in property.enum" :key="option" :value="option">
|
||||||
|
{{ option }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
<FormField
|
<FormField
|
||||||
v-if="property.type === 'string'"
|
v-if="property.type === 'string'"
|
||||||
:name="key"
|
:name="key"
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ const selectedItems = ref<string[]>(props.modelValue)
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => selectedItems.value,
|
() => selectedItems.value,
|
||||||
(value) => emit('update:modelValue', value)
|
(value) => emit('update:modelValue', value),
|
||||||
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const filteredItems = computed(() => {
|
const filteredItems = computed(() => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CatalystLogo from '@/components/common/CatalystLogo.vue'
|
||||||
import IncidentNav from '@/components/sidebar/IncidentNav.vue'
|
import IncidentNav from '@/components/sidebar/IncidentNav.vue'
|
||||||
import NavList from '@/components/sidebar/NavList.vue'
|
import NavList from '@/components/sidebar/NavList.vue'
|
||||||
import UserDropDown from '@/components/sidebar/UserDropDown.vue'
|
import UserDropDown from '@/components/sidebar/UserDropDown.vue'
|
||||||
@@ -14,16 +15,8 @@ const catalystStore = useCatalystStore()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-[57px] items-center border-b bg-background">
|
<div class="flex h-[57px] items-center border-b bg-background">
|
||||||
<img
|
<CatalystLogo
|
||||||
src="@/assets/flask.svg"
|
class="size-8"
|
||||||
alt="Catalyst"
|
|
||||||
class="h-8 w-8 dark:hidden"
|
|
||||||
:class="{ 'flex-1': catalystStore.sidebarCollapsed, 'mx-3': !catalystStore.sidebarCollapsed }"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="@/assets/flask_white.svg"
|
|
||||||
alt="Catalyst"
|
|
||||||
class="hidden h-8 w-8 dark:flex"
|
|
||||||
:class="{ 'flex-1': catalystStore.sidebarCollapsed, 'mx-3': !catalystStore.sidebarCollapsed }"
|
:class="{ 'flex-1': catalystStore.sidebarCollapsed, 'mx-3': !catalystStore.sidebarCollapsed }"
|
||||||
/>
|
/>
|
||||||
<h1 class="text-xl font-bold" v-if="!catalystStore.sidebarCollapsed">Catalyst</h1>
|
<h1 class="text-xl font-bold" v-if="!catalystStore.sidebarCollapsed">Catalyst</h1>
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ import {
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
import { Info, LoaderCircle, Search } from 'lucide-vue-next'
|
import { LoaderCircle, Search } from 'lucide-vue-next'
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
@@ -28,7 +27,7 @@ import { computed, ref, watch } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { pb } from '@/lib/pocketbase'
|
import { pb } from '@/lib/pocketbase'
|
||||||
import type { Ticket, Type } from '@/lib/types'
|
import type { SearchTicket, Type } from '@/lib/types'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -48,15 +47,13 @@ const filter = computed(() => {
|
|||||||
let raws: Array<string> = [
|
let raws: Array<string> = [
|
||||||
'name ~ {:search}',
|
'name ~ {:search}',
|
||||||
'description ~ {:search}',
|
'description ~ {:search}',
|
||||||
'owner.name ~ {:search}',
|
'owner_name ~ {:search}',
|
||||||
'owner.email ~ {:search}',
|
'comment_messages ~ {:search}',
|
||||||
'links_via_ticket.name ~ {:search}',
|
'file_names ~ {:search}',
|
||||||
'links_via_ticket.url ~ {:search}',
|
'link_names ~ {:search}',
|
||||||
'tasks_via_ticket.name ~ {:search}',
|
'link_urls ~ {:search}',
|
||||||
'comments_via_ticket.message ~ {:search}',
|
'task_names ~ {:search}',
|
||||||
'files_via_ticket.name ~ {:search}',
|
'timeline_messages ~ {:search}'
|
||||||
'timeline_via_ticket.message ~ {:search}',
|
|
||||||
'state.severity ~ {:search}'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
Object.keys(props.selectedType.schema.properties).forEach((key) => {
|
Object.keys(props.selectedType.schema.properties).forEach((key) => {
|
||||||
@@ -96,12 +93,10 @@ const {
|
|||||||
refetch
|
refetch
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['tickets', filter.value],
|
queryKey: ['tickets', filter.value],
|
||||||
queryFn: (): Promise<ListResult<Ticket>> =>
|
queryFn: (): Promise<ListResult<SearchTicket>> =>
|
||||||
pb.collection('tickets').getList(page.value, perPage.value, {
|
pb.collection('ticket_search').getList(page.value, perPage.value, {
|
||||||
sort: '-created',
|
sort: '-created',
|
||||||
filter: filter.value,
|
filter: filter.value
|
||||||
expand:
|
|
||||||
'type,owner,comments_via_ticket.author,files_via_ticket,timeline_via_ticket,links_via_ticket,tasks_via_ticket.owner'
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -139,9 +134,9 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
|
|||||||
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
|
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
|
||||||
<div class="flex items-center justify-between px-4 pt-2">
|
<div class="flex items-center justify-between px-4 pt-2">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="all"> All</TabsTrigger>
|
<TabsTrigger value="all">All</TabsTrigger>
|
||||||
<TabsTrigger value="open"> Open</TabsTrigger>
|
<TabsTrigger value="open">Open</TabsTrigger>
|
||||||
<TabsTrigger value="closed"> Closed</TabsTrigger>
|
<TabsTrigger value="closed">Closed</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
|
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
|
||||||
<ListFilter class="h-3.5 w-3.5" />
|
<ListFilter class="h-3.5 w-3.5" />
|
||||||
@@ -155,23 +150,6 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
|
|||||||
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
|
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
|
||||||
<Search class="size-4 text-muted-foreground" />
|
<Search class="size-4 text-muted-foreground" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div>
|
|
||||||
<TooltipProvider :delay-duration="0">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<Info class="ml-2 size-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p class="w-64">
|
|
||||||
Search name, description, or owner. Links, tasks, comments, files, and
|
|
||||||
timeline messages are also searched, but cause unreliable results if there are
|
|
||||||
more than 1000 records.
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,7 +200,6 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
|
|||||||
</PaginationListItem>
|
</PaginationListItem>
|
||||||
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<PaginationNext />
|
<PaginationNext />
|
||||||
<PaginationLast />
|
<PaginationLast />
|
||||||
</PaginationList>
|
</PaginationList>
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import ResourceListElement from '@/components/common/ResourceListElement.vue'
|
|||||||
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import type { Ticket } from '@/lib/types'
|
import type { SearchTicket } from '@/lib/types'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
tickets: Array<Ticket>
|
tickets: Array<SearchTicket>
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@ defineProps<{
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
:created="item.created"
|
:created="item.created"
|
||||||
:subtitle="item.expand.owner ? item.expand.owner.name : ''"
|
:subtitle="item.owner_name"
|
||||||
:description="item.description ? item.description.substring(0, 300) : ''"
|
:description="item.description ? item.description.substring(0, 300) : ''"
|
||||||
:active="route.params.id === item.id"
|
:active="route.params.id === item.id"
|
||||||
:to="`/tickets/${item.expand.type.id}/${item.id}`"
|
:to="`/tickets/${item.type}/${item.id}`"
|
||||||
:open="item.open"
|
:open="item.open"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ export interface Ticket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchTicket {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
created: string
|
||||||
|
description: string
|
||||||
|
open: boolean
|
||||||
|
type: string
|
||||||
|
owner_name: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
@@ -126,6 +136,7 @@ export interface JSONSchema {
|
|||||||
title: string
|
title: string
|
||||||
type: string
|
type: string
|
||||||
description?: string
|
description?: string
|
||||||
|
enum?: Array<string>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
required?: Array<string>
|
required?: Array<string>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
|
|
||||||
import DashboardView from '@/views/DashboardView.vue'
|
import DashboardView from '@/views/DashboardView.vue'
|
||||||
import LoginView from '@/views/LoginView.vue'
|
import LoginView from '@/views/LoginView.vue'
|
||||||
|
import PasswordResetView from '@/views/PasswordResetView.vue'
|
||||||
import ReactionView from '@/views/ReactionView.vue'
|
import ReactionView from '@/views/ReactionView.vue'
|
||||||
import TicketView from '@/views/TicketView.vue'
|
import TicketView from '@/views/TicketView.vue'
|
||||||
|
|
||||||
@@ -12,25 +13,30 @@ const router = createRouter({
|
|||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/dashboard'
|
redirect: '/dashboard'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/reactions/:id?',
|
|
||||||
name: 'reactions',
|
|
||||||
component: ReactionView
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
component: DashboardView
|
component: DashboardView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tickets/:type/:id?',
|
||||||
|
name: 'tickets',
|
||||||
|
component: TicketView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/reactions/:id?',
|
||||||
|
name: 'reactions',
|
||||||
|
component: ReactionView
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: LoginView
|
component: LoginView
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tickets/:type/:id?',
|
path: '/password-reset',
|
||||||
name: 'tickets',
|
name: 'password-reset',
|
||||||
component: TicketView
|
component: PasswordResetView
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CatalystLogo from '@/components/common/CatalystLogo.vue'
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
@@ -8,9 +9,11 @@ import { useQuery } from '@tanstack/vue-query'
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import { pb } from '@/lib/pocketbase'
|
import { pb } from '@/lib/pocketbase'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const mail = ref('')
|
const mail = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
|
const errorTitle = ref('')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
@@ -20,6 +23,7 @@ const login = () => {
|
|||||||
window.location.href = '/ui/'
|
window.location.href = '/ui/'
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
errorTitle.value = 'Login failed'
|
||||||
errorMessage.value = error.message
|
errorMessage.value = error.message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -37,7 +41,8 @@ watch(
|
|||||||
mail.value = 'user@catalyst-soar.com'
|
mail.value = 'user@catalyst-soar.com'
|
||||||
password.value = '1234567890'
|
password.value = '1234567890'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -45,12 +50,18 @@ watch(
|
|||||||
<div class="flex h-full w-full flex-1 items-center justify-center">
|
<div class="flex h-full w-full flex-1 items-center justify-center">
|
||||||
<Card class="m-auto w-96">
|
<Card class="m-auto w-96">
|
||||||
<CardHeader class="flex flex-row justify-between">
|
<CardHeader class="flex flex-row justify-between">
|
||||||
<CardTitle>Catalyst</CardTitle>
|
<CardTitle class="flex flex-row">
|
||||||
|
<CatalystLogo class="size-12" />
|
||||||
|
<div>
|
||||||
|
<h1 class="text-lg font-bold">Catalyst</h1>
|
||||||
|
<div class="text-muted-foreground">Login</div>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="flex flex-col gap-4">
|
<CardContent class="flex flex-col gap-4">
|
||||||
<Alert v-if="errorMessage" variant="destructive" class="border-4 p-4">
|
<Alert v-if="errorTitle || errorMessage" variant="destructive" class="border-4 p-4">
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle v-if="errorTitle">{{ errorTitle }}</AlertTitle>
|
||||||
<AlertDescription>{{ errorMessage }}</AlertDescription>
|
<AlertDescription v-if="errorMessage">{{ errorMessage }}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<Input
|
<Input
|
||||||
v-model="mail"
|
v-model="mail"
|
||||||
@@ -66,7 +77,14 @@ watch(
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
@keydown.enter="login"
|
@keydown.enter="login"
|
||||||
/>
|
/>
|
||||||
<Button variant="outline" class="w-full" @click="login"> Login</Button>
|
<Button variant="outline" class="w-full" @click="login">Login</Button>
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'password-reset' }"
|
||||||
|
:class="
|
||||||
|
cn(buttonVariants({ variant: 'link', size: 'default' }), 'w-full text-foreground')
|
||||||
|
"
|
||||||
|
>Reset Password
|
||||||
|
</RouterLink>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
79
ui/src/views/PasswordResetView.vue
Normal file
79
ui/src/views/PasswordResetView.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import CatalystLogo from '@/components/common/CatalystLogo.vue'
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
|
import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { pb } from '@/lib/pocketbase'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface AlertData {
|
||||||
|
variant: 'default' | 'destructive'
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const mail = ref('')
|
||||||
|
const alert = ref<AlertData | null>(null)
|
||||||
|
|
||||||
|
const resetPassword = () => {
|
||||||
|
pb.collection('users')
|
||||||
|
.requestPasswordReset(mail.value)
|
||||||
|
.then(() => {
|
||||||
|
alert.value = {
|
||||||
|
variant: 'default',
|
||||||
|
title: 'Password reset',
|
||||||
|
message: 'Password reset email sent'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert.value = {
|
||||||
|
variant: 'destructive',
|
||||||
|
title: 'Password reset failed',
|
||||||
|
message: error.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full w-full flex-1 items-center justify-center">
|
||||||
|
<Card class="m-auto w-96">
|
||||||
|
<CardHeader class="flex flex-row justify-between">
|
||||||
|
<CardTitle class="flex flex-row">
|
||||||
|
<CatalystLogo class="size-12" />
|
||||||
|
<div>
|
||||||
|
<h1 class="text-lg font-bold">Catalyst</h1>
|
||||||
|
<div class="text-muted-foreground">Password Reset</div>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-col gap-4">
|
||||||
|
<Alert v-if="alert" :variant="alert.variant" class="border-4 p-4">
|
||||||
|
<AlertTitle>{{ alert.title }}</AlertTitle>
|
||||||
|
<AlertDescription>{{ alert.message }}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<div v-else class="flex flex-col gap-4">
|
||||||
|
<Input
|
||||||
|
v-model="mail"
|
||||||
|
type="text"
|
||||||
|
placeholder="Email"
|
||||||
|
class="w-full"
|
||||||
|
@keydown.enter="resetPassword"
|
||||||
|
/>
|
||||||
|
<Button variant="outline" class="w-full" @click="resetPassword">Reset Password</Button>
|
||||||
|
</div>
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'login' }"
|
||||||
|
:class="
|
||||||
|
cn(buttonVariants({ variant: 'link', size: 'default' }), 'w-full text-foreground')
|
||||||
|
"
|
||||||
|
>Back to Login
|
||||||
|
</RouterLink>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user