mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +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"))
|
||||
|
||||
BIN_ARTEMIS := artemis.bin
|
||||
STATIC := frontend/dist i18n
|
||||
STATIC := frontend/dist i18n schema.sql
|
||||
GOPATH ?= $(HOME)/go
|
||||
STUFFBIN ?= $(GOPATH)/bin/stuffbin
|
||||
|
||||
|
@@ -82,6 +82,8 @@ func initFlags() {
|
||||
f.StringSlice("config", []string{"config.toml"},
|
||||
"path to one or more config files (will be merged in order)")
|
||||
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 {
|
||||
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{
|
||||
DB: db,
|
||||
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.
|
||||
initConfig(ko)
|
||||
|
||||
// Init stuffbin fs.
|
||||
fs := initFS()
|
||||
|
||||
// Init DB.
|
||||
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.
|
||||
setting := initSettingsManager(db)
|
||||
loadSettings(setting)
|
||||
settings := initSettings(db)
|
||||
loadSettings(settings)
|
||||
|
||||
var (
|
||||
shutdownCh = make(chan struct{})
|
||||
ctx, stop = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
wsHub = ws.NewHub()
|
||||
fs = initFS()
|
||||
i18n = initI18n(fs)
|
||||
lo = initLogger("artemis")
|
||||
rdb = initRedis()
|
||||
@@ -127,7 +150,7 @@ func main() {
|
||||
fs: fs,
|
||||
i18n: i18n,
|
||||
media: media,
|
||||
setting: setting,
|
||||
setting: settings,
|
||||
contact: contact,
|
||||
inbox: inbox,
|
||||
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)
|
||||
}
|
||||
|
||||
// Call the handler.
|
||||
// Return handler.
|
||||
return handler(r)
|
||||
}
|
||||
}
|
||||
@@ -88,10 +88,10 @@ func noAuthPage(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler
|
||||
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"))
|
||||
if len(nextURI) == 0 {
|
||||
nextURI = "/dashboard"
|
||||
nextURI = "/"
|
||||
}
|
||||
|
||||
return r.RedirectURI(nextURI, fasthttp.StatusFound, nil, "")
|
||||
|
@@ -73,7 +73,8 @@ const allNavLinks = ref([
|
||||
|
||||
const bottomLinks = ref([
|
||||
{
|
||||
to: '/logout',
|
||||
to: '/api/logout',
|
||||
isLink: false,
|
||||
icon: 'lucide:log-out',
|
||||
title: 'Logout'
|
||||
}
|
||||
|
@@ -26,29 +26,21 @@ const getButtonVariant = (to) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:data-collapsed="isCollapsed"
|
||||
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"
|
||||
>
|
||||
<div :data-collapsed="isCollapsed" 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">
|
||||
<!-- Collapsed -->
|
||||
<router-link :to="link.to" v-if="isCollapsed" :key="`1-${index}`">
|
||||
<TooltipProvider :delay-duration="10">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<span
|
||||
:class="
|
||||
cn(
|
||||
buttonVariants({ variant: getButtonVariant(link.to), size: 'icon' }),
|
||||
'h-9 w-9',
|
||||
link.variant === getButtonVariant(link.to) &&
|
||||
'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'
|
||||
)
|
||||
"
|
||||
>
|
||||
<span :class="cn(
|
||||
buttonVariants({ variant: getButtonVariant(link.to), size: 'icon' }),
|
||||
'h-9 w-9',
|
||||
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" />
|
||||
<span class="sr-only">{{ link.title }}</span>
|
||||
</span>
|
||||
@@ -64,30 +56,20 @@ const getButtonVariant = (to) => {
|
||||
</router-link>
|
||||
|
||||
<!-- Expanded -->
|
||||
<router-link
|
||||
v-else
|
||||
:to="link.to"
|
||||
:key="`2-${index}`"
|
||||
:class="
|
||||
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'
|
||||
)
|
||||
"
|
||||
>
|
||||
<router-link v-else :to="link.to" :key="`2-${index}`" :class="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" />
|
||||
{{ link.title }}
|
||||
<span
|
||||
v-if="link.label"
|
||||
:class="
|
||||
cn(
|
||||
'ml-',
|
||||
link.variant === getButtonVariant(link.to) && 'text-background dark:text-white'
|
||||
)
|
||||
"
|
||||
>
|
||||
<span v-if="link.label" :class="cn(
|
||||
'ml-',
|
||||
link.variant === getButtonVariant(link.to) && 'text-background dark:text-white'
|
||||
)
|
||||
">
|
||||
{{ link.label }}
|
||||
</span>
|
||||
</router-link>
|
||||
@@ -101,24 +83,20 @@ const getButtonVariant = (to) => {
|
||||
<TooltipProvider :delay-duration="10">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<router-link
|
||||
:to="bottomLink.to"
|
||||
:class="
|
||||
cn(
|
||||
buttonVariants({
|
||||
variant: getButtonVariant(bottomLink.to),
|
||||
size: isCollapsed ? 'icon' : 'sm'
|
||||
}),
|
||||
bottomLink.variant === getButtonVariant(bottomLink.to) &&
|
||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||
'justify-start'
|
||||
)
|
||||
"
|
||||
>
|
||||
<a :href="bottomLink.to" :class="cn(
|
||||
buttonVariants({
|
||||
variant: getButtonVariant(bottomLink.to),
|
||||
size: isCollapsed ? 'icon' : 'sm'
|
||||
}),
|
||||
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" />
|
||||
<span v-if="!isCollapsed">{{ bottomLink.title }}</span>
|
||||
<Icon :icon="bottomLink.icon" class="size-5 mx-auto" v-else />
|
||||
</router-link>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" class="flex items-center gap-4">
|
||||
{{ bottomLink.title }}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<Tabs v-model="conversationType">
|
||||
<TabsList class="w-full flex justify-evenly">
|
||||
<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>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
@@ -18,7 +18,6 @@ import { LineChart } from '@/components/ui/chart-line'
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
@@ -8,6 +8,6 @@ export const CONVERSATION_FILTERS = {
|
||||
|
||||
export const CONVERSATION_LIST_TYPE = {
|
||||
ASSIGNED: 'assigned',
|
||||
UNASSIGNED: 'unassigned',
|
||||
UNASSIGNED: 'team',
|
||||
ALL: 'all'
|
||||
}
|
||||
|
@@ -69,7 +69,8 @@ const getCardStats = () => {
|
||||
const getDashboardCharts = () => {
|
||||
return api.getGlobalDashboardCharts()
|
||||
.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) => {
|
||||
toast({
|
||||
|
@@ -96,7 +96,7 @@ const redirectToOIDC = (provider) => {
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
if (!validateEmail(loginForm.value.email)) {
|
||||
if (!validateEmail(loginForm.value.email) && loginForm.value.email !== 'System') {
|
||||
errorMessage.value = 'Invalid email address.'
|
||||
useTemporaryClass('login-container', 'animate-shake')
|
||||
return false
|
||||
@@ -144,7 +144,10 @@ const enabledOIDCProviders = computed(() => {
|
||||
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 !== '')
|
||||
|
||||
</script>
|
||||
|
@@ -75,12 +75,12 @@ func New(cfg Config, rd *redis.Client, logger *logf.Logger) (*Auth, error) {
|
||||
}
|
||||
|
||||
sess := simplesessions.New(simplesessions.Options{
|
||||
EnableAutoCreate: false,
|
||||
EnableAutoCreate: true,
|
||||
SessionIDLength: 64,
|
||||
Cookie: simplesessions.CookieOptions{
|
||||
IsHTTPOnly: 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) {
|
||||
sess, err := a.sess.Acquire(r.RequestCtx, r, r)
|
||||
if err != nil {
|
||||
a.logger.Error("error acquiring session", "error", err)
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
// Get the session variables
|
||||
sessVals, err := sess.GetMulti("id", "email", "first_name", "last_name")
|
||||
if err != nil {
|
||||
a.logger.Error("error fetching session variables", "error", err)
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
@@ -184,7 +186,6 @@ func (a *Auth) ValidateSession(r *fastglue.Request) (models.User, error) {
|
||||
|
||||
// Logged in?
|
||||
if userID <= 0 {
|
||||
a.logger.Error("error fetching session", "error", 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)
|
||||
return err
|
||||
}
|
||||
if err := sess.Destroy(); err != nil {
|
||||
if err := sess.Clear(); err != nil {
|
||||
a.logger.Error("error clearing session", "error", err)
|
||||
return err
|
||||
}
|
||||
|
@@ -322,22 +322,8 @@ SELECT
|
||||
m.uuid,
|
||||
m.private,
|
||||
m.sender_type,
|
||||
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
|
||||
u.uuid as sender_uuid
|
||||
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
|
||||
WHERE m.uuid = $1
|
||||
GROUP BY
|
||||
|
@@ -103,7 +103,6 @@ func (s *Service) worker(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.lo.Info("worker exiting due to context cancellation")
|
||||
return
|
||||
case message := <-s.messageChannel:
|
||||
sender, exists := s.providers[message.Provider]
|
||||
|
@@ -52,7 +52,7 @@ func New(opts Opts) (*Manager, error) {
|
||||
|
||||
// GetAll retrieves all tags.
|
||||
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 {
|
||||
t.lo.Error("error fetching tags", "error", err)
|
||||
return nil, envelope.NewError(envelope.GeneralError, "Error fetching tags", nil)
|
||||
|
@@ -59,9 +59,9 @@ func New(opts Opts) (*Manager, error) {
|
||||
|
||||
// GetAll retrieves all teams.
|
||||
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 errors.Is(sql.ErrNoRows, err) {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return teams, nil
|
||||
}
|
||||
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) {
|
||||
var team models.Team
|
||||
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)
|
||||
return team, nil
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/abhinavxd/artemis/internal/dbutil"
|
||||
@@ -30,7 +31,9 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
SystemUserUUID = "00000000-0000-0000-0000-000000000000"
|
||||
SystemUserEmail = "System"
|
||||
MinSystemUserPasswordLen = 8
|
||||
MaxSystemUserPasswordLen = 50
|
||||
)
|
||||
|
||||
// Manager handles user-related operations.
|
||||
@@ -48,15 +51,15 @@ type Opts struct {
|
||||
|
||||
// queries contains prepared SQL queries.
|
||||
type queries struct {
|
||||
CreateUser *sqlx.Stmt `query:"create-user"`
|
||||
GetUsers *sqlx.Stmt `query:"get-users"`
|
||||
GetUser *sqlx.Stmt `query:"get-user"`
|
||||
GetEmail *sqlx.Stmt `query:"get-email"`
|
||||
GetPermissions *sqlx.Stmt `query:"get-permissions"`
|
||||
GetUserByEmail *sqlx.Stmt `query:"get-user-by-email"`
|
||||
UpdateUser *sqlx.Stmt `query:"update-user"`
|
||||
UpdateAvatar *sqlx.Stmt `query:"update-avatar"`
|
||||
SetUserPassword *sqlx.Stmt `query:"set-user-password"`
|
||||
CreateUser *sqlx.Stmt `query:"create-user"`
|
||||
GetUsers *sqlx.Stmt `query:"get-users"`
|
||||
GetUser *sqlx.Stmt `query:"get-user"`
|
||||
GetEmail *sqlx.Stmt `query:"get-email"`
|
||||
GetPermissions *sqlx.Stmt `query:"get-permissions"`
|
||||
GetUserByEmail *sqlx.Stmt `query:"get-user-by-email"`
|
||||
UpdateUser *sqlx.Stmt `query:"update-user"`
|
||||
UpdateAvatar *sqlx.Stmt `query:"update-avatar"`
|
||||
SetUserPassword *sqlx.Stmt `query:"set-user-password"`
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
var users []models.User
|
||||
if err := u.q.GetUsers.Select(&users); err != nil {
|
||||
if errors.Is(sql.ErrNoRows, err) {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return users, nil
|
||||
}
|
||||
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.
|
||||
func (u *Manager) GetSystemUser() (models.User, error) {
|
||||
return u.Get(0, SystemUserUUID)
|
||||
return u.GetByEmail(SystemUserEmail)
|
||||
}
|
||||
|
||||
// UpdateAvatar updates the user avatar.
|
||||
@@ -208,22 +211,6 @@ func (u *Manager) verifyPassword(pwd []byte, pwdHash string) error {
|
||||
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.
|
||||
func (u *Manager) generatePassword() ([]byte, error) {
|
||||
password, _ := stringutil.RandomAlNumString(16)
|
||||
@@ -234,3 +221,77 @@ func (u *Manager) generatePassword() ([]byte, error) {
|
||||
}
|
||||
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 public.attachments;
|
||||
|
||||
CREATE TABLE public.attachments (
|
||||
id int8 DEFAULT nextval('media_id_seq'::regclass) NOT NULL,
|
||||
"uuid" uuid DEFAULT gen_random_uuid() 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,
|
||||
DROP TABLE IF EXISTS automation_rules CASCADE;
|
||||
CREATE TABLE automation_rules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
"type" varchar NOT NULL,
|
||||
rules jsonb NULL,
|
||||
updated_at timestamp DEFAULT now() 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)
|
||||
);
|
||||
|
||||
|
||||
-- public.canned_responses definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.canned_responses;
|
||||
|
||||
CREATE TABLE public.canned_responses (
|
||||
id serial4 NOT NULL,
|
||||
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 canned_responses CASCADE;
|
||||
CREATE TABLE canned_responses (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
title TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
CONSTRAINT constraint_canned_responses_on_title CHECK (length(title) <= 100),
|
||||
CONSTRAINT constraint_canned_responses_on_content CHECK (length("content") <= 5000)
|
||||
);
|
||||
|
||||
|
||||
-- public.contacts definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- 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,
|
||||
DROP TABLE IF EXISTS contacts CASCADE;
|
||||
CREATE TABLE contacts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
first_name text NULL,
|
||||
last_name text NULL,
|
||||
email varchar NULL,
|
||||
phone_number text NULL,
|
||||
avatar_url text NULL,
|
||||
inbox_id int4 NULL,
|
||||
source_id text NULL,
|
||||
CONSTRAINT contacts_pkey PRIMARY KEY (id)
|
||||
first_name TEXT NULL,
|
||||
last_name TEXT NULL,
|
||||
email VARCHAR(254) NULL,
|
||||
phone_number TEXT NULL,
|
||||
avatar_url TEXT NULL,
|
||||
inbox_id INT NULL,
|
||||
source_id TEXT NULL,
|
||||
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)
|
||||
);
|
||||
|
||||
|
||||
-- public.conversation_participants definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.conversation_participants;
|
||||
|
||||
CREATE TABLE public.conversation_participants (
|
||||
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 conversation_participants CASCADE;
|
||||
CREATE TABLE conversation_participants (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
user_id BIGINT NULL,
|
||||
conversation_id BIGINT NULL,
|
||||
CONSTRAINT constraint_conversation_participants_conversation_id_and_user_id_unique UNIQUE (conversation_id, user_id)
|
||||
);
|
||||
|
||||
|
||||
-- public.file_upload_providers definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.file_upload_providers;
|
||||
|
||||
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,
|
||||
DROP TABLE IF EXISTS inboxes CASCADE;
|
||||
CREATE TABLE inboxes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
channel "channels" NOT NULL,
|
||||
disabled bool DEFAULT false NOT NULL,
|
||||
config jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"name" varchar(140) NOT NULL,
|
||||
"from" varchar(200) NULL,
|
||||
assign_to_team int4 NULL,
|
||||
soft_delete bool DEFAULT false NOT NULL,
|
||||
CONSTRAINT inboxes_pkey PRIMARY KEY (id)
|
||||
"name" VARCHAR(140) NOT NULL,
|
||||
"from" VARCHAR(300) NULL,
|
||||
assign_to_team INT NULL,
|
||||
soft_delete bool DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
|
||||
-- public.media definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.media;
|
||||
|
||||
CREATE TABLE public.media (
|
||||
id serial4 NOT NULL,
|
||||
created_at timestamp DEFAULT now() NOT NULL,
|
||||
DROP TABLE IF EXISTS media CASCADE;
|
||||
CREATE TABLE media (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
store public."media_store" NOT NULL,
|
||||
filename text NOT NULL,
|
||||
content_type text NOT NULL,
|
||||
model_id int4 NULL,
|
||||
model_type text NULL,
|
||||
"size" int4 NULL,
|
||||
store "media_store" NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
content_type TEXT NOT NULL,
|
||||
model_id INT NULL,
|
||||
model_type TEXT NULL,
|
||||
"size" INT NULL,
|
||||
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
CONSTRAINT media_pkey1 PRIMARY KEY (id)
|
||||
CONSTRAINT constraint_media_on_filename CHECK (length(filename) <= 1000)
|
||||
);
|
||||
|
||||
|
||||
-- public.oidc definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.oidc;
|
||||
|
||||
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,
|
||||
DROP TABLE IF EXISTS oidc CASCADE;
|
||||
CREATE TABLE oidc (
|
||||
id SERIAL PRIMARY KEY,
|
||||
provider_url TEXT NOT NULL,
|
||||
client_id TEXT NOT NULL,
|
||||
client_secret TEXT NOT NULL,
|
||||
disabled bool DEFAULT false NOT NULL,
|
||||
created_at timestamp DEFAULT now() NOT NULL,
|
||||
updated_at timestamp DEFAULT now() NOT NULL,
|
||||
provider varchar NULL,
|
||||
"name" text NULL,
|
||||
CONSTRAINT social_login_pkey PRIMARY KEY (id)
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
provider VARCHAR NULL,
|
||||
"name" TEXT NULL
|
||||
);
|
||||
|
||||
|
||||
-- public.priority definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.priority;
|
||||
|
||||
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 priority CASCADE;
|
||||
CREATE TABLE priority (
|
||||
id SERIAL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT constraint_priority_on_name_unique UNIQUE ("name")
|
||||
);
|
||||
|
||||
|
||||
-- public.roles definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.roles;
|
||||
|
||||
CREATE TABLE public.roles (
|
||||
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)
|
||||
DROP TABLE IF EXISTS roles CASCADE;
|
||||
CREATE TABLE roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
permissions _text DEFAULT '{}'::text [] NOT NULL,
|
||||
"name" TEXT NULL,
|
||||
description TEXT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
-- 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.');
|
||||
|
||||
|
||||
-- public.settings definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.settings;
|
||||
|
||||
CREATE TABLE public.settings (
|
||||
"key" text NOT NULL,
|
||||
DROP TABLE IF EXISTS settings CASCADE;
|
||||
CREATE TABLE settings (
|
||||
"key" TEXT NOT NULL,
|
||||
value jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
updated_at timestamptz DEFAULT now() NULL,
|
||||
CONSTRAINT settings_key_key UNIQUE (key)
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
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");
|
||||
|
||||
|
||||
-- public.status definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.status;
|
||||
|
||||
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 status CASCADE;
|
||||
CREATE TABLE status (
|
||||
id SERIAL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT constraint_status_on_name_unique UNIQUE ("name")
|
||||
);
|
||||
|
||||
|
||||
-- public.tags definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.tags;
|
||||
|
||||
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 tags CASCADE;
|
||||
CREATE TABLE tags (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT constraint_tags_on_name_unique UNIQUE ("name")
|
||||
);
|
||||
|
||||
|
||||
-- public.team_members definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.team_members;
|
||||
|
||||
CREATE TABLE public.team_members (
|
||||
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 team_members CASCADE;
|
||||
CREATE TABLE team_members (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
team_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
CONSTRAINT constraint_team_members_on_team_id_and_user_id_unique UNIQUE (team_id, user_id)
|
||||
);
|
||||
|
||||
|
||||
-- public.teams definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.teams;
|
||||
|
||||
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,
|
||||
DROP TABLE IF EXISTS teams CASCADE;
|
||||
CREATE TABLE teams (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
"name" VARCHAR(140) NOT NULL,
|
||||
disabled bool DEFAULT false NOT NULL,
|
||||
auto_assign_conversations bool DEFAULT false NOT NULL,
|
||||
CONSTRAINT teams_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT teams_unique UNIQUE (name)
|
||||
CONSTRAINT constraint_teams_on_name_unique UNIQUE ("name")
|
||||
);
|
||||
|
||||
|
||||
-- public.templates definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.templates;
|
||||
|
||||
CREATE TABLE public.templates (
|
||||
id serial4 NOT NULL,
|
||||
body text NOT NULL,
|
||||
DROP TABLE IF EXISTS templates CASCADE;
|
||||
CREATE TABLE templates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
body TEXT NOT NULL,
|
||||
is_default bool DEFAULT false NOT NULL,
|
||||
created_at timestamptz DEFAULT now() NULL,
|
||||
updated_at timestamptz DEFAULT now() NULL,
|
||||
"name" text NULL,
|
||||
CONSTRAINT email_templates_pkey PRIMARY KEY (id)
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
"name" TEXT NULL
|
||||
);
|
||||
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);
|
||||
|
||||
|
||||
-- public.uploads definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.uploads;
|
||||
|
||||
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,
|
||||
DROP TABLE IF EXISTS users CASCADE;
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
email VARCHAR(254) NOT NULL,
|
||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
first_name varchar(100) NOT NULL,
|
||||
last_name varchar(100) NULL,
|
||||
"password" varchar(150) NULL,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NULL,
|
||||
"password" VARCHAR(150) NULL,
|
||||
disabled bool DEFAULT false NOT NULL,
|
||||
avatar_url text NULL,
|
||||
roles _text DEFAULT '{}'::text[] NOT NULL,
|
||||
CONSTRAINT agents_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT users_email_unique UNIQUE (email)
|
||||
avatar_url TEXT NULL,
|
||||
roles _text DEFAULT '{}'::text [] NOT NULL,
|
||||
CONSTRAINT constraint_users_on_email_unique UNIQUE (email)
|
||||
);
|
||||
|
||||
|
||||
-- public.contact_methods definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.contact_methods;
|
||||
|
||||
CREATE TABLE public.contact_methods (
|
||||
id bigserial NOT NULL,
|
||||
contact_id int8 NOT NULL,
|
||||
"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 contact_methods CASCADE;
|
||||
CREATE TABLE contact_methods (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
contact_id BIGINT REFERENCES contacts(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"source" TEXT NOT NULL,
|
||||
source_id TEXT NOT NULL,
|
||||
inbox_id INT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT constraint_contact_methods_on_source_and_source_id_unique UNIQUE (contact_id, source_id)
|
||||
);
|
||||
|
||||
|
||||
-- public.conversations definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- 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,
|
||||
DROP TABLE IF EXISTS conversations CASCADE;
|
||||
CREATE TABLE conversations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
reference_number text NOT NULL,
|
||||
closed_at timestamp NULL,
|
||||
contact_id int8 NOT NULL,
|
||||
assigned_user_id int8 NULL,
|
||||
assigned_team_id int8 NULL,
|
||||
resolved_at timestamp NULL,
|
||||
inbox_id int4 NOT NULL,
|
||||
reference_number TEXT NOT NULL,
|
||||
contact_id BIGINT NOT NULL,
|
||||
assigned_user_id BIGINT NULL,
|
||||
assigned_team_id BIGINT NULL,
|
||||
inbox_id INT NOT NULL,
|
||||
meta jsonb DEFAULT '{}'::json NOT NULL,
|
||||
assignee_last_seen_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
|
||||
first_reply_at timestamp NULL,
|
||||
status_id int8 DEFAULT 1 NOT NULL,
|
||||
priority_id int8 NULL,
|
||||
CONSTRAINT messages_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT conversations_status_id_fkey FOREIGN KEY (status_id) REFERENCES public.status(id)
|
||||
assignee_last_seen_at TIMESTAMPTZ DEFAULT now(),
|
||||
first_reply_at TIMESTAMPTZ NULL,
|
||||
closed_at TIMESTAMPTZ NULL,
|
||||
resolved_at TIMESTAMPTZ NULL,
|
||||
status_id int REFERENCES status(id),
|
||||
priority_id int REFERENCES priority(id)
|
||||
);
|
||||
|
||||
|
||||
-- public.messages definition
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.messages;
|
||||
|
||||
CREATE TABLE public.messages (
|
||||
id bigserial NOT NULL,
|
||||
updated_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
DROP TABLE IF EXISTS messages CASCADE;
|
||||
CREATE TABLE messages (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
status text NULL,
|
||||
conversation_id bigserial NOT NULL,
|
||||
"content" text NULL,
|
||||
sender_id int4 NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
status TEXT NULL,
|
||||
conversation_id BIGSERIAL REFERENCES conversations(id),
|
||||
"content" TEXT NULL,
|
||||
sender_id INT NULL,
|
||||
private bool NULL,
|
||||
content_type varchar(50) DEFAULT false NOT NULL,
|
||||
source_id text NULL,
|
||||
content_type TEXT,
|
||||
source_id TEXT NOT NULL,
|
||||
meta jsonb DEFAULT '{}'::jsonb NULL,
|
||||
inbox_id int4 NULL,
|
||||
inbox_id INT NULL,
|
||||
sender_type varchar NULL,
|
||||
created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
CONSTRAINT messages__pkey PRIMARY KEY (id),
|
||||
CONSTRAINT messages_unique UNIQUE (source_id),
|
||||
CONSTRAINT fk_conversation_id FOREIGN KEY (conversation_id) REFERENCES public.conversations(id)
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
CONSTRAINT constraint_messages_on_source_id_unique UNIQUE (source_id),
|
||||
CONSTRAINT constraint_messages_on_content_type CHECK (length(content_type) <= 50)
|
||||
);
|
||||
|
||||
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
|
||||
|
||||
-- Drop table
|
||||
|
||||
-- DROP TABLE public.conversation_tags;
|
||||
|
||||
CREATE TABLE public.conversation_tags (
|
||||
id bigserial NOT NULL,
|
||||
conversation_id int8 DEFAULT nextval('conversation_tags_converastion_id_seq'::regclass) NOT NULL,
|
||||
tag_id bigserial NOT NULL,
|
||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NULL,
|
||||
CONSTRAINT conversation_tags_unique UNIQUE (conversation_id, tag_id),
|
||||
CONSTRAINT message_tags_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT message_tags_conversation_id_fkey FOREIGN KEY (conversation_id) REFERENCES public.conversations(id),
|
||||
CONSTRAINT message_tags_tag_id_fkey FOREIGN KEY (tag_id) REFERENCES public.tags(id)
|
||||
);
|
||||
-- Default settings
|
||||
INSERT INTO settings ("key", value)
|
||||
VALUES
|
||||
('app.lang', '"en"'::jsonb),
|
||||
('app.root_url', '"http://localhost:9009"'::jsonb),
|
||||
('app.site_name', '"Helpdesk"'::jsonb),
|
||||
('app.favicon_url', '""'::jsonb),
|
||||
('app.max_file_upload_size', '20'::jsonb),
|
||||
('app.allowed_file_upload_extensions', '["*"]'::jsonb),
|
||||
('notification.email.username', '""'::jsonb),
|
||||
('notification.email.host', '""'::jsonb),
|
||||
('notification.email.port', '587'::jsonb),
|
||||
('notification.email.password', '""'::jsonb),
|
||||
('notification.email.max_conns', '5'::jsonb),
|
||||
('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