Compare commits

...

11 Commits

Author SHA1 Message Date
Abhinav Raut
fcbd16f042 feat: implement app update checker and UI notification 2025-02-25 02:49:09 +05:30
Abhinav Raut
e8f3f24422 feat: update build configuration and versioning system 2025-02-25 01:35:52 +05:30
Abhinav Raut
425bb4ed04 feat: add database upgrade functionality (adapted from listmonk) 2025-02-25 01:35:27 +05:30
Abhinav Raut
0c3da82250 fix: remove redundant margin class from Actions AccordionItem 2025-02-25 00:34:28 +05:30
Abhinav Raut
8649826a89 Update .gitattributes 2025-02-25 00:19:06 +05:30
Abhinav Raut
d427dfd20c Update .gitattributes 2025-02-25 00:18:42 +05:30
Abhinav Raut
afb54c371b Update .gitattributes 2025-02-25 00:16:28 +05:30
Abhinav Raut
46459599c7 Update .gitattributes 2025-02-25 00:10:14 +05:30
Abhinav Raut
63a6aedfd0 chore: update .gitattributes to specify Go language for all files 2025-02-25 00:08:04 +05:30
Abhinav Raut
ffbf613e68 chore: add .gitattributes to mark frontend files as vendored 2025-02-25 00:06:28 +05:30
Abhinav Raut
88f82fe80b fix: typos in docker image and curl request. 2025-02-24 22:28:01 +05:30
23 changed files with 368 additions and 53 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
VERSION export-subst

View File

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

View File

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

View File

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

2
VERSION Normal file
View File

@@ -0,0 +1,2 @@
$Format:%h$
$Format:%D$

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
services:
# Libredesk app
app:
image: libredesk:latest
image: libredesk/libredesk:latest
container_name: libredesk_app
restart: unless-stopped
ports:

View File

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

View File

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

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

View File

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

View File

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

View 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
View File

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

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

View File

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

View File

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