mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +00:00
Compare commits
11 Commits
v0.1.1-alp
...
v0.2.1-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcbd16f042 | ||
|
|
e8f3f24422 | ||
|
|
425bb4ed04 | ||
|
|
0c3da82250 | ||
|
|
8649826a89 | ||
|
|
d427dfd20c | ||
|
|
afb54c371b | ||
|
|
46459599c7 | ||
|
|
63a6aedfd0 | ||
|
|
ffbf613e68 | ||
|
|
88f82fe80b |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
VERSION export-subst
|
||||
@@ -10,7 +10,7 @@ before:
|
||||
- make frontend-build
|
||||
|
||||
builds:
|
||||
- id: "standard"
|
||||
- id: "universal"
|
||||
main: ./cmd
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -24,29 +24,13 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
binary: 'libredesk{{ if eq .Os "windows" }}.exe{{ end }}'
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
hooks:
|
||||
post: make stuff BIN={{ .Path }}
|
||||
|
||||
- id: "arm"
|
||||
main: ./cmd
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- freebsd
|
||||
- linux
|
||||
- netbsd
|
||||
- openbsd
|
||||
goarch:
|
||||
- arm
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
binary: 'libredesk{{ if eq .Os "windows" }}.exe{{ end }}'
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
- -s -w -X "main.buildString={{ .Tag }} ({{ .ShortCommit }} {{ .Date }}, {{ .Os }}/{{ .Arch }})" -X "main.versionString={{ .Tag }}"
|
||||
hooks:
|
||||
post: make stuff BIN={{ .Path }}
|
||||
|
||||
@@ -70,7 +54,7 @@ dockers:
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
ids:
|
||||
- standard
|
||||
- universal
|
||||
image_templates:
|
||||
- "{{ .Env.DOCKER_ORG }}/{{ .ProjectName }}:latest-amd64"
|
||||
- "{{ .Env.DOCKER_ORG }}/{{ .ProjectName }}:{{ .Tag }}-amd64"
|
||||
@@ -94,7 +78,7 @@ dockers:
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
ids:
|
||||
- standard
|
||||
- universal
|
||||
image_templates:
|
||||
- "{{ .Env.DOCKER_ORG }}/{{ .ProjectName }}:latest-arm64"
|
||||
- "{{ .Env.DOCKER_ORG }}/{{ .ProjectName }}:{{ .Tag }}-arm64"
|
||||
@@ -119,7 +103,7 @@ dockers:
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
ids:
|
||||
- arm
|
||||
- universal
|
||||
image_templates:
|
||||
- "{{ .Env.DOCKER_ORG }}/{{ .ProjectName }}:latest-armv6"
|
||||
- "{{ .Env.DOCKER_ORG }}/{{ .ProjectName }}:{{ .Tag }}-armv6"
|
||||
@@ -144,7 +128,7 @@ dockers:
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
ids:
|
||||
- arm
|
||||
- universal
|
||||
image_templates:
|
||||
- "{{ .Env.DOCKER_ORG }}/{{ .ProjectName }}:latest-armv7"
|
||||
- "{{ .Env.DOCKER_ORG }}/{{ .ProjectName }}:{{ .Tag }}-armv7"
|
||||
@@ -195,4 +179,4 @@ release:
|
||||
owner: abhinavxd
|
||||
name: libredesk
|
||||
prerelease: auto
|
||||
draft: true
|
||||
draft: true
|
||||
|
||||
26
Makefile
26
Makefile
@@ -1,8 +1,10 @@
|
||||
# Build variables
|
||||
LAST_COMMIT := $(shell git rev-parse --short HEAD)
|
||||
LAST_COMMIT_DATE := $(shell git show -s --format=%ci ${LAST_COMMIT})
|
||||
VERSION := $(shell git describe --tags)
|
||||
BUILDSTR := ${VERSION} (Commit: ${LAST_COMMIT_DATE} (${LAST_COMMIT}), Build: $(shell date +"%Y-%m-%d %H:%M:%S %z"))
|
||||
# Try to get the commit hash from 1) git 2) the VERSION file 3) fallback.
|
||||
LAST_COMMIT := $(or $(shell git rev-parse --short HEAD 2> /dev/null),$(shell head -n 1 VERSION | grep -oP -m 1 "^[a-z0-9]+$$"), "")
|
||||
|
||||
# Try to get the semver from 1) git 2) the VERSION file 3) fallback.
|
||||
VERSION := $(or $(LIBREDESK_VERSION),$(shell git describe --tags --abbrev=0 2> /dev/null),$(shell grep -oP 'tag: \Kv\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?' VERSION),"v0.0.0")
|
||||
|
||||
BUILDSTR := ${VERSION} (\#${LAST_COMMIT} $(shell date -u +"%Y-%m-%dT%H:%M:%S%z"))
|
||||
|
||||
# Binary names and paths
|
||||
BIN := libredesk
|
||||
@@ -30,13 +32,13 @@ install-deps: $(STUFFBIN)
|
||||
.PHONY: frontend-build
|
||||
frontend-build: install-deps
|
||||
@echo "→ Building frontend for production..."
|
||||
@cd ${FRONTEND_DIR} && pnpm build
|
||||
@export VITE_APP_VERSION="${VERSION}" && cd ${FRONTEND_DIR} && pnpm build
|
||||
|
||||
# Run the Go backend server in development mode.
|
||||
.PHONY: run-backend
|
||||
run-backend:
|
||||
@echo "→ Running backend..."
|
||||
CGO_ENABLED=0 go run -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.frontendDir=frontend/dist'" cmd/*.go
|
||||
CGO_ENABLED=0 go run -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}' -X 'main.frontendDir=frontend/dist'" cmd/*.go
|
||||
|
||||
# Run the JS frontend server in development mode.
|
||||
.PHONY: run-frontend
|
||||
@@ -44,19 +46,19 @@ run-frontend:
|
||||
@echo "→ Installing frontend dependencies (if not already installed)..."
|
||||
@cd ${FRONTEND_DIR} && pnpm install
|
||||
@echo "→ Running frontend..."
|
||||
@export VUE_APP_VERSION="${VERSION}" && cd ${FRONTEND_DIR} && pnpm dev
|
||||
@export VITE_APP_VERSION="${VERSION}" && cd ${FRONTEND_DIR} && pnpm dev
|
||||
|
||||
# Build the backend binary.
|
||||
.PHONY: backend-build
|
||||
backend-build: $(STUFFBIN)
|
||||
.PHONY: build-backend
|
||||
build-backend: $(STUFFBIN)
|
||||
@echo "→ Building backend..."
|
||||
@CGO_ENABLED=0 go build -a\
|
||||
-ldflags="-X 'main.buildString=${BUILDSTR}' -s -w" \
|
||||
-ldflags="-X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}' -s -w" \
|
||||
-o ${BIN} cmd/*.go
|
||||
|
||||
# Main build target: builds both frontend and backend, then stuffs static assets into the binary.
|
||||
.PHONY: build
|
||||
build: frontend-build backend-build stuff
|
||||
build: frontend-build build-backend stuff
|
||||
@echo "→ Build successful. Current version: $(VERSION)"
|
||||
|
||||
# Stuff static assets into the binary using stuffbin.
|
||||
|
||||
@@ -47,8 +47,9 @@ And more checkout - [libredesk.io](https://libredesk.io)
|
||||
The latest image is available on DockerHub at [`libredesk/libredesk:latest`](https://hub.docker.com/r/libredesk/libredesk/tags?page=1&ordering=last_updated&name=latest)
|
||||
|
||||
```shell
|
||||
# Download the compose file to the current directory.
|
||||
curl -LO https://github.com/abhinavxd/libredesk/raw/master/docker-compose.yml
|
||||
# Download the compose file and sample config file in the current directory.
|
||||
curl -LO https://github.com/abhinavxd/libredesk/raw/main/docker-compose.yml
|
||||
curl -LO https://github.com/abhinavxd/libredesk/raw/main/config.sample.toml
|
||||
|
||||
# Copy the config.sample.toml to config.toml and edit it as needed.
|
||||
cp config.sample.toml config.toml
|
||||
|
||||
@@ -549,7 +549,7 @@ func initEmailInbox(inboxRecord imodels.Inbox, store inbox.MessageStore) (inbox.
|
||||
return nil, fmt.Errorf("initializing `%s` inbox: `%s` error : %w", inboxRecord.Channel, inboxRecord.Name, err)
|
||||
}
|
||||
|
||||
log.Printf("`%s` inbox successfully initialized. %d SMTP servers. %d IMAP clients.", inboxRecord.Name, len(config.SMTP), len(config.IMAP))
|
||||
log.Printf("`%s` inbox successfully initialized", inboxRecord.Name)
|
||||
|
||||
return inbox, nil
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/abhinavxd/libredesk/internal/colorlog"
|
||||
"github.com/abhinavxd/libredesk/internal/dbutil"
|
||||
"github.com/abhinavxd/libredesk/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.
|
||||
@@ -76,7 +76,7 @@ func setSystemUserPass(ctx context.Context, db *sqlx.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" {
|
||||
if dbutil.IsTableNotExistError(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
|
||||
24
cmd/main.go
24
cmd/main.go
@@ -6,8 +6,10 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/abhinavxd/libredesk/internal/ai"
|
||||
auth_ "github.com/abhinavxd/libredesk/internal/auth"
|
||||
@@ -50,7 +52,8 @@ var (
|
||||
frontendDir = "frontend/dist"
|
||||
|
||||
// Injected at build time.
|
||||
buildString = ""
|
||||
buildString string
|
||||
versionString string
|
||||
)
|
||||
|
||||
// App is the global app context which is passed and injected in the http handlers.
|
||||
@@ -82,6 +85,10 @@ type App struct {
|
||||
ai *ai.Manager
|
||||
search *search.Manager
|
||||
notifier *notifier.Service
|
||||
|
||||
// Global state that stores data on an available app update.
|
||||
update *AppUpdate
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -99,9 +106,8 @@ func main() {
|
||||
}
|
||||
|
||||
// Build string injected at build time.
|
||||
if buildString != "" {
|
||||
colorlog.Green("Build: %s", buildString)
|
||||
}
|
||||
colorlog.Green("Build: %s", buildString)
|
||||
colorlog.Green("Version: %s", versionString)
|
||||
|
||||
// Load the config files into Koanf.
|
||||
initConfig(ko)
|
||||
@@ -136,10 +142,13 @@ func main() {
|
||||
|
||||
// Upgrade.
|
||||
if ko.Bool("upgrade") {
|
||||
log.Println("no upgrades available")
|
||||
upgrade(db, fs, !ko.Bool("yes"))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Check for pending upgrade.
|
||||
checkPendingUpgrade(db)
|
||||
|
||||
// Load app settings from DB into the Koanf instance.
|
||||
settings := initSettings(db)
|
||||
loadSettings(settings)
|
||||
@@ -239,6 +248,11 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the app update checker.
|
||||
if ko.Bool("app.check_updates") {
|
||||
go checkUpdates(versionString, time.Hour*24, app)
|
||||
}
|
||||
|
||||
// Wait for shutdown signal.
|
||||
<-ctx.Done()
|
||||
colorlog.Red("Shutting down HTTP server...")
|
||||
|
||||
@@ -20,7 +20,15 @@ func handleGetGeneralSettings(r *fastglue.Request) error {
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
return r.SendEnvelope(out)
|
||||
// Unmarshal to add the app.update to the settings.
|
||||
var settings map[string]interface{}
|
||||
if err := json.Unmarshal(out, &settings); err != nil {
|
||||
app.lo.Error("error unmarshalling settings", "err", err)
|
||||
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, "Error fetching settings", nil))
|
||||
}
|
||||
// Add the app.update to the settings, adding `app` prefix to the key to match the settings structure in db.
|
||||
settings["app.update"] = app.update
|
||||
return r.SendEnvelope(settings)
|
||||
}
|
||||
|
||||
// handleUpdateGeneralSettings updates general settings.
|
||||
|
||||
98
cmd/updates.go
Normal file
98
cmd/updates.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright Kailash Nadh (https://github.com/knadh/listmonk)
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
// Adapted from listmonk for Libredesk.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const updateCheckURL = "https://updates.libredesk.io/updates.json"
|
||||
|
||||
type AppUpdate struct {
|
||||
Update struct {
|
||||
ReleaseVersion string `json:"release_version"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
URL string `json:"url"`
|
||||
Description string `json:"description"`
|
||||
|
||||
// This is computed and set locally based on the local version.
|
||||
IsNew bool `json:"is_new"`
|
||||
} `json:"update"`
|
||||
Messages []struct {
|
||||
Date string `json:"date"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
Priority string `json:"priority"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
|
||||
var reSemver = regexp.MustCompile(`-(.*)`)
|
||||
|
||||
// checkUpdates is a blocking function that checks for updates to the app
|
||||
// at the given intervals. On detecting a new update (new semver), it
|
||||
// sets the global update status that renders a prompt on the UI.
|
||||
func checkUpdates(curVersion string, interval time.Duration, app *App) {
|
||||
// Strip -* suffix.
|
||||
curVersion = reSemver.ReplaceAllString(curVersion, "")
|
||||
|
||||
fnCheck := func() {
|
||||
resp, err := http.Get(updateCheckURL)
|
||||
if err != nil {
|
||||
app.lo.Error("error checking for app updates", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
app.lo.Error("non-ok status code checking for app updates", "status", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
app.lo.Error("error reading response body", "err", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var out AppUpdate
|
||||
if err := json.Unmarshal(b, &out); err != nil {
|
||||
app.lo.Error("error unmarshalling response body", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// There is an update. Set it on the global app state.
|
||||
if semver.IsValid(out.Update.ReleaseVersion) {
|
||||
v := reSemver.ReplaceAllString(out.Update.ReleaseVersion, "")
|
||||
if semver.Compare(v, curVersion) > 0 {
|
||||
out.Update.IsNew = true
|
||||
app.lo.Info("new update available", "version", out.Update.ReleaseVersion)
|
||||
}
|
||||
}
|
||||
|
||||
app.Lock()
|
||||
app.update = &out
|
||||
app.Unlock()
|
||||
}
|
||||
|
||||
// Give a 15 minute buffer after app start in case the admin wants to disable
|
||||
// update checks entirely and not make a request to upstream.
|
||||
time.Sleep(time.Minute * 15)
|
||||
fnCheck()
|
||||
|
||||
// Thereafter, check every $interval.
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
fnCheck()
|
||||
}
|
||||
}
|
||||
145
cmd/upgrade.go
Normal file
145
cmd/upgrade.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright Kailash Nadh (https://github.com/knadh/listmonk)
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
// Adapted from listmonk for Libredesk.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/abhinavxd/libredesk/internal/dbutil"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/knadh/stuffbin"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// migFunc represents a migration function for a particular version.
|
||||
// fn (generally) executes database migrations and additionally
|
||||
// takes the filesystem and config objects in case there are additional bits
|
||||
// of logic to be performed before executing upgrades. fn is idempotent.
|
||||
type migFunc struct {
|
||||
version string
|
||||
fn func(*sqlx.DB, stuffbin.FileSystem, *koanf.Koanf) error
|
||||
}
|
||||
|
||||
// migList is the list of available migList ordered by the semver.
|
||||
// Each migration is a Go file in internal/migrations named after the semver.
|
||||
// The functions are named as: v0.7.0 => migrations.V0_7_0() and are idempotent.
|
||||
var migList = []migFunc{}
|
||||
|
||||
// upgrade upgrades the database to the current version by running SQL migration files
|
||||
// for all version from the last known version to the current one.
|
||||
func upgrade(db *sqlx.DB, fs stuffbin.FileSystem, prompt bool) {
|
||||
if prompt {
|
||||
var ok string
|
||||
fmt.Printf("** IMPORTANT: Take a backup of the database before upgrading.\n")
|
||||
fmt.Print("continue (y/n)? ")
|
||||
if _, err := fmt.Scanf("%s", &ok); err != nil {
|
||||
log.Fatalf("error reading value from terminal: %v", err)
|
||||
}
|
||||
if !strings.EqualFold(ok, "y") {
|
||||
fmt.Println("upgrade cancelled")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, toRun, err := getPendingMigrations(db)
|
||||
if err != nil {
|
||||
log.Fatalf("error checking migrations: %v", err)
|
||||
}
|
||||
|
||||
// No migrations to run.
|
||||
if len(toRun) == 0 {
|
||||
log.Printf("no upgrades to run. Database is up to date.")
|
||||
return
|
||||
}
|
||||
|
||||
// Execute migrations in succession.
|
||||
for _, m := range toRun {
|
||||
log.Printf("running migration %s", m.version)
|
||||
if err := m.fn(db, fs, ko); err != nil {
|
||||
log.Fatalf("error running migration %s: %v", m.version, err)
|
||||
}
|
||||
|
||||
// Record the migration version in the settings table. There was no
|
||||
// settings table until v0.7.0, so ignore the no-table errors.
|
||||
if err := recordMigrationVersion(m.version, db); err != nil {
|
||||
if dbutil.IsTableNotExistError(err) {
|
||||
continue
|
||||
}
|
||||
log.Fatalf("error recording migration version %s: %v", m.version, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("upgrade complete")
|
||||
}
|
||||
|
||||
// getPendingMigrations gets the pending migrations by comparing the last
|
||||
// recorded migration in the DB against all migrations listed in `migrations`.
|
||||
func getPendingMigrations(db *sqlx.DB) (string, []migFunc, error) {
|
||||
lastVer, err := getLastMigrationVersion(db)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Iterate through the migration versions and get everything above the last
|
||||
// upgraded semver.
|
||||
var toRun []migFunc
|
||||
for i, m := range migList {
|
||||
if semver.Compare(m.version, lastVer) > 0 {
|
||||
toRun = migList[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return lastVer, toRun, nil
|
||||
}
|
||||
|
||||
// getLastMigrationVersion returns the last migration semver recorded in the DB.
|
||||
// If there isn't any, `v0.0.0` is returned.
|
||||
func getLastMigrationVersion(db *sqlx.DB) (string, error) {
|
||||
var v string
|
||||
if err := db.Get(&v, `
|
||||
SELECT COALESCE(
|
||||
(SELECT value->>-1 FROM settings WHERE key='migrations'),
|
||||
'v0.0.0')`); err != nil {
|
||||
if dbutil.IsTableNotExistError(err) {
|
||||
return "v0.0.0", nil
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// recordMigrationVersion inserts the given version (of DB migration) into the
|
||||
// `migrations` array in the settings table.
|
||||
func recordMigrationVersion(ver string, db *sqlx.DB) error {
|
||||
_, err := db.Exec(fmt.Sprintf(`INSERT INTO settings (key, value)
|
||||
VALUES('migrations', '["%s"]'::JSONB)
|
||||
ON CONFLICT (key) DO UPDATE SET value = settings.value || EXCLUDED.value`, ver))
|
||||
return err
|
||||
}
|
||||
|
||||
// checkPendingUpgrade checks if the current database schema matches the expected binary version.
|
||||
func checkPendingUpgrade(db *sqlx.DB) {
|
||||
lastVer, toRun, err := getPendingMigrations(db)
|
||||
if err != nil {
|
||||
log.Fatalf("error checking migrations: %v", err)
|
||||
}
|
||||
|
||||
// No migrations to run.
|
||||
if len(toRun) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var vers []string
|
||||
for _, m := range toRun {
|
||||
vers = append(vers, m.version)
|
||||
}
|
||||
|
||||
log.Fatalf(`there are %d pending database upgrade(s): %v. The last upgrade was %s. Backup the database and run libredesk --upgrade`,
|
||||
len(toRun), vers, lastVer)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
[app]
|
||||
log_level = "debug"
|
||||
env = "dev"
|
||||
check_updates = true
|
||||
|
||||
# HTTP server.
|
||||
[app.server]
|
||||
@@ -45,7 +46,7 @@ max_lifetime = "300s"
|
||||
|
||||
# Redis.
|
||||
[redis]
|
||||
# If using docker compose, use the service name as the host. e.g. redis
|
||||
# If using docker compose, use the service name as the host. e.g. redis:6379
|
||||
address = "127.0.0.1:6379"
|
||||
password = ""
|
||||
db = 0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
# Libredesk app
|
||||
app:
|
||||
image: libredesk:latest
|
||||
image: libredesk/libredesk:latest
|
||||
container_name: libredesk_app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
@@ -20,8 +20,9 @@ The latest image is available on DockerHub at `libredesk/llibredeskistmonk:lates
|
||||
The recommended method is to download the [docker-compose.yml](https://github.com/abhinavxd/libredesk/blob/master/docker-compose.yml) file, customize it for your environment and then to simply run `docker compose up -d`.
|
||||
|
||||
```shell
|
||||
# Download the compose file to the current directory.
|
||||
curl -LO https://github.com/abhinavxd/libredesk/raw/master/docker-compose.yml
|
||||
# Download the compose file and the sample config file in the current directory.
|
||||
curl -LO https://github.com/abhinavxd/libredesk/raw/main/docker-compose.yml
|
||||
curl -LO https://github.com/abhinavxd/libredesk/raw/main/config.sample.toml
|
||||
|
||||
# Copy the config.sample.toml to config.toml and edit it as needed.
|
||||
cp config.sample.toml config.toml
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
@delete-view="deleteView"
|
||||
>
|
||||
<div class="flex flex-col h-screen">
|
||||
<AppUpdate />
|
||||
<PageHeader />
|
||||
<RouterView class="flex-grow" />
|
||||
</div>
|
||||
@@ -77,6 +78,7 @@ import { useMacroStore } from '@/stores/macro'
|
||||
import { useTagStore } from '@/stores/tag'
|
||||
import PageHeader from './components/layout/PageHeader.vue'
|
||||
import ViewForm from '@/features/view/ViewForm.vue'
|
||||
import AppUpdate from '@/components/update/AppUpdate.vue'
|
||||
import api from '@/api'
|
||||
import { toast as sooner } from 'vue-sonner'
|
||||
import Sidebar from '@/components/sidebar/Sidebar.vue'
|
||||
|
||||
25
frontend/src/components/update/AppUpdate.vue
Normal file
25
frontend/src/components/update/AppUpdate.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="appSettingsStore.settings['app.update']?.update?.is_new"
|
||||
class="p-2 mb-2 border-b bg-secondary text-secondary-foreground"
|
||||
>
|
||||
A new update is available:
|
||||
{{ appSettingsStore.settings['app.update'].update.release_version }} ({{
|
||||
appSettingsStore.settings['app.update'].update.release_date
|
||||
}})
|
||||
<a
|
||||
:href="appSettingsStore.settings['app.update'].update.url"
|
||||
target="_blank"
|
||||
nofollow
|
||||
noreferrer
|
||||
class="underline ml-2"
|
||||
>
|
||||
View details
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
collapsible
|
||||
:default-value="['Actions', 'Information', 'Previous conversations']"
|
||||
>
|
||||
<AccordionItem value="Actions" class="border-0 mb-2 mb-2">
|
||||
<AccordionItem value="Actions" class="border-0 mb-2">
|
||||
<AccordionTrigger class="bg-muted px-4 py-3 text-sm font-medium rounded-lg mx-2">
|
||||
Actions
|
||||
</AccordionTrigger>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { useAppSettingsStore } from './stores/appSettings'
|
||||
import router from './router'
|
||||
import mitt from 'mitt'
|
||||
import api from './api'
|
||||
@@ -38,12 +39,16 @@ async function initApp () {
|
||||
const i18n = createI18n(i18nConfig)
|
||||
const app = createApp(Root)
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
|
||||
// Store app settings in Pinia
|
||||
const settingsStore = useAppSettingsStore()
|
||||
settingsStore.setSettings(settings)
|
||||
|
||||
// Add emitter to global properties.
|
||||
app.config.globalProperties.emitter = emitter
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.use(i18n)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
12
frontend/src/stores/appSettings.js
Normal file
12
frontend/src/stores/appSettings.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppSettingsStore = defineStore('settings', {
|
||||
state: () => ({
|
||||
settings: {}
|
||||
}),
|
||||
actions: {
|
||||
setSettings (newSettings) {
|
||||
this.settings = newSettings
|
||||
}
|
||||
}
|
||||
})
|
||||
1
go.mod
1
go.mod
@@ -35,6 +35,7 @@ require (
|
||||
github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0
|
||||
github.com/zerodha/simplesessions/v3 v3.0.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/mod v0.17.0
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
)
|
||||
|
||||
|
||||
2
go.sum
2
go.sum
@@ -187,6 +187,8 @@ golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
||||
@@ -23,3 +23,14 @@ func IsUniqueViolationError(err error) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTableNotExistError checks if the given error is a PostgreSQL table does not exist error (error code 42P01)
|
||||
func IsTableNotExistError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if pqErr, ok := err.(*pq.Error); ok {
|
||||
return pqErr.Code == "42P01"
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -335,7 +335,7 @@ func ChangeSystemUserPassword(ctx context.Context, db *sqlx.DB) error {
|
||||
if err := updateSystemUserPassword(db, hashedPassword); err != nil {
|
||||
return fmt.Errorf("error updating system user password: %v", err)
|
||||
}
|
||||
fmt.Println("password updated successfully.")
|
||||
fmt.Println("password updated successfully. Login with email 'System' and the new password.")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user