mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-01 12:33:42 +00:00
feat: new install & set-system-user-password flags for setting up DB schema and setting system user password
- fixes issues in auth redirect to login, by making simple session cookie auto create = true. - fixes issues with conversation tab filters WS subscription. - fixes nulls returned for empty team list in handlers - fixes logout btn not working. - fixes charts error when there's no data returned from the API. - Updates schema.sql
This commit is contained in:
2
Makefile
2
Makefile
@@ -5,7 +5,7 @@ VERSION := $(shell git describe --tags)
|
|||||||
BUILDSTR := ${VERSION} (Commit: ${LAST_COMMIT_DATE} (${LAST_COMMIT}), Build: $(shell date +"%Y-%m-%d %H:%M:%S %z"))
|
BUILDSTR := ${VERSION} (Commit: ${LAST_COMMIT_DATE} (${LAST_COMMIT}), Build: $(shell date +"%Y-%m-%d %H:%M:%S %z"))
|
||||||
|
|
||||||
BIN_ARTEMIS := artemis.bin
|
BIN_ARTEMIS := artemis.bin
|
||||||
STATIC := frontend/dist i18n
|
STATIC := frontend/dist i18n schema.sql
|
||||||
GOPATH ?= $(HOME)/go
|
GOPATH ?= $(HOME)/go
|
||||||
STUFFBIN ?= $(GOPATH)/bin/stuffbin
|
STUFFBIN ?= $(GOPATH)/bin/stuffbin
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ func initFlags() {
|
|||||||
f.StringSlice("config", []string{"config.toml"},
|
f.StringSlice("config", []string{"config.toml"},
|
||||||
"path to one or more config files (will be merged in order)")
|
"path to one or more config files (will be merged in order)")
|
||||||
f.Bool("version", false, "show current version of the build")
|
f.Bool("version", false, "show current version of the build")
|
||||||
|
f.Bool("install", false, "setup database")
|
||||||
|
f.Bool("set-system-user-password", false, "set password for the system user")
|
||||||
|
|
||||||
if err := f.Parse(os.Args[1:]); err != nil {
|
if err := f.Parse(os.Args[1:]); err != nil {
|
||||||
log.Fatalf("loading flags: %v", err)
|
log.Fatalf("loading flags: %v", err)
|
||||||
@@ -151,7 +153,7 @@ func loadSettings(m *setting.Manager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSettingsManager(db *sqlx.DB) *setting.Manager {
|
func initSettings(db *sqlx.DB) *setting.Manager {
|
||||||
s, err := setting.New(setting.Opts{
|
s, err := setting.New(setting.Opts{
|
||||||
DB: db,
|
DB: db,
|
||||||
Lo: initLogger("settings"),
|
Lo: initLogger("settings"),
|
||||||
|
|||||||
68
cmd/install.go
Normal file
68
cmd/install.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/abhinavxd/artemis/internal/user"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/knadh/stuffbin"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// install checks if the schema is already installed, prompts for confirmation, and installs the schema if needed.
|
||||||
|
func install(db *sqlx.DB, fs stuffbin.FileSystem) error {
|
||||||
|
installed, err := checkSchema(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error checking db schema: %v", err)
|
||||||
|
}
|
||||||
|
if installed {
|
||||||
|
fmt.Printf("\033[31m** WARNING: This will wipe your entire DB - '%s' **\033[0m\n", ko.String("db.database"))
|
||||||
|
fmt.Print("Continue (y/n)? ")
|
||||||
|
var ok string
|
||||||
|
fmt.Scanf("%s", &ok)
|
||||||
|
if !strings.EqualFold(ok, "y") {
|
||||||
|
log.Fatalf("installation cancelled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install schema.
|
||||||
|
if err := installSchema(db, fs); err != nil {
|
||||||
|
log.Fatalf("error installing schema: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Schema installed successfully")
|
||||||
|
|
||||||
|
// Create system user.
|
||||||
|
if err := user.CreateSystemUser(db); err != nil {
|
||||||
|
log.Fatalf("error creating system user: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSystemUserPass prompts for pass and sets system user password.
|
||||||
|
func setSystemUserPass(db *sqlx.DB) {
|
||||||
|
user.ChangeSystemUserPassword(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSchema verifies if the DB schema is already installed by querying a table.
|
||||||
|
func checkSchema(db *sqlx.DB) (bool, error) {
|
||||||
|
if _, err := db.Exec(`SELECT * FROM settings LIMIT 1`); err != nil {
|
||||||
|
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "42P01" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// installSchema reads the schema file and installs it in the database.
|
||||||
|
func installSchema(db *sqlx.DB, fs stuffbin.FileSystem) error {
|
||||||
|
q, err := fs.Read("/schema.sql")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = db.Exec(string(q))
|
||||||
|
return err
|
||||||
|
}
|
||||||
31
cmd/main.go
31
cmd/main.go
@@ -69,18 +69,41 @@ func main() {
|
|||||||
// Load the config files into Koanf.
|
// Load the config files into Koanf.
|
||||||
initConfig(ko)
|
initConfig(ko)
|
||||||
|
|
||||||
|
// Init stuffbin fs.
|
||||||
|
fs := initFS()
|
||||||
|
|
||||||
// Init DB.
|
// Init DB.
|
||||||
db := initDB()
|
db := initDB()
|
||||||
|
|
||||||
|
// Installer.
|
||||||
|
if ko.Bool("install") {
|
||||||
|
install(db, fs)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ko.Bool("set-system-user-password") {
|
||||||
|
setSystemUserPass(db)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if schema is installed.
|
||||||
|
installed, err := checkSchema(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error checking db schema: %v", err)
|
||||||
|
}
|
||||||
|
if !installed {
|
||||||
|
log.Println("Database tables are missing. Use the `--install` flag to set up the database schema.")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// Load app settings from DB into Koanf.
|
// Load app settings from DB into Koanf.
|
||||||
setting := initSettingsManager(db)
|
settings := initSettings(db)
|
||||||
loadSettings(setting)
|
loadSettings(settings)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
shutdownCh = make(chan struct{})
|
shutdownCh = make(chan struct{})
|
||||||
ctx, stop = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
ctx, stop = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
wsHub = ws.NewHub()
|
wsHub = ws.NewHub()
|
||||||
fs = initFS()
|
|
||||||
i18n = initI18n(fs)
|
i18n = initI18n(fs)
|
||||||
lo = initLogger("artemis")
|
lo = initLogger("artemis")
|
||||||
rdb = initRedis()
|
rdb = initRedis()
|
||||||
@@ -127,7 +150,7 @@ func main() {
|
|||||||
fs: fs,
|
fs: fs,
|
||||||
i18n: i18n,
|
i18n: i18n,
|
||||||
media: media,
|
media: media,
|
||||||
setting: setting,
|
setting: settings,
|
||||||
contact: contact,
|
contact: contact,
|
||||||
inbox: inbox,
|
inbox: inbox,
|
||||||
user: user,
|
user: user,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func perm(handler fastglue.FastRequestHandler, obj, act string) fastglue.FastReq
|
|||||||
return r.SendErrorEnvelope(http.StatusForbidden, "Permission denied", nil, envelope.PermissionError)
|
return r.SendErrorEnvelope(http.StatusForbidden, "Permission denied", nil, envelope.PermissionError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the handler.
|
// Return handler.
|
||||||
return handler(r)
|
return handler(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,10 +88,10 @@ func noAuthPage(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler
|
|||||||
return handler(r)
|
return handler(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// User is logged in direct if `next` is available else redirect to the dashboard.
|
// User is logged in direct if `next` is available else redirect.
|
||||||
nextURI := string(r.RequestCtx.QueryArgs().Peek("next"))
|
nextURI := string(r.RequestCtx.QueryArgs().Peek("next"))
|
||||||
if len(nextURI) == 0 {
|
if len(nextURI) == 0 {
|
||||||
nextURI = "/dashboard"
|
nextURI = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.RedirectURI(nextURI, fasthttp.StatusFound, nil, "")
|
return r.RedirectURI(nextURI, fasthttp.StatusFound, nil, "")
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ const allNavLinks = ref([
|
|||||||
|
|
||||||
const bottomLinks = ref([
|
const bottomLinks = ref([
|
||||||
{
|
{
|
||||||
to: '/logout',
|
to: '/api/logout',
|
||||||
|
isLink: false,
|
||||||
icon: 'lucide:log-out',
|
icon: 'lucide:log-out',
|
||||||
title: 'Logout'
|
title: 'Logout'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,29 +26,21 @@ const getButtonVariant = (to) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :data-collapsed="isCollapsed" class="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2 h-full">
|
||||||
:data-collapsed="isCollapsed"
|
<nav class="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
||||||
class="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2 h-full"
|
|
||||||
>
|
|
||||||
<nav
|
|
||||||
class="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2"
|
|
||||||
>
|
|
||||||
<template v-for="(link, index) of links">
|
<template v-for="(link, index) of links">
|
||||||
<!-- Collapsed -->
|
<!-- Collapsed -->
|
||||||
<router-link :to="link.to" v-if="isCollapsed" :key="`1-${index}`">
|
<router-link :to="link.to" v-if="isCollapsed" :key="`1-${index}`">
|
||||||
<TooltipProvider :delay-duration="10">
|
<TooltipProvider :delay-duration="10">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span
|
<span :class="cn(
|
||||||
:class="
|
buttonVariants({ variant: getButtonVariant(link.to), size: 'icon' }),
|
||||||
cn(
|
'h-9 w-9',
|
||||||
buttonVariants({ variant: getButtonVariant(link.to), size: 'icon' }),
|
link.variant === getButtonVariant(link.to) &&
|
||||||
'h-9 w-9',
|
'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'
|
||||||
link.variant === getButtonVariant(link.to) &&
|
)
|
||||||
'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'
|
">
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Icon :icon="link.icon" class="size-5" />
|
<Icon :icon="link.icon" class="size-5" />
|
||||||
<span class="sr-only">{{ link.title }}</span>
|
<span class="sr-only">{{ link.title }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -64,30 +56,20 @@ const getButtonVariant = (to) => {
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<!-- Expanded -->
|
<!-- Expanded -->
|
||||||
<router-link
|
<router-link v-else :to="link.to" :key="`2-${index}`" :class="cn(
|
||||||
v-else
|
buttonVariants({ variant: getButtonVariant(link.to), size: 'sm' }),
|
||||||
:to="link.to"
|
link.variant === getButtonVariant(link.to) &&
|
||||||
:key="`2-${index}`"
|
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||||
:class="
|
'justify-start'
|
||||||
cn(
|
)
|
||||||
buttonVariants({ variant: getButtonVariant(link.to), size: 'sm' }),
|
">
|
||||||
link.variant === getButtonVariant(link.to) &&
|
|
||||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
|
||||||
'justify-start'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Icon :icon="link.icon" class="mr-2 size-5" />
|
<Icon :icon="link.icon" class="mr-2 size-5" />
|
||||||
{{ link.title }}
|
{{ link.title }}
|
||||||
<span
|
<span v-if="link.label" :class="cn(
|
||||||
v-if="link.label"
|
'ml-',
|
||||||
:class="
|
link.variant === getButtonVariant(link.to) && 'text-background dark:text-white'
|
||||||
cn(
|
)
|
||||||
'ml-',
|
">
|
||||||
link.variant === getButtonVariant(link.to) && 'text-background dark:text-white'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ link.label }}
|
{{ link.label }}
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -101,24 +83,20 @@ const getButtonVariant = (to) => {
|
|||||||
<TooltipProvider :delay-duration="10">
|
<TooltipProvider :delay-duration="10">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<router-link
|
<a :href="bottomLink.to" :class="cn(
|
||||||
:to="bottomLink.to"
|
buttonVariants({
|
||||||
:class="
|
variant: getButtonVariant(bottomLink.to),
|
||||||
cn(
|
size: isCollapsed ? 'icon' : 'sm'
|
||||||
buttonVariants({
|
}),
|
||||||
variant: getButtonVariant(bottomLink.to),
|
bottomLink.variant === getButtonVariant(bottomLink.to) &&
|
||||||
size: isCollapsed ? 'icon' : 'sm'
|
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||||
}),
|
'justify-start'
|
||||||
bottomLink.variant === getButtonVariant(bottomLink.to) &&
|
)
|
||||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
">
|
||||||
'justify-start'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Icon :icon="bottomLink.icon" class="mr-2 size-5" v-if="!isCollapsed" />
|
<Icon :icon="bottomLink.icon" class="mr-2 size-5" v-if="!isCollapsed" />
|
||||||
<span v-if="!isCollapsed">{{ bottomLink.title }}</span>
|
<span v-if="!isCollapsed">{{ bottomLink.title }}</span>
|
||||||
<Icon :icon="bottomLink.icon" class="size-5 mx-auto" v-else />
|
<Icon :icon="bottomLink.icon" class="size-5 mx-auto" v-else />
|
||||||
</router-link>
|
</a>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" class="flex items-center gap-4">
|
<TooltipContent side="right" class="flex items-center gap-4">
|
||||||
{{ bottomLink.title }}
|
{{ bottomLink.title }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<Tabs v-model="conversationType">
|
<Tabs v-model="conversationType">
|
||||||
<TabsList class="w-full flex justify-evenly">
|
<TabsList class="w-full flex justify-evenly">
|
||||||
<TabsTrigger value="assigned" class="w-full"> Assigned </TabsTrigger>
|
<TabsTrigger value="assigned" class="w-full"> Assigned </TabsTrigger>
|
||||||
<TabsTrigger value="unassigned" class="w-full"> Unassigned </TabsTrigger>
|
<TabsTrigger value="team" class="w-full"> Team </TabsTrigger>
|
||||||
<TabsTrigger value="all" class="w-full"> All </TabsTrigger>
|
<TabsTrigger value="all" class="w-full"> All </TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { LineChart } from '@/components/ui/chart-line'
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export const CONVERSATION_FILTERS = {
|
|||||||
|
|
||||||
export const CONVERSATION_LIST_TYPE = {
|
export const CONVERSATION_LIST_TYPE = {
|
||||||
ASSIGNED: 'assigned',
|
ASSIGNED: 'assigned',
|
||||||
UNASSIGNED: 'unassigned',
|
UNASSIGNED: 'team',
|
||||||
ALL: 'all'
|
ALL: 'all'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ const getCardStats = () => {
|
|||||||
const getDashboardCharts = () => {
|
const getDashboardCharts = () => {
|
||||||
return api.getGlobalDashboardCharts()
|
return api.getGlobalDashboardCharts()
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
chartData.value = resp.data.data
|
chartData.value.new_conversations = resp.data.data.new_conversations || []
|
||||||
|
chartData.value.status_summary = resp.data.data.status_summary || []
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const redirectToOIDC = (provider) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
if (!validateEmail(loginForm.value.email)) {
|
if (!validateEmail(loginForm.value.email) && loginForm.value.email !== 'System') {
|
||||||
errorMessage.value = 'Invalid email address.'
|
errorMessage.value = 'Invalid email address.'
|
||||||
useTemporaryClass('login-container', 'animate-shake')
|
useTemporaryClass('login-container', 'animate-shake')
|
||||||
return false
|
return false
|
||||||
@@ -144,7 +144,10 @@ const enabledOIDCProviders = computed(() => {
|
|||||||
return oidcProviders.value.filter((provider) => !provider.disabled)
|
return oidcProviders.value.filter((provider) => !provider.disabled)
|
||||||
})
|
})
|
||||||
|
|
||||||
const emailHasError = computed(() => !validateEmail(loginForm.value.email) && loginForm.value.email !== '')
|
const emailHasError = computed(() => {
|
||||||
|
const email = loginForm.value.email;
|
||||||
|
return email !== 'System' && !validateEmail(email) && email !== '';
|
||||||
|
})
|
||||||
const passwordHasError = computed(() => !loginForm.value.password && loginForm.value.password !== '')
|
const passwordHasError = computed(() => !loginForm.value.password && loginForm.value.password !== '')
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -75,12 +75,12 @@ func New(cfg Config, rd *redis.Client, logger *logf.Logger) (*Auth, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sess := simplesessions.New(simplesessions.Options{
|
sess := simplesessions.New(simplesessions.Options{
|
||||||
EnableAutoCreate: false,
|
EnableAutoCreate: true,
|
||||||
SessionIDLength: 64,
|
SessionIDLength: 64,
|
||||||
Cookie: simplesessions.CookieOptions{
|
Cookie: simplesessions.CookieOptions{
|
||||||
IsHTTPOnly: true,
|
IsHTTPOnly: true,
|
||||||
IsSecure: true,
|
IsSecure: true,
|
||||||
MaxAge: time.Hour * 12,
|
MaxAge: time.Hour * 6,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -166,12 +166,14 @@ func (a *Auth) SaveSession(user models.User, r *fastglue.Request) error {
|
|||||||
func (a *Auth) ValidateSession(r *fastglue.Request) (models.User, error) {
|
func (a *Auth) ValidateSession(r *fastglue.Request) (models.User, error) {
|
||||||
sess, err := a.sess.Acquire(r.RequestCtx, r, r)
|
sess, err := a.sess.Acquire(r.RequestCtx, r, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
a.logger.Error("error acquiring session", "error", err)
|
||||||
return models.User{}, err
|
return models.User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the session variables
|
// Get the session variables
|
||||||
sessVals, err := sess.GetMulti("id", "email", "first_name", "last_name")
|
sessVals, err := sess.GetMulti("id", "email", "first_name", "last_name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
a.logger.Error("error fetching session variables", "error", err)
|
||||||
return models.User{}, err
|
return models.User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +186,6 @@ func (a *Auth) ValidateSession(r *fastglue.Request) (models.User, error) {
|
|||||||
|
|
||||||
// Logged in?
|
// Logged in?
|
||||||
if userID <= 0 {
|
if userID <= 0 {
|
||||||
a.logger.Error("error fetching session", "error", err)
|
|
||||||
return models.User{}, err
|
return models.User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ func (a *Auth) DestroySession(r *fastglue.Request) error {
|
|||||||
a.logger.Error("error acquiring session", "error", err)
|
a.logger.Error("error acquiring session", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := sess.Destroy(); err != nil {
|
if err := sess.Clear(); err != nil {
|
||||||
a.logger.Error("error clearing session", "error", err)
|
a.logger.Error("error clearing session", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,22 +322,8 @@ SELECT
|
|||||||
m.uuid,
|
m.uuid,
|
||||||
m.private,
|
m.private,
|
||||||
m.sender_type,
|
m.sender_type,
|
||||||
u.uuid as sender_uuid,
|
u.uuid as sender_uuid
|
||||||
COALESCE(
|
|
||||||
json_agg(
|
|
||||||
json_build_object(
|
|
||||||
'name', a.filename,
|
|
||||||
'content_type', a.content_type,
|
|
||||||
'uuid', a.uuid,
|
|
||||||
'size', a.size
|
|
||||||
) ORDER BY a.filename
|
|
||||||
) FILTER (WHERE a.message_id IS NOT NULL),
|
|
||||||
'[]'::json
|
|
||||||
) AS attachments
|
|
||||||
FROM messages m
|
FROM messages m
|
||||||
LEFT JOIN attachments a
|
|
||||||
ON a.message_id = m.id
|
|
||||||
AND a.content_disposition = 'attachment'
|
|
||||||
LEFT JOIN users u on u.id = m.sender_id
|
LEFT JOIN users u on u.id = m.sender_id
|
||||||
WHERE m.uuid = $1
|
WHERE m.uuid = $1
|
||||||
GROUP BY
|
GROUP BY
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ func (s *Service) worker(ctx context.Context) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
s.lo.Info("worker exiting due to context cancellation")
|
|
||||||
return
|
return
|
||||||
case message := <-s.messageChannel:
|
case message := <-s.messageChannel:
|
||||||
sender, exists := s.providers[message.Provider]
|
sender, exists := s.providers[message.Provider]
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func New(opts Opts) (*Manager, error) {
|
|||||||
|
|
||||||
// GetAll retrieves all tags.
|
// GetAll retrieves all tags.
|
||||||
func (t *Manager) GetAll() ([]models.Tag, error) {
|
func (t *Manager) GetAll() ([]models.Tag, error) {
|
||||||
var tags []models.Tag
|
var tags = make([]models.Tag, 0)
|
||||||
if err := t.q.GetAllTags.Select(&tags); err != nil {
|
if err := t.q.GetAllTags.Select(&tags); err != nil {
|
||||||
t.lo.Error("error fetching tags", "error", err)
|
t.lo.Error("error fetching tags", "error", err)
|
||||||
return nil, envelope.NewError(envelope.GeneralError, "Error fetching tags", nil)
|
return nil, envelope.NewError(envelope.GeneralError, "Error fetching tags", nil)
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ func New(opts Opts) (*Manager, error) {
|
|||||||
|
|
||||||
// GetAll retrieves all teams.
|
// GetAll retrieves all teams.
|
||||||
func (u *Manager) GetAll() ([]models.Team, error) {
|
func (u *Manager) GetAll() ([]models.Team, error) {
|
||||||
var teams []models.Team
|
var teams = make([]models.Team, 0)
|
||||||
if err := u.q.GetTeams.Select(&teams); err != nil {
|
if err := u.q.GetTeams.Select(&teams); err != nil {
|
||||||
if errors.Is(sql.ErrNoRows, err) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return teams, nil
|
return teams, nil
|
||||||
}
|
}
|
||||||
u.lo.Error("error fetching teams from db", "error", err)
|
u.lo.Error("error fetching teams from db", "error", err)
|
||||||
@@ -74,7 +74,7 @@ func (u *Manager) GetAll() ([]models.Team, error) {
|
|||||||
func (u *Manager) GetTeam(id int) (models.Team, error) {
|
func (u *Manager) GetTeam(id int) (models.Team, error) {
|
||||||
var team models.Team
|
var team models.Team
|
||||||
if err := u.q.GetTeam.Get(&team, id); err != nil {
|
if err := u.q.GetTeam.Get(&team, id); err != nil {
|
||||||
if errors.Is(sql.ErrNoRows, err) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
u.lo.Error("team not found", "id", id, "error", err)
|
u.lo.Error("team not found", "id", id, "error", err)
|
||||||
return team, nil
|
return team, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/abhinavxd/artemis/internal/dbutil"
|
"github.com/abhinavxd/artemis/internal/dbutil"
|
||||||
@@ -30,7 +31,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SystemUserUUID = "00000000-0000-0000-0000-000000000000"
|
SystemUserEmail = "System"
|
||||||
|
MinSystemUserPasswordLen = 8
|
||||||
|
MaxSystemUserPasswordLen = 50
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager handles user-related operations.
|
// Manager handles user-related operations.
|
||||||
@@ -48,15 +51,15 @@ type Opts struct {
|
|||||||
|
|
||||||
// queries contains prepared SQL queries.
|
// queries contains prepared SQL queries.
|
||||||
type queries struct {
|
type queries struct {
|
||||||
CreateUser *sqlx.Stmt `query:"create-user"`
|
CreateUser *sqlx.Stmt `query:"create-user"`
|
||||||
GetUsers *sqlx.Stmt `query:"get-users"`
|
GetUsers *sqlx.Stmt `query:"get-users"`
|
||||||
GetUser *sqlx.Stmt `query:"get-user"`
|
GetUser *sqlx.Stmt `query:"get-user"`
|
||||||
GetEmail *sqlx.Stmt `query:"get-email"`
|
GetEmail *sqlx.Stmt `query:"get-email"`
|
||||||
GetPermissions *sqlx.Stmt `query:"get-permissions"`
|
GetPermissions *sqlx.Stmt `query:"get-permissions"`
|
||||||
GetUserByEmail *sqlx.Stmt `query:"get-user-by-email"`
|
GetUserByEmail *sqlx.Stmt `query:"get-user-by-email"`
|
||||||
UpdateUser *sqlx.Stmt `query:"update-user"`
|
UpdateUser *sqlx.Stmt `query:"update-user"`
|
||||||
UpdateAvatar *sqlx.Stmt `query:"update-avatar"`
|
UpdateAvatar *sqlx.Stmt `query:"update-avatar"`
|
||||||
SetUserPassword *sqlx.Stmt `query:"set-user-password"`
|
SetUserPassword *sqlx.Stmt `query:"set-user-password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and returns a new instance of the Manager.
|
// New creates and returns a new instance of the Manager.
|
||||||
@@ -79,7 +82,7 @@ func (u *Manager) Login(email string, password []byte) (models.User, error) {
|
|||||||
var user models.User
|
var user models.User
|
||||||
|
|
||||||
if err := u.q.GetUserByEmail.Get(&user, email); err != nil {
|
if err := u.q.GetUserByEmail.Get(&user, email); err != nil {
|
||||||
if errors.Is(sql.ErrNoRows, err) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return user, envelope.NewError(envelope.InputError, u.i18n.T("user.invalidEmailPassword"), nil)
|
return user, envelope.NewError(envelope.InputError, u.i18n.T("user.invalidEmailPassword"), nil)
|
||||||
}
|
}
|
||||||
u.lo.Error("error fetching user from db", "error", err)
|
u.lo.Error("error fetching user from db", "error", err)
|
||||||
@@ -97,7 +100,7 @@ func (u *Manager) Login(email string, password []byte) (models.User, error) {
|
|||||||
func (u *Manager) GetUsers() ([]models.User, error) {
|
func (u *Manager) GetUsers() ([]models.User, error) {
|
||||||
var users []models.User
|
var users []models.User
|
||||||
if err := u.q.GetUsers.Select(&users); err != nil {
|
if err := u.q.GetUsers.Select(&users); err != nil {
|
||||||
if errors.Is(sql.ErrNoRows, err) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
u.lo.Error("error fetching users from db", "error", err)
|
u.lo.Error("error fetching users from db", "error", err)
|
||||||
@@ -156,7 +159,7 @@ func (u *Manager) GetByEmail(email string) (models.User, error) {
|
|||||||
|
|
||||||
// GetSystemUser retrieves the system user.
|
// GetSystemUser retrieves the system user.
|
||||||
func (u *Manager) GetSystemUser() (models.User, error) {
|
func (u *Manager) GetSystemUser() (models.User, error) {
|
||||||
return u.Get(0, SystemUserUUID)
|
return u.GetByEmail(SystemUserEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAvatar updates the user avatar.
|
// UpdateAvatar updates the user avatar.
|
||||||
@@ -208,22 +211,6 @@ func (u *Manager) verifyPassword(pwd []byte, pwdHash string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setPassword sets a new password for a user.
|
|
||||||
func (u *Manager) setPassword(uid int, pwd string) error {
|
|
||||||
if len(pwd) > 72 {
|
|
||||||
return ErrPasswordTooLong
|
|
||||||
}
|
|
||||||
bytes, err := bcrypt.GenerateFromPassword([]byte(pwd), 12)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := u.q.SetUserPassword.Exec(bytes, uid); err != nil {
|
|
||||||
u.lo.Error("setting password", "error", err)
|
|
||||||
return fmt.Errorf("error setting password")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generatePassword generates a random password and returns its bcrypt hash.
|
// generatePassword generates a random password and returns its bcrypt hash.
|
||||||
func (u *Manager) generatePassword() ([]byte, error) {
|
func (u *Manager) generatePassword() ([]byte, error) {
|
||||||
password, _ := stringutil.RandomAlNumString(16)
|
password, _ := stringutil.RandomAlNumString(16)
|
||||||
@@ -234,3 +221,77 @@ func (u *Manager) generatePassword() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return bytes, nil
|
return bytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateSystemUser inserts a default system user into the users table with the prompted password.
|
||||||
|
func CreateSystemUser(db *sqlx.DB) error {
|
||||||
|
// Prompt for password and get hashed password
|
||||||
|
hashedPassword, err := promptAndHashPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(`
|
||||||
|
INSERT INTO users (email, first_name, last_name, password, roles)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)`,
|
||||||
|
SystemUserEmail, "System", "", hashedPassword, pq.StringArray{"Admin"})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create system user: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("System user created successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeSystemUserPassword updates the system user's password with a newly prompted one.
|
||||||
|
func ChangeSystemUserPassword(db *sqlx.DB) error {
|
||||||
|
// Prompt for password and get hashed password
|
||||||
|
hashedPassword, err := promptAndHashPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update system user's password in the database.
|
||||||
|
if err := updateSystemUserPassword(db, hashedPassword); err != nil {
|
||||||
|
return fmt.Errorf("error updating system user password: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("System user password updated successfully.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// promptAndHashPassword handles password input and validation, and returns the hashed password.
|
||||||
|
func promptAndHashPassword() ([]byte, error) {
|
||||||
|
var password string
|
||||||
|
for {
|
||||||
|
fmt.Print("Please set System admin password (min 8, max 50 characters, at least 1 uppercase letter, 1 number): ")
|
||||||
|
fmt.Scanf("%s", &password)
|
||||||
|
if isValidSystemUserPassword(password) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println("Password does not meet the strength requirements.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the password using bcrypt.
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to hash password: %v", err)
|
||||||
|
}
|
||||||
|
return hashedPassword, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSystemUserPassword updates the password of the system user in the database.
|
||||||
|
func updateSystemUserPassword(db *sqlx.DB, hashedPassword []byte) error {
|
||||||
|
_, err := db.Exec(`UPDATE users SET password = $1 WHERE email = $2`, hashedPassword, SystemUserEmail)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update system user password: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidSystemUserPassword checks if the password meets the required strength for system user.
|
||||||
|
func isValidSystemUserPassword(password string) bool {
|
||||||
|
if len(password) < MinSystemUserPasswordLen || len(password) > MaxSystemUserPasswordLen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hasUppercase := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
||||||
|
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
|
||||||
|
return hasUppercase && hasNumber
|
||||||
|
}
|
||||||
|
|||||||
605
schema.sql
605
schema.sql
@@ -1,442 +1,285 @@
|
|||||||
-- public.attachments definition
|
DROP TYPE IF EXISTS "channels" CASCADE; CREATE TYPE "channels" AS ENUM ('email');
|
||||||
|
DROP TYPE IF EXISTS "media_store" CASCADE; CREATE TYPE "media_store" AS ENUM ('s3', 'localfs');
|
||||||
|
DROP TYPE IF EXISTS "message_type" CASCADE; CREATE TYPE "message_type" AS ENUM ('incoming','outgoing','activity');
|
||||||
|
|
||||||
-- Drop table
|
DROP TABLE IF EXISTS automation_rules CASCADE;
|
||||||
|
CREATE TABLE automation_rules (
|
||||||
-- DROP TABLE public.attachments;
|
id SERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
CREATE TABLE public.attachments (
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
id int8 DEFAULT nextval('media_id_seq'::regclass) NOT NULL,
|
"name" VARCHAR(255) NOT NULL,
|
||||||
"uuid" uuid DEFAULT gen_random_uuid() NULL,
|
description TEXT NULL,
|
||||||
created_at timestamp DEFAULT now() NULL,
|
|
||||||
store varchar(10) DEFAULT ''::text NOT NULL,
|
|
||||||
filename varchar(140) NOT NULL,
|
|
||||||
content_type varchar(140) NOT NULL,
|
|
||||||
message_id int8 NULL,
|
|
||||||
"size" varchar(10) NULL,
|
|
||||||
content_disposition varchar(50) NULL,
|
|
||||||
CONSTRAINT media_pkey PRIMARY KEY (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
-- public.automation_rules definition
|
|
||||||
|
|
||||||
-- Drop table
|
|
||||||
|
|
||||||
-- DROP TABLE public.automation_rules;
|
|
||||||
|
|
||||||
CREATE TABLE public.automation_rules (
|
|
||||||
id int4 DEFAULT nextval('rules_id_seq'::regclass) NOT NULL,
|
|
||||||
"name" varchar(255) NOT NULL,
|
|
||||||
description text NULL,
|
|
||||||
created_at timestamp DEFAULT now() NOT NULL,
|
|
||||||
"type" varchar NOT NULL,
|
"type" varchar NOT NULL,
|
||||||
rules jsonb NULL,
|
rules jsonb NULL,
|
||||||
updated_at timestamp DEFAULT now() NOT NULL,
|
|
||||||
disabled bool DEFAULT false NOT NULL,
|
disabled bool DEFAULT false NOT NULL,
|
||||||
CONSTRAINT rules_pkey PRIMARY KEY (id)
|
CONSTRAINT constraint_automation_rules_on_name CHECK (length("name") <= 100),
|
||||||
|
CONSTRAINT constraint_automation_rules_on_description CHECK (length(description) <= 300)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS canned_responses CASCADE;
|
||||||
-- public.canned_responses definition
|
CREATE TABLE canned_responses (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.canned_responses;
|
title TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
CREATE TABLE public.canned_responses (
|
CONSTRAINT constraint_canned_responses_on_title CHECK (length(title) <= 100),
|
||||||
id serial4 NOT NULL,
|
CONSTRAINT constraint_canned_responses_on_content CHECK (length("content") <= 5000)
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
title text NOT NULL,
|
|
||||||
"content" text NOT NULL,
|
|
||||||
CONSTRAINT canned_responses_pkey PRIMARY KEY (id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS contacts CASCADE;
|
||||||
-- public.contacts definition
|
CREATE TABLE contacts (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.contacts;
|
|
||||||
|
|
||||||
CREATE TABLE public.contacts (
|
|
||||||
id bigserial NOT NULL,
|
|
||||||
created_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
first_name text NULL,
|
first_name TEXT NULL,
|
||||||
last_name text NULL,
|
last_name TEXT NULL,
|
||||||
email varchar NULL,
|
email VARCHAR(254) NULL,
|
||||||
phone_number text NULL,
|
phone_number TEXT NULL,
|
||||||
avatar_url text NULL,
|
avatar_url TEXT NULL,
|
||||||
inbox_id int4 NULL,
|
inbox_id INT NULL,
|
||||||
source_id text NULL,
|
source_id TEXT NULL,
|
||||||
CONSTRAINT contacts_pkey PRIMARY KEY (id)
|
CONSTRAINT constraint_contacts_on_first_name CHECK (length(first_name) <= 100),
|
||||||
|
CONSTRAINT constraint_contacts_on_last_name CHECK (length(last_name) <= 100),
|
||||||
|
CONSTRAINT constraint_contacts_on_email CHECK (length(email) <= 254),
|
||||||
|
CONSTRAINT constraint_contacts_on_phone_number CHECK (length(phone_number) <= 50),
|
||||||
|
CONSTRAINT constraint_contacts_on_avatar_url CHECK (length(avatar_url) <= 1000),
|
||||||
|
CONSTRAINT constraint_contacts_on_source_id CHECK (length(source_id) <= 5000)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS conversation_participants CASCADE;
|
||||||
-- public.conversation_participants definition
|
CREATE TABLE conversation_participants (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.conversation_participants;
|
user_id BIGINT NULL,
|
||||||
|
conversation_id BIGINT NULL,
|
||||||
CREATE TABLE public.conversation_participants (
|
CONSTRAINT constraint_conversation_participants_conversation_id_and_user_id_unique UNIQUE (conversation_id, user_id)
|
||||||
id bigserial NOT NULL,
|
|
||||||
created_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
user_id int8 NULL,
|
|
||||||
conversation_id int8 NULL,
|
|
||||||
CONSTRAINT conversation_participants_pkey PRIMARY KEY (id),
|
|
||||||
CONSTRAINT conversation_participants_unique UNIQUE (conversation_id, user_id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS inboxes CASCADE;
|
||||||
-- public.file_upload_providers definition
|
CREATE TABLE inboxes (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.file_upload_providers;
|
channel "channels" NOT NULL,
|
||||||
|
|
||||||
CREATE TABLE public.file_upload_providers (
|
|
||||||
id serial4 NOT NULL,
|
|
||||||
provider_name text NOT NULL,
|
|
||||||
region text NULL,
|
|
||||||
access_key text NULL,
|
|
||||||
access_secret text NULL,
|
|
||||||
bucket_name text NULL,
|
|
||||||
bucket_type text NULL,
|
|
||||||
bucket_path text NULL,
|
|
||||||
upload_expiry interval NULL,
|
|
||||||
s3_backend_url text NULL,
|
|
||||||
custom_public_url text NULL,
|
|
||||||
upload_path text NULL,
|
|
||||||
upload_uri text NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
CONSTRAINT file_upload_providers_pkey PRIMARY KEY (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
-- public.inboxes definition
|
|
||||||
|
|
||||||
-- Drop table
|
|
||||||
|
|
||||||
-- DROP TABLE public.inboxes;
|
|
||||||
|
|
||||||
CREATE TABLE public.inboxes (
|
|
||||||
id serial4 NOT NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
channel public."channels" NOT NULL,
|
|
||||||
disabled bool DEFAULT false NOT NULL,
|
disabled bool DEFAULT false NOT NULL,
|
||||||
config jsonb DEFAULT '{}'::jsonb NOT NULL,
|
config jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
"name" varchar(140) NOT NULL,
|
"name" VARCHAR(140) NOT NULL,
|
||||||
"from" varchar(200) NULL,
|
"from" VARCHAR(300) NULL,
|
||||||
assign_to_team int4 NULL,
|
assign_to_team INT NULL,
|
||||||
soft_delete bool DEFAULT false NOT NULL,
|
soft_delete bool DEFAULT false NOT NULL
|
||||||
CONSTRAINT inboxes_pkey PRIMARY KEY (id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS media CASCADE;
|
||||||
-- public.media definition
|
CREATE TABLE media (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
|
||||||
-- DROP TABLE public.media;
|
|
||||||
|
|
||||||
CREATE TABLE public.media (
|
|
||||||
id serial4 NOT NULL,
|
|
||||||
created_at timestamp DEFAULT now() NOT NULL,
|
|
||||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
store public."media_store" NOT NULL,
|
store "media_store" NOT NULL,
|
||||||
filename text NOT NULL,
|
filename TEXT NOT NULL,
|
||||||
content_type text NOT NULL,
|
content_type TEXT NOT NULL,
|
||||||
model_id int4 NULL,
|
model_id INT NULL,
|
||||||
model_type text NULL,
|
model_type TEXT NULL,
|
||||||
"size" int4 NULL,
|
"size" INT NULL,
|
||||||
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
|
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
CONSTRAINT media_pkey1 PRIMARY KEY (id)
|
CONSTRAINT constraint_media_on_filename CHECK (length(filename) <= 1000)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS oidc CASCADE;
|
||||||
-- public.oidc definition
|
CREATE TABLE oidc (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
provider_url TEXT NOT NULL,
|
||||||
|
client_id TEXT NOT NULL,
|
||||||
-- DROP TABLE public.oidc;
|
client_secret TEXT NOT NULL,
|
||||||
|
|
||||||
CREATE TABLE public.oidc (
|
|
||||||
id int4 DEFAULT nextval('social_login_id_seq'::regclass) NOT NULL,
|
|
||||||
provider_url text NOT NULL,
|
|
||||||
client_id text NOT NULL,
|
|
||||||
client_secret text NOT NULL,
|
|
||||||
disabled bool DEFAULT false NOT NULL,
|
disabled bool DEFAULT false NOT NULL,
|
||||||
created_at timestamp DEFAULT now() NOT NULL,
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
updated_at timestamp DEFAULT now() NOT NULL,
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
provider varchar NULL,
|
provider VARCHAR NULL,
|
||||||
"name" text NULL,
|
"name" TEXT NULL
|
||||||
CONSTRAINT social_login_pkey PRIMARY KEY (id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS priority CASCADE;
|
||||||
-- public.priority definition
|
CREATE TABLE priority (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
"name" TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.priority;
|
CONSTRAINT constraint_priority_on_name_unique UNIQUE ("name")
|
||||||
|
|
||||||
CREATE TABLE public.priority (
|
|
||||||
id serial4 NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
CONSTRAINT priority_pkey PRIMARY KEY (id),
|
|
||||||
CONSTRAINT priority_priority_name_key UNIQUE (name)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS roles CASCADE;
|
||||||
-- public.roles definition
|
CREATE TABLE roles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
permissions _text DEFAULT '{}'::text [] NOT NULL,
|
||||||
|
"name" TEXT NULL,
|
||||||
-- DROP TABLE public.roles;
|
description TEXT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
CREATE TABLE public.roles (
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
id serial4 NOT NULL,
|
|
||||||
permissions _text DEFAULT '{}'::text[] NOT NULL,
|
|
||||||
"name" text NULL,
|
|
||||||
description text NULL,
|
|
||||||
created_at timestamptz DEFAULT now() NULL,
|
|
||||||
updated_at timestamptz DEFAULT now() NULL,
|
|
||||||
CONSTRAINT roles_pkey PRIMARY KEY (id)
|
|
||||||
);
|
);
|
||||||
|
-- Create roles.
|
||||||
|
INSERT INTO roles
|
||||||
|
(permissions, "name", description)
|
||||||
|
VALUES('{conversations:read_team,conversations:read_all,conversations:read,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,messages:read,messages:write,templates:write,templates:read,roles:delete,roles:write,roles:read,inboxes:delete,inboxes:write,inboxes:read,automations:write,automations:delete,automations:read,teams:write,teams:read,users:write,users:read,dashboard_global:read,canned_responses:delete,tags:delete,canned_responses:write,tags:write,status:delete,status:write,status:read,oidc:delete,oidc:read,oidc:write,settings_notifications:read,settings_notifications:write,settings_general:write,templates:delete}', 'Admin', 'Role for users who have access to the admin panel.');
|
||||||
|
INSERT INTO roles
|
||||||
|
(permissions, "name", description)
|
||||||
|
VALUES('{conversations:read,conversations:read_team,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,status:write,status:delete,tags:write,tags:delete,canned_responses:write,canned_responses:delete,dashboard:global,users:write,users:read,teams:read,teams:write,automations:read,automations:write,automations:delete,inboxes:read,inboxes:write,inboxes:delete,roles:read,roles:write,roles:delete,templates:read,templates:write,messages:read,messages:write,dashboard_global:read,oidc:delete,status:read,oidc:write,settings_notifications:read,oidc:read,settings_general:write,settings_notifications:write,conversations:read_all,templates:delete}', 'Agent', 'Role for all agents with limited access.');
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS settings CASCADE;
|
||||||
-- public.settings definition
|
CREATE TABLE settings (
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
-- Drop table
|
|
||||||
|
|
||||||
-- DROP TABLE public.settings;
|
|
||||||
|
|
||||||
CREATE TABLE public.settings (
|
|
||||||
"key" text NOT NULL,
|
|
||||||
value jsonb DEFAULT '{}'::jsonb NOT NULL,
|
value jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
updated_at timestamptz DEFAULT now() NULL,
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
CONSTRAINT settings_key_key UNIQUE (key)
|
CONSTRAINT settings_key_key UNIQUE ("key")
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_settings_key ON public.settings USING btree (key);
|
CREATE INDEX index_settings_on_key ON settings USING btree ("key");
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS status CASCADE;
|
||||||
-- public.status definition
|
CREATE TABLE status (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
"name" TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.status;
|
CONSTRAINT constraint_status_on_name_unique UNIQUE ("name")
|
||||||
|
|
||||||
CREATE TABLE public.status (
|
|
||||||
id serial4 NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
CONSTRAINT status_pkey PRIMARY KEY (id),
|
|
||||||
CONSTRAINT status_status_name_key UNIQUE (name)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS tags CASCADE;
|
||||||
-- public.tags definition
|
CREATE TABLE tags (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
"name" TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.tags;
|
CONSTRAINT constraint_tags_on_name_unique UNIQUE ("name")
|
||||||
|
|
||||||
CREATE TABLE public.tags (
|
|
||||||
id bigserial NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
CONSTRAINT tags_pkey PRIMARY KEY (id),
|
|
||||||
CONSTRAINT tags_tag_name_key UNIQUE (name)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS team_members CASCADE;
|
||||||
-- public.team_members definition
|
CREATE TABLE team_members (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.team_members;
|
team_id INT NOT NULL,
|
||||||
|
user_id INT NOT NULL,
|
||||||
CREATE TABLE public.team_members (
|
CONSTRAINT constraint_team_members_on_team_id_and_user_id_unique UNIQUE (team_id, user_id)
|
||||||
id serial4 NOT NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
team_id int4 NOT NULL,
|
|
||||||
user_id int4 NOT NULL,
|
|
||||||
CONSTRAINT team_members_pkey PRIMARY KEY (id),
|
|
||||||
CONSTRAINT unique_team_user UNIQUE (team_id, user_id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS teams CASCADE;
|
||||||
-- public.teams definition
|
CREATE TABLE teams (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.teams;
|
"name" VARCHAR(140) NOT NULL,
|
||||||
|
|
||||||
CREATE TABLE public.teams (
|
|
||||||
id serial4 NOT NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
"name" varchar(140) NOT NULL,
|
|
||||||
disabled bool DEFAULT false NOT NULL,
|
disabled bool DEFAULT false NOT NULL,
|
||||||
auto_assign_conversations bool DEFAULT false NOT NULL,
|
auto_assign_conversations bool DEFAULT false NOT NULL,
|
||||||
CONSTRAINT teams_pkey PRIMARY KEY (id),
|
CONSTRAINT constraint_teams_on_name_unique UNIQUE ("name")
|
||||||
CONSTRAINT teams_unique UNIQUE (name)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS templates CASCADE;
|
||||||
-- public.templates definition
|
CREATE TABLE templates (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
body TEXT NOT NULL,
|
||||||
|
|
||||||
-- DROP TABLE public.templates;
|
|
||||||
|
|
||||||
CREATE TABLE public.templates (
|
|
||||||
id serial4 NOT NULL,
|
|
||||||
body text NOT NULL,
|
|
||||||
is_default bool DEFAULT false NOT NULL,
|
is_default bool DEFAULT false NOT NULL,
|
||||||
created_at timestamptz DEFAULT now() NULL,
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
updated_at timestamptz DEFAULT now() NULL,
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
"name" text NULL,
|
"name" TEXT NULL
|
||||||
CONSTRAINT email_templates_pkey PRIMARY KEY (id)
|
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX email_templates_is_default_idx ON public.templates USING btree (is_default) WHERE (is_default = true);
|
CREATE UNIQUE INDEX unique_index_templates_on_is_default_when_is_default_is_true ON templates USING btree (is_default)
|
||||||
|
WHERE (is_default = true);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS users CASCADE;
|
||||||
-- public.uploads definition
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.uploads;
|
email VARCHAR(254) NOT NULL,
|
||||||
|
|
||||||
CREATE TABLE public.uploads (
|
|
||||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
filename text NOT NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
CONSTRAINT uploads_pkey PRIMARY KEY (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
-- public.users definition
|
|
||||||
|
|
||||||
-- Drop table
|
|
||||||
|
|
||||||
-- DROP TABLE public.users;
|
|
||||||
|
|
||||||
CREATE TABLE public.users (
|
|
||||||
id int4 DEFAULT nextval('agents_id_seq'::regclass) NOT NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
email varchar(255) NOT NULL,
|
|
||||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
first_name varchar(100) NOT NULL,
|
first_name VARCHAR(100) NOT NULL,
|
||||||
last_name varchar(100) NULL,
|
last_name VARCHAR(100) NULL,
|
||||||
"password" varchar(150) NULL,
|
"password" VARCHAR(150) NULL,
|
||||||
disabled bool DEFAULT false NOT NULL,
|
disabled bool DEFAULT false NOT NULL,
|
||||||
avatar_url text NULL,
|
avatar_url TEXT NULL,
|
||||||
roles _text DEFAULT '{}'::text[] NOT NULL,
|
roles _text DEFAULT '{}'::text [] NOT NULL,
|
||||||
CONSTRAINT agents_pkey PRIMARY KEY (id),
|
CONSTRAINT constraint_users_on_email_unique UNIQUE (email)
|
||||||
CONSTRAINT users_email_unique UNIQUE (email)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS contact_methods CASCADE;
|
||||||
-- public.contact_methods definition
|
CREATE TABLE contact_methods (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
contact_id BIGINT REFERENCES contacts(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
"source" TEXT NOT NULL,
|
||||||
-- DROP TABLE public.contact_methods;
|
source_id TEXT NOT NULL,
|
||||||
|
inbox_id INT NULL,
|
||||||
CREATE TABLE public.contact_methods (
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
id bigserial NOT NULL,
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
contact_id int8 NOT NULL,
|
CONSTRAINT constraint_contact_methods_on_source_and_source_id_unique UNIQUE (contact_id, source_id)
|
||||||
"source" text NOT NULL,
|
|
||||||
source_id text NOT NULL,
|
|
||||||
inbox_id int4 NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
CONSTRAINT contact_methods_pkey PRIMARY KEY (id),
|
|
||||||
CONSTRAINT unique_contact_method UNIQUE (source, source_id),
|
|
||||||
CONSTRAINT fk_contact FOREIGN KEY (contact_id) REFERENCES public.contacts(id) ON DELETE CASCADE
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS conversations CASCADE;
|
||||||
-- public.conversations definition
|
CREATE TABLE conversations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
-- DROP TABLE public.conversations;
|
|
||||||
|
|
||||||
CREATE TABLE public.conversations (
|
|
||||||
id bigserial NOT NULL,
|
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
updated_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
|
||||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
reference_number text NOT NULL,
|
reference_number TEXT NOT NULL,
|
||||||
closed_at timestamp NULL,
|
contact_id BIGINT NOT NULL,
|
||||||
contact_id int8 NOT NULL,
|
assigned_user_id BIGINT NULL,
|
||||||
assigned_user_id int8 NULL,
|
assigned_team_id BIGINT NULL,
|
||||||
assigned_team_id int8 NULL,
|
inbox_id INT NOT NULL,
|
||||||
resolved_at timestamp NULL,
|
|
||||||
inbox_id int4 NOT NULL,
|
|
||||||
meta jsonb DEFAULT '{}'::json NOT NULL,
|
meta jsonb DEFAULT '{}'::json NOT NULL,
|
||||||
assignee_last_seen_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
|
assignee_last_seen_at TIMESTAMPTZ DEFAULT now(),
|
||||||
first_reply_at timestamp NULL,
|
first_reply_at TIMESTAMPTZ NULL,
|
||||||
status_id int8 DEFAULT 1 NOT NULL,
|
closed_at TIMESTAMPTZ NULL,
|
||||||
priority_id int8 NULL,
|
resolved_at TIMESTAMPTZ NULL,
|
||||||
CONSTRAINT messages_pkey PRIMARY KEY (id),
|
status_id int REFERENCES status(id),
|
||||||
CONSTRAINT conversations_status_id_fkey FOREIGN KEY (status_id) REFERENCES public.status(id)
|
priority_id int REFERENCES priority(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS messages CASCADE;
|
||||||
-- public.messages definition
|
CREATE TABLE messages (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
-- Drop table
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
|
||||||
-- DROP TABLE public.messages;
|
|
||||||
|
|
||||||
CREATE TABLE public.messages (
|
|
||||||
id bigserial NOT NULL,
|
|
||||||
updated_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
"type" text NOT NULL,
|
"type" TEXT NOT NULL,
|
||||||
status text NULL,
|
status TEXT NULL,
|
||||||
conversation_id bigserial NOT NULL,
|
conversation_id BIGSERIAL REFERENCES conversations(id),
|
||||||
"content" text NULL,
|
"content" TEXT NULL,
|
||||||
sender_id int4 NULL,
|
sender_id INT NULL,
|
||||||
private bool NULL,
|
private bool NULL,
|
||||||
content_type varchar(50) DEFAULT false NOT NULL,
|
content_type TEXT,
|
||||||
source_id text NULL,
|
source_id TEXT NOT NULL,
|
||||||
meta jsonb DEFAULT '{}'::jsonb NULL,
|
meta jsonb DEFAULT '{}'::jsonb NULL,
|
||||||
inbox_id int4 NULL,
|
inbox_id INT NULL,
|
||||||
sender_type varchar NULL,
|
sender_type varchar NULL,
|
||||||
created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
CONSTRAINT messages__pkey PRIMARY KEY (id),
|
CONSTRAINT constraint_messages_on_source_id_unique UNIQUE (source_id),
|
||||||
CONSTRAINT messages_unique UNIQUE (source_id),
|
CONSTRAINT constraint_messages_on_content_type CHECK (length(content_type) <= 50)
|
||||||
CONSTRAINT fk_conversation_id FOREIGN KEY (conversation_id) REFERENCES public.conversations(id)
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS conversation_tags CASCADE;
|
||||||
|
CREATE TABLE conversation_tags (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tag_id BIGSERIAL REFERENCES tags(id),
|
||||||
|
conversation_id BIGSERIAL REFERENCES conversations(id),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
CONSTRAINT constraint_conversation_tags_on_conversation_id_and_tag_id_unique UNIQUE (conversation_id, tag_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
-- public.conversation_tags definition
|
-- Default settings
|
||||||
|
INSERT INTO settings ("key", value)
|
||||||
-- Drop table
|
VALUES
|
||||||
|
('app.lang', '"en"'::jsonb),
|
||||||
-- DROP TABLE public.conversation_tags;
|
('app.root_url', '"http://localhost:9009"'::jsonb),
|
||||||
|
('app.site_name', '"Helpdesk"'::jsonb),
|
||||||
CREATE TABLE public.conversation_tags (
|
('app.favicon_url', '""'::jsonb),
|
||||||
id bigserial NOT NULL,
|
('app.max_file_upload_size', '20'::jsonb),
|
||||||
conversation_id int8 DEFAULT nextval('conversation_tags_converastion_id_seq'::regclass) NOT NULL,
|
('app.allowed_file_upload_extensions', '["*"]'::jsonb),
|
||||||
tag_id bigserial NOT NULL,
|
('notification.email.username', '""'::jsonb),
|
||||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
('notification.email.host', '""'::jsonb),
|
||||||
CONSTRAINT conversation_tags_unique UNIQUE (conversation_id, tag_id),
|
('notification.email.port', '587'::jsonb),
|
||||||
CONSTRAINT message_tags_pkey PRIMARY KEY (id),
|
('notification.email.password', '""'::jsonb),
|
||||||
CONSTRAINT message_tags_conversation_id_fkey FOREIGN KEY (conversation_id) REFERENCES public.conversations(id),
|
('notification.email.max_conns', '5'::jsonb),
|
||||||
CONSTRAINT message_tags_tag_id_fkey FOREIGN KEY (tag_id) REFERENCES public.tags(id)
|
('notification.email.idle_timeout', '"30s"'::jsonb),
|
||||||
);
|
('notification.email.wait_timeout', '"30s"'::jsonb),
|
||||||
|
('notification.email.auth_protocol', '""'::jsonb),
|
||||||
|
('notification.email.email_address', '""'::jsonb),
|
||||||
|
('notification.email.max_msg_retries', '3'::jsonb),
|
||||||
|
('notification.email.enabled', 'false'::jsonb);
|
||||||
|
|||||||
Reference in New Issue
Block a user