mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-03 13:33:32 +00:00
feat: implement app update checker and UI notification
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
11
cmd/main.go
11
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"
|
||||
@@ -83,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() {
|
||||
@@ -242,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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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