Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
g.POST("/api/v1/contacts/{id}/notes", perm(handleCreateContactNote, "contact_notes:write"))
g.DELETE("/api/v1/contacts/{id}/notes/{note_id}", perm(handleDeleteContactNote, "contact_notes:delete"))

// Organizations.
g.GET("/api/v1/organizations/compact", auth(handleGetOrganizationsCompact))
g.GET("/api/v1/organizations", perm(handleGetOrganizations, "organizations:manage"))
g.GET("/api/v1/organizations/{id}", perm(handleGetOrganization, "organizations:manage"))
g.POST("/api/v1/organizations", perm(handleCreateOrganization, "organizations:manage"))
g.PUT("/api/v1/organizations/{id}", perm(handleUpdateOrganization, "organizations:manage"))
g.DELETE("/api/v1/organizations/{id}", perm(handleDeleteOrganization, "organizations:manage"))

// Teams.
g.GET("/api/v1/teams/compact", auth(handleGetTeamsCompact))
g.GET("/api/v1/teams", perm(handleGetTeams, "teams:manage"))
Expand Down
15 changes: 15 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
notifier "github.com/abhinavxd/libredesk/internal/notification"
emailnotifier "github.com/abhinavxd/libredesk/internal/notification/providers/email"
"github.com/abhinavxd/libredesk/internal/oidc"
"github.com/abhinavxd/libredesk/internal/organization"
"github.com/abhinavxd/libredesk/internal/report"
"github.com/abhinavxd/libredesk/internal/role"
"github.com/abhinavxd/libredesk/internal/search"
Expand Down Expand Up @@ -249,6 +250,20 @@ func initTag(db *sqlx.DB, i18n *i18n.I18n) *tag.Manager {
return mgr
}

// initOrganization inits organization manager.
func initOrganization(db *sqlx.DB, i18n *i18n.I18n) *organization.Manager {
var lo = initLogger("organization_manager")
mgr, err := organization.New(organization.Opts{
DB: db,
Lo: lo,
I18n: i18n,
})
if err != nil {
log.Fatalf("error initializing organizations: %v", err)
}
return mgr
}

// initViews inits view manager.
func initView(db *sqlx.DB, i18n *i18n.I18n) *view.Manager {
var lo = initLogger("view_manager")
Expand Down
3 changes: 3 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/abhinavxd/libredesk/internal/media"
"github.com/abhinavxd/libredesk/internal/oidc"
"github.com/abhinavxd/libredesk/internal/role"
"github.com/abhinavxd/libredesk/internal/organization"
"github.com/abhinavxd/libredesk/internal/setting"
"github.com/abhinavxd/libredesk/internal/tag"
"github.com/abhinavxd/libredesk/internal/team"
Expand Down Expand Up @@ -78,6 +79,7 @@ type App struct {
status *status.Manager
priority *priority.Manager
tag *tag.Manager
organization *organization.Manager
inbox *inbox.Manager
tmpl *template.Manager
macro *macro.Manager
Expand Down Expand Up @@ -247,6 +249,7 @@ func main() {
search: initSearch(db, i18n),
role: initRole(db, i18n),
tag: initTag(db, i18n),
organization: initOrganization(db, i18n),
macro: initMacro(db, i18n),
ai: initAI(db, i18n),
webhook: webhook,
Expand Down
134 changes: 134 additions & 0 deletions cmd/organizations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package main

import (
"strconv"

"github.com/abhinavxd/libredesk/internal/envelope"
omodels "github.com/abhinavxd/libredesk/internal/organization/models"
"github.com/valyala/fasthttp"
"github.com/zerodha/fastglue"
)

// handleGetOrganizations returns a list of organizations with pagination and filtering.
func handleGetOrganizations(r *fastglue.Request) error {
var (
app = r.Context.(*App)
order = string(r.RequestCtx.QueryArgs().Peek("order"))
orderBy = string(r.RequestCtx.QueryArgs().Peek("order_by"))
filters = string(r.RequestCtx.QueryArgs().Peek("filters"))
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
total = 0
)

organizations, err := app.organization.GetAll(page, pageSize, order, orderBy, filters)
if err != nil {
return sendErrorEnvelope(r, err)
}

if len(organizations) > 0 {
total = organizations[0].Total
}

return r.SendEnvelope(envelope.PageResults{
Results: organizations,
Total: total,
PerPage: pageSize,
TotalPages: (total + pageSize - 1) / pageSize,
Page: page,
})
}

// handleGetOrganization returns a single organization by ID.
func handleGetOrganization(r *fastglue.Request) error {
var (
app = r.Context.(*App)
id = r.RequestCtx.UserValue("id").(string)
)

if id == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
}

org, err := app.organization.Get(id)
if err != nil {
return sendErrorEnvelope(r, err)
}

return r.SendEnvelope(org)
}

// handleCreateOrganization creates a new organization.
func handleCreateOrganization(r *fastglue.Request) error {
var (
app = r.Context.(*App)
org = omodels.Organization{}
)

if err := r.Decode(&org, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), err.Error(), envelope.InputError)
}

createdOrg, err := app.organization.Create(org)
if err != nil {
return sendErrorEnvelope(r, err)
}

return r.SendEnvelope(createdOrg)
}

// handleUpdateOrganization updates an existing organization.
func handleUpdateOrganization(r *fastglue.Request) error {
var (
app = r.Context.(*App)
id = r.RequestCtx.UserValue("id").(string)
org = omodels.Organization{}
)

if id == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
}

if err := r.Decode(&org, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), err.Error(), envelope.InputError)
}

updatedOrg, err := app.organization.Update(id, org)
if err != nil {
return sendErrorEnvelope(r, err)
}

return r.SendEnvelope(updatedOrg)
}

// handleDeleteOrganization deletes an organization.
func handleDeleteOrganization(r *fastglue.Request) error {
var (
app = r.Context.(*App)
id = r.RequestCtx.UserValue("id").(string)
)

if id == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
}

if err := app.organization.Delete(id); err != nil {
return sendErrorEnvelope(r, err)
}

return r.SendEnvelope(true)
}

// handleGetOrganizationsCompact returns a compact list of all organizations (for dropdowns).
func handleGetOrganizationsCompact(r *fastglue.Request) error {
var (
app = r.Context.(*App)
)

organizations, err := app.organization.GetCompact()
if err != nil {
return sendErrorEnvelope(r, err)
}

return r.SendEnvelope(organizations)
}
1 change: 1 addition & 0 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var migList = []migFunc{
{"v0.6.0", migrations.V0_6_0},
{"v0.7.0", migrations.V0_7_0},
{"v0.7.4", migrations.V0_7_4},
{"v0.8.0", migrations.V0_8_0},
}

// upgrade upgrades the database to the current version by running SQL migration files
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM=
github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rhnvrm/simples3 v0.9.1 h1:pYfEe2wTjx8B2zFzUdy4kZn3I3Otd9ZvzIhHkFR85kE=
github.com/rhnvrm/simples3 v0.9.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/rhnvrm/simples3 v0.9.2 h1:XrwsiMnwWf7t/kskvhMYXW6keqp5u3u6t5Va3ltzCQI=
github.com/rhnvrm/simples3 v0.9.2/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
Expand Down
74 changes: 74 additions & 0 deletions internal/migrations/v0.8.0.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package migrations

import (
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
)

// V0_8_0 updates the database schema to v0.8.0.
// This migration adds the organizations table and organization_id to users table.
func V0_8_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
// Create organizations table if it doesn't exist
_, err := db.Exec(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'organizations'
) THEN
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
"name" TEXT NOT NULL,
website TEXT NULL,
email_domain TEXT NULL,
phone TEXT NULL,
CONSTRAINT constraint_organizations_on_name CHECK (length("name") <= 255),
CONSTRAINT constraint_organizations_on_website CHECK (length(website) <= 255),
CONSTRAINT constraint_organizations_on_email_domain CHECK (length(email_domain) <= 255),
CONSTRAINT constraint_organizations_on_phone CHECK (length(phone) <= 50)
);

CREATE INDEX index_organizations_on_name ON organizations USING btree ("name");
CREATE INDEX index_organizations_on_email_domain ON organizations USING btree (email_domain);
END IF;
END $$;
`)
if err != nil {
return err
}

// Add organization_id column to users table if it doesn't exist
_, err = db.Exec(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'organization_id'
) THEN
ALTER TABLE users
ADD COLUMN organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL ON UPDATE CASCADE NULL;

CREATE INDEX index_users_on_organization_id ON users(organization_id);
END IF;
END $$;
`)
if err != nil {
return err
}

// Add organizations:manage permission to Admin role if not already present
_, err = db.Exec(`
UPDATE roles
SET permissions = array_append(permissions, 'organizations:manage')
WHERE name = 'Admin'
AND NOT ('organizations:manage' = ANY(permissions));
`)
if err != nil {
return err
}

return nil
}
30 changes: 30 additions & 0 deletions internal/organization/models/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package models

import (
"time"

"github.com/volatiletech/null/v9"
)

// Organization represents a company or organization that contacts belong to.
type Organization struct {
ID string `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Website null.String `db:"website" json:"website"`
EmailDomain null.String `db:"email_domain" json:"email_domain"`
Phone null.String `db:"phone" json:"phone"`

// Total is used for pagination count
Total int `db:"total" json:"-"`
}

// OrganizationCompact represents a minimal organization for list views.
type OrganizationCompact struct {
ID string `db:"id" json:"id"`
Name string `db:"name" json:"name"`

// Total is used for pagination count
Total int `db:"total" json:"-"`
}
Loading