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:
Abhinav Raut
2024-10-07 04:26:10 +05:30
parent 328f3fec96
commit 4f9118d675
19 changed files with 469 additions and 504 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: () => []
} }
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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