From 5f8f8b3dd3f1652c3665666b403b948fc58ee923 Mon Sep 17 00:00:00 2001 From: Marek Goc Date: Thu, 29 May 2025 11:48:39 +0200 Subject: [PATCH] add pocketbase --- .gitignore | 6 + .../custom/cloudflare/TurnStilleCaptcha.js | 82 ++++++++ backend/custom/cloudflare/cloudflare.go | 95 +++++++++ backend/custom/cloudflare/js.go | 8 + backend/custom/custom.go | 61 ++++++ backend/custom/gtm/gtm.go | 56 +++++ backend/custom/gtm/gtm.js | 29 +++ backend/custom/gtm/js.go | 8 + backend/custom/mail/mail.go | 175 ++++++++++++++++ backend/custom/manifest/manifest.go | 50 +++++ backend/custom/proxy/proxy.go | 121 +++++++++++ backend/custom/seo/feeds.go | 194 ++++++++++++++++++ backend/custom/seo/robots.go | 50 +++++ backend/custom/supervise/supervise.go | 88 ++++++++ backend/custom/version/version.go | 26 +++ backend/custom/webpConverter/memFileReader.go | 14 ++ .../custom/webpConverter/readSeekCloser.go | 9 + backend/custom/webpConverter/webp_convert.go | 144 +++++++++++++ backend/go.mod | 42 ++++ backend/go.sum | 127 ++++++++++++ backend/main.go | 27 +++ composables/usePB.ts | 15 ++ package.json | 1 + pnpm-lock.yaml | 10 +- taskfile.yml | 94 ++++----- 25 files changed, 1484 insertions(+), 48 deletions(-) create mode 100644 backend/custom/cloudflare/TurnStilleCaptcha.js create mode 100644 backend/custom/cloudflare/cloudflare.go create mode 100644 backend/custom/cloudflare/js.go create mode 100644 backend/custom/custom.go create mode 100644 backend/custom/gtm/gtm.go create mode 100644 backend/custom/gtm/gtm.js create mode 100644 backend/custom/gtm/js.go create mode 100644 backend/custom/mail/mail.go create mode 100644 backend/custom/manifest/manifest.go create mode 100644 backend/custom/proxy/proxy.go create mode 100644 backend/custom/seo/feeds.go create mode 100644 backend/custom/seo/robots.go create mode 100644 backend/custom/supervise/supervise.go create mode 100644 backend/custom/version/version.go create mode 100644 backend/custom/webpConverter/memFileReader.go create mode 100644 backend/custom/webpConverter/readSeekCloser.go create mode 100644 backend/custom/webpConverter/webp_convert.go create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/main.go create mode 100644 composables/usePB.ts diff --git a/.gitignore b/.gitignore index 4a7f73a..5594d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ logs .env .env.* !.env.example + + +# Pocketbase storage +backend/pb_data +backend/tmp +.pocketbase diff --git a/backend/custom/cloudflare/TurnStilleCaptcha.js b/backend/custom/cloudflare/TurnStilleCaptcha.js new file mode 100644 index 0000000..bb68c4b --- /dev/null +++ b/backend/custom/cloudflare/TurnStilleCaptcha.js @@ -0,0 +1,82 @@ +export class TurnStilleCaptcha { + sitekey = "{{.SiteKey}}"; + /** + * Initializes the TurnStilleCaptcha instance. + * Creates a container for the captcha and appends it to the target element. + * If the Cloudflare Turnstile script is already loaded, it runs the captcha. + * Otherwise, it loads the script and initializes the captcha with the given properties. + * @param {HTMLElement} target - The element to attach the captcha container to. + * @param {Object} [props={}] - Optional properties for captcha initialization, such as theme. + */ + + constructor(target, props = {}) { + // create holder + this.holder = document.createElement("div"); + this.holder.id = "turnstile-container"; + this.theme = props.theme || "auto"; + target.appendChild(this.holder); + + + // execute code + if (window.turnstile) { + this.runCaptcha(); + } else { + this.loadCloudflareScript(); + } + } + runCaptcha() { + setTimeout(() => { + if (globalThis.turnstileInstance) { + window.turnstile.remove(globalThis.turnstileInstance); + } + globalThis.turnstileInstance = window.turnstile.render(this.holder, { + sitekey: this.sitekey, + theme: this.theme, + callback: (token) => { + if (token) { + const event = new CustomEvent("token", { + detail: token, + bubbles: true, + }); + this.holder.dispatchEvent(event); + } + }, + error: (error) => { + const event = new CustomEvent("failure", { + detail: error, + bubbles: true, + }); + this.holder.dispatchEvent(event); + window.turnstile.reset(globalThis.turnstileInstance); + }, + }); + }, 1000); + } + + loadCloudflareScript() { + const script = document.createElement("script"); + script.id = "turnstile-script"; + script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js"; + // script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"; + script.async = true; + script.defer = true; + + script.onload = () => { + const event = new CustomEvent("loaded", { + detail: "Turnstile script loaded", + bubbles: true, + }); + this.holder.dispatchEvent(event); + + this.runCaptcha(); + }; + script.onerror = () => { + const event = new CustomEvent("failure", { + detail: "Failed to load Turnstile script", + bubbles: true, + }); + this.holder.dispatchEvent(event); + }; + document.head.appendChild(script); + } +} diff --git a/backend/custom/cloudflare/cloudflare.go b/backend/custom/cloudflare/cloudflare.go new file mode 100644 index 0000000..72cabf5 --- /dev/null +++ b/backend/custom/cloudflare/cloudflare.go @@ -0,0 +1,95 @@ +package cloudflare + +import ( + "bytes" + "encoding/json" + "errors" + "html/template" + "io" + "net/http" + "strings" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" +) + +var VerifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify" + +type TurnstileResponse struct { + Success bool `json:"success"` + ErrorCodes []string `json:"error-codes,omitempty"` + ChallengeTS string `json:"challenge_ts,omitempty"` + Hostname string `json:"hostname,omitempty"` +} + +type TurnStilleCaptcha struct { + SiteKey string + SecretKey string +} + +func ServeTurnstilleCaptchaJS(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + return se.Router.GET("/api/email/script.js", func(e *core.RequestEvent) error { + // www.abrasive.ma-al.pl + // siteKey: "0x4AAAAAABdgeAdu4Pxxovj3" + // secretKey: "0x4AAAAAABdgeHJDjMwmeX5aXaXGh6HWZbw" + + settings, err := GetSettings(app) + if err != nil { + return err + } + + file, err := JS.ReadFile("TurnStilleCaptcha.js") + if err != nil { + return err + } + templ, err := template.New("test").Parse(string(file)) + + buf := bytes.Buffer{} + templ.Execute(&buf, map[string]interface{}{ + "SiteKey": settings.SiteKey, + }) + if err != nil { + return err + } + + e.Response.Header().Set("Content-Type", "application/javascript") + return e.String(http.StatusOK, buf.String()) + }) +} + +func GetSettings(app *pocketbase.PocketBase) (*TurnStilleCaptcha, error) { + record, err := app.FindFirstRecordByFilter("settings", "key='turnstile'", nil) + + settings := TurnStilleCaptcha{} + json.Unmarshal([]byte(record.GetString("value")), &settings) + + if err != nil { + return nil, err + } + + return &settings, nil +} + +func VerifyTurnstile(app *pocketbase.PocketBase, token, ip string) error { + conf, err := GetSettings(app) + if err != nil { + return err + } + + data := map[string]string{"secret": conf.SecretKey, "response": token, "remoteip": ip} + jsonData, _ := json.Marshal(data) + resp, err := http.Post(VerifyUrl, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + defer resp.Body.Close() + var turnstileResp TurnstileResponse + body, _ := io.ReadAll(resp.Body) + json.Unmarshal(body, &turnstileResp) + + if !turnstileResp.Success { + return errors.New(turnstileResp.ChallengeTS + ": " + strings.Join(turnstileResp.ErrorCodes, " ")) + } + return nil +} diff --git a/backend/custom/cloudflare/js.go b/backend/custom/cloudflare/js.go new file mode 100644 index 0000000..6a1e806 --- /dev/null +++ b/backend/custom/cloudflare/js.go @@ -0,0 +1,8 @@ +package cloudflare + +import ( + "embed" +) + +//go:embed TurnStilleCaptcha.js +var JS embed.FS diff --git a/backend/custom/custom.go b/backend/custom/custom.go new file mode 100644 index 0000000..e8c98c4 --- /dev/null +++ b/backend/custom/custom.go @@ -0,0 +1,61 @@ +package custom + +import ( + "pocketbase/custom/cloudflare" + "pocketbase/custom/gtm" + "pocketbase/custom/mail" + "pocketbase/custom/manifest" + "pocketbase/custom/proxy" + "pocketbase/custom/seo" + "pocketbase/custom/supervise" + "pocketbase/custom/version" + webpconverter "pocketbase/custom/webpConverter" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" +) + +func LoadCustomCode(app *pocketbase.PocketBase, se *core.ServeEvent) error { + + // include supervised subprocess if command provided + supervise.ServeSubprocessSupervisor(app, se) + + // include serving js code from cloudflare + cloudflare.ServeTurnstilleCaptchaJS(app, se) + + // include sending emails service + mail.ServeMailSender(app, se) + + // include robots.txt endpoint + seo.ServeRobotsTxt(app, se) + + // include feeds endpoint + seo.ServeFeeds(app, se) + seo.ServeFeedsIndex(app, se) + + // include proxy to serve nuxt app + proxy.ServeProxyPassingToNuxt(app, se) + + // create endpoint to serve version information + version.ServeVersionInfo(app, se) + + // include endpoint to server GTM script + gtm.ServeTagMangerJS(app, se) + + // include endpoint serving manifest + manifest.ServeManifst(app, se) + + return nil +} + +func ExtendApp(app *pocketbase.PocketBase) *pocketbase.PocketBase { + app.RootCmd.PersistentFlags().String("proxy", "", "inner proxy target") + app.RootCmd.PersistentFlags().String("subcommand", "", "provide command with params like cli that will be executed and supervised by main process") + + // include webp converter + app.OnRecordCreate().Bind(webpconverter.CreateEventHandler(app)) + app.OnRecordUpdate().Bind(webpconverter.CreateEventHandler(app)) + app.OnFileDownloadRequest().Bind(webpconverter.ThumbEventHandler(app)) + + return app +} diff --git a/backend/custom/gtm/gtm.go b/backend/custom/gtm/gtm.go new file mode 100644 index 0000000..4dfa642 --- /dev/null +++ b/backend/custom/gtm/gtm.go @@ -0,0 +1,56 @@ +package gtm + +import ( + "bytes" + "encoding/json" + "net/http" + "text/template" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" +) + +func ServeTagMangerJS(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + return se.Router.GET("/api/gtm/script.js", func(e *core.RequestEvent) error { + + settings, err := GetSettings(app) + if err != nil { + return err + } + + file, err := JS.ReadFile("gtm.js") + if err != nil { + return err + } + templ, err := template.New("test").Parse(string(file)) + + buf := bytes.Buffer{} + templ.Execute(&buf, map[string]interface{}{ + "GtagID": settings.GtagID, + }) + if err != nil { + return err + } + + e.Response.Header().Set("Content-Type", "application/javascript") + return e.String(http.StatusOK, buf.String()) + }) +} + +type GTagSettings struct { + GtagID string `json:"gtagID"` +} + +func GetSettings(app *pocketbase.PocketBase) (*GTagSettings, error) { + record, err := app.FindFirstRecordByFilter("settings", "key='gtagID'", nil) + + settings := GTagSettings{} + json.Unmarshal([]byte(record.GetString("value")), &settings) + + if err != nil { + return nil, err + } + + return &settings, nil +} diff --git a/backend/custom/gtm/gtm.js b/backend/custom/gtm/gtm.js new file mode 100644 index 0000000..561f9ae --- /dev/null +++ b/backend/custom/gtm/gtm.js @@ -0,0 +1,29 @@ +export class GTM { + gtagID = "{{.GtagID}}"; + + constructor() { + this.insertScript(window, document, 'script', 'dataLayer', this.gtagID); + this.insertNoScript(); + } + + + insertScript(w, d, s, l, i) { + w[l] = w[l] || []; w[l].push({ + 'gtm.start': + new Date().getTime(), event: 'gtm.js' + }); var f = d.getElementsByTagName(s)[0], + j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = + 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f); + } + + insertNoScript() { + const noscript = document.createElement("noscript"); + const iframe = document.createElement("iframe"); + iframe.src = "https://www.googletagmanager.com/ns.html?id=" + this.gtagID; + iframe.height = 0; + iframe.width = 0; + iframe.style = "display:none;visibility:hidden"; + noscript.appendChild(iframe); + document.body.appendChild(noscript); + } +} diff --git a/backend/custom/gtm/js.go b/backend/custom/gtm/js.go new file mode 100644 index 0000000..fcf53fa --- /dev/null +++ b/backend/custom/gtm/js.go @@ -0,0 +1,8 @@ +package gtm + +import ( + "embed" +) + +//go:embed gtm.js +var JS embed.FS diff --git a/backend/custom/mail/mail.go b/backend/custom/mail/mail.go new file mode 100644 index 0000000..9592e92 --- /dev/null +++ b/backend/custom/mail/mail.go @@ -0,0 +1,175 @@ +package mail + +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "net/http" + "net/mail" + "pocketbase/custom/cloudflare" + "strings" + "text/template" + "time" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/mailer" + "github.com/pocketbase/pocketbase/tools/router" +) + +type EmailData struct { + Name string `json:"name"` + Email string `json:"email"` + Token string `json:"token"` + Message string `json:"message"` + Phone string `json:"phone"` + LangIso string `json:"lang_iso"` +} + +type MailsSettings struct { + ReceiverMail string `json:"receiver_mail"` + ReceiverName string `json:"receiver_name"` + Subject string `json:"subject"` +} + +func ServeMailSender(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + return se.Router.POST("/api/email/send", func(e *core.RequestEvent) error { + // name := e.Request.PathValue("name") + + data := EmailData{} + if err := e.BindBody(&data); err != nil { + e.Response.Header().Set("Content-Type", "application/json") + app.Logger().Error(err.Error(), "type", "mail") + return e.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + if pass, err := validateMX(data.Email); !pass && err != nil { + e.Response.Header().Set("Content-Type", "application/json") + app.Logger().Error("Invalid email address.", "type", "mail", "error", err.Error()) + return e.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid email address."}) + } + + // fmt.Printf("e.Request.Body: %v IP: %s\n", data, e.RealIP()) + + err := cloudflare.VerifyTurnstile(app, data.Token, e.RealIP()) + if err != nil { + e.Response.Header().Set("Content-Type", "application/json") + app.Logger().Error("Captcha verification failed.", "type", "mail", "error", err.Error()) + return e.JSON(http.StatusBadRequest, map[string]string{"error": "Captcha verification failed."}) + } + + eamil_body, err := app.FindFirstRecordByFilter("email_template", fmt.Sprintf("name='contact_form'&&id_lang='%s'", data.LangIso), nil) + if err != nil { + e.Response.Header().Set("Content-Type", "application/json") + app.Logger().Error("Template not available", "type", "mail", "error", err.Error()) + return e.JSON(http.StatusBadRequest, map[string]string{"error": "Template not available"}) + } + templ, err := template.New("test").Parse(eamil_body.GetString("template")) + if err != nil { + app.Logger().Error("Template parsing error.", "type", "mail", "error", err.Error()) + e.Response.Header().Set("Content-Type", "application/json") + return e.JSON(http.StatusBadRequest, map[string]string{"error": "Template parsing error."}) + } + + buf := bytes.Buffer{} + templ.Execute(&buf, map[string]interface{}{ + "Data": time.Now().Local().Format("2006-01-02 15:04:05"), + "Name": data.Name, + "Email": data.Email, + "Message": strings.ReplaceAll(data.Message, "\n", "
"), + "Phone": data.Phone, + }) + + mailsSettings, err := getSettings(app) + if err != nil { + app.Logger().Error("Mails settings corrupted", "type", "mail", "error", err.Error()) + e.Response.Header().Set("Content-Type", "application/json") + return e.JSON(http.StatusBadRequest, map[string]string{"error": "Mails settings corrupted"}) + } + + cc := []mail.Address{{Address: data.Email, Name: data.Name}} + to := []mail.Address{{Address: mailsSettings.ReceiverMail, Name: mailsSettings.ReceiverName}} + + message := &mailer.Message{ + From: mail.Address{ + Address: e.App.Settings().Meta.SenderAddress, + Name: e.App.Settings().Meta.SenderName, + }, + Cc: cc, + To: to, + Subject: mailsSettings.Subject, + HTML: buf.String(), + } + + err = e.App.NewMailClient().Send(message) + if err != nil { + app.Logger().Error("Mails sending error", "type", "mail", "error", err.Error()) + e.Response.Header().Set("Content-Type", "application/json") + return e.JSON(http.StatusBadRequest, map[string]string{"error": "Mails sending error"}) + } + + receivers := formReceiversList(to, cc, []mail.Address{}) + app.Logger().Info("Mail Sent", "type", "mail", "receivers", receivers) + return e.JSON(http.StatusOK, map[string]string{"status": "success", "message": "Email sent successfully. to " + receivers}) + }) +} + +func formReceiversList(to []mail.Address, cc []mail.Address, bcc []mail.Address) string { + + res := "" + + for _, a := range to { + res = fmt.Sprintf("%s %s<%s>", res, a.Name, a.Address) + } + + for _, a := range cc { + res = fmt.Sprintf("%s %s<%s>", res, a.Name, a.Address) + } + + for _, a := range bcc { + res = fmt.Sprintf("%s %s<%s>", res, a.Name, a.Address) + } + return res +} +func getSettings(app *pocketbase.PocketBase) (*MailsSettings, error) { + record, err := app.FindFirstRecordByFilter("settings", "key='contact_page'", nil) + + settings := MailsSettings{} + json.Unmarshal([]byte(record.GetString("value")), &settings) + + if err != nil { + return nil, err + } + + return &settings, nil +} + +func extractDomain(email string) string { + parts := strings.Split(email, "@") + if len(parts) != 2 { + return "" + } + return strings.TrimSpace(parts[1]) +} + +// validateMX checks if the domain has valid MX records or A records as a fallback +func validateMX(email string) (bool, error) { + // Check MX records + + domain := extractDomain(email) + mxRecords, err := net.LookupMX(domain) + if err != nil { + return false, fmt.Errorf("'MX' records for %s not found", domain) + } else if len(mxRecords) > 0 { + // At least one MX record exists + return true, nil + } + + // Fallback: Check for A records (some domains accept mail via A records) + aRecords, err := net.LookupHost(domain) + if err != nil { + return false, fmt.Errorf("'A' record for %s not found", domain) + } + return len(aRecords) > 0, nil +} diff --git a/backend/custom/manifest/manifest.go b/backend/custom/manifest/manifest.go new file mode 100644 index 0000000..60bc24c --- /dev/null +++ b/backend/custom/manifest/manifest.go @@ -0,0 +1,50 @@ +package manifest + +import ( + "net/http" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" +) + +type Manifest struct { + Name string `json:"name"` + ShortName string `json:"short_name"` + Description string `json:"description"` + Icons []Icon `json:"icons"` + StartURL string `json:"start_url"` + Display string `json:"display"` + BackgroundColor string `json:"background_color"` + ThemeColor string `json:"theme_color"` + Lang string `json:"lang"` + Author string `json:"author"` + OgHost string `json:"ogHost"` + Orientation string `json:"orientation"` +} + +type Icon struct { + Src string `json:"src"` + Sizes string `json:"sizes"` + Type string `json:"type"` +} + +func ServeManifst(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + return se.Router.GET("/api/manifest.json", func(e *core.RequestEvent) error { + manifest, err := GetSettings(app) + if err != nil { + return err + } + e.Response.Header().Add("content-type", "application/json") + return e.String(http.StatusOK, manifest) + }) +} + +func GetSettings(app *pocketbase.PocketBase) (string, error) { + record, err := app.FindFirstRecordByFilter("settings", "key='manifest'", nil) + if err != nil { + return "", err + } + + return record.GetString("value"), nil +} diff --git a/backend/custom/proxy/proxy.go b/backend/custom/proxy/proxy.go new file mode 100644 index 0000000..e47a16d --- /dev/null +++ b/backend/custom/proxy/proxy.go @@ -0,0 +1,121 @@ +package proxy + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" +) + +var Logger *slog.Logger + +func ServeProxyPassingToNuxt(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + Logger = app.Logger() + + proxyUrl, _ := app.RootCmd.Flags().GetString("proxy") + + if len(proxyUrl) > 0 { + target, _ := url.Parse(proxyUrl) // Node.js app + + proxy := httputil.NewSingleHostReverseProxy(target) + + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) + req.Host = target.Host + } + proxy.Transport = &loggingTransport{http.DefaultTransport} + + proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) { + if errors.Is(err, context.Canceled) { + return + } + app.Logger().Error(fmt.Sprintf("Proxy error: %v for %s %s", err, req.Method, req.URL.Path), "type", "proxy") + http.Error(rw, "Proxy error", http.StatusBadGateway) + } + + return se.Router.Any("/", func(e *core.RequestEvent) error { + // Ping the backend with a HEAD request (or TCP dial) + backendUp := isBackendAlive(target) + if !backendUp { + app.Logger().Error(fmt.Sprintf("Backend %s is unreachable, sending 502", target), "type", "proxy", "path", e.Request.URL.Path, "remoteIP", e.Request.RemoteAddr) + e.Response.WriteHeader(http.StatusBadGateway) + e.Response.Write([]byte("502 Backend is unavailable")) + return nil + } + + // Forward to Node.js + proxy.ServeHTTP(e.Response, e.Request) + return nil + }) + } else { + return nil + } +} + +func isBackendAlive(target *url.URL) bool { + conn, err := net.DialTimeout("tcp", target.Host, 500*time.Millisecond) + if err != nil { + return false + } + conn.Close() + return true +} + +type loggingTransport struct { + rt http.RoundTripper +} + +func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + start := time.Now() + + // Do the actual request + resp, err := t.rt.RoundTrip(req) + duration := time.Since(start) + + // Prepare fields + remoteAddr := req.RemoteAddr + if ip := req.Header.Get("X-Real-IP"); len(ip) > 0 { + remoteAddr = ip + } + method := req.Method + uri := req.URL.RequestURI() + // proto := req.Proto + status := 0 + size := 0 + + if resp != nil { + status = resp.StatusCode + if resp.ContentLength > 0 { + size = int(resp.ContentLength) + } + } + + referer := req.Referer() + ua := req.UserAgent() + // timestamp := time.Now().Format("02/Jan/2006:15:04:05 -0700") + + Logger.Info( + fmt.Sprintf("%s %s", method, uri), + "type", "proxy", + // "method", method, + // "url", uri, + "referer", referer, + "remoteIP", remoteAddr, + "userAgent", ua, + "execTime", fmt.Sprintf("%.4fms", float64(duration.Milliseconds())), + "status", status, + "size", size, + ) + + return resp, err +} diff --git a/backend/custom/seo/feeds.go b/backend/custom/seo/feeds.go new file mode 100644 index 0000000..cce7998 --- /dev/null +++ b/backend/custom/seo/feeds.go @@ -0,0 +1,194 @@ +package seo + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "net/http" + "path" + "time" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" +) + +type MenuRecord struct { + Active bool `json:"active"` + CollectionID string `json:"collectionId"` + CollectionName string `json:"collectionName"` + Created string `json:"created"` + ID string `json:"id"` + IDLang string `json:"id_lang"` + IDPage string `json:"id_page"` + IDParent string `json:"id_parent"` + IsDefault bool `json:"is_default"` + IsRoot bool `json:"is_root"` + LinkRewrite string `json:"link_rewrite"` + LinkTitle string `json:"link_title"` + MetaDescription string `json:"meta_description"` + MetaTitle string `json:"meta_title"` + Name string `json:"name"` + PageName string `json:"page_name"` + PositionID int `json:"position_id"` + Updated string `json:"updated"` + Url string `json:"url"` +} + +type UrlSet struct { + XMLName xml.Name `xml:"urlset"` + Xmlns string `xml:"xmlns,attr"` + Urls []Url `xml:"url"` +} + +type Url struct { + Loc string `xml:"loc"` + LastMod string `xml:"lastmod,omitempty"` + ChangeFreq string `xml:"changefreq,omitempty"` + Priority string `xml:"priority,omitempty"` +} + +func ServeFeeds(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + return se.Router.GET("/feeds/{lang}/sitemap.xml", func(e *core.RequestEvent) error { + + lang := e.Request.PathValue("lang") + + urls, err := getLocations(app, lang) + if err != nil { + return err + } + + xx := UrlSet{ + Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9", + Urls: urls, + } + + bytes, err := xml.MarshalIndent(xx, "", " ") + if err != nil { + return err + } + + e.Response.Header().Add("content-type", "text/xml") + return e.String(http.StatusOK, xml.Header+string(bytes)) + }) +} + +func getLocations(app *pocketbase.PocketBase, lang string) ([]Url, error) { + records, err := app.FindRecordsByFilter("menu_view", fmt.Sprintf("id_lang='%s'&&active=true", lang), "position_id", 200, 0) + if err != nil { + return nil, err + } + + baseUrl, err := getBaseUrl(app) + if err != nil { + return nil, err + } + + locations := []Url{} + lastMod := time.Now().Add(time.Hour * 24 * 7 * -1).Format("2006-01-02") + + for _, r := range records { + rec := MenuRecord{} + x, _ := r.MarshalJSON() + json.Unmarshal(x, &rec) + if rec.IsRoot { + continue + } + if len(rec.Url) > 0 { + continue + } + if rec.IsDefault { + + locations = append(locations, Url{ + Loc: baseUrl + path.Join(lang), + LastMod: lastMod, + ChangeFreq: "weekly", + Priority: "1.0", + }) + } + locations = append(locations, Url{ + Loc: baseUrl + path.Join(lang, rec.IDPage, rec.LinkRewrite), + LastMod: lastMod, + ChangeFreq: "weekly", + Priority: "1.0", + }) + } + + return locations, nil +} + +type BaseUrl struct { + BaseURL string `json:"baseUrl"` +} + +func getBaseUrl(app *pocketbase.PocketBase) (string, error) { + record, err := app.FindFirstRecordByFilter("settings", "key='baseUrl'", nil) + if err != nil { + return "", err + } + + settings := BaseUrl{} + json.Unmarshal([]byte(record.GetString("value")), &settings) + + if err != nil { + return "", err + } + + return settings.BaseURL, nil +} + +type SitemapIndex struct { + XMLName xml.Name `xml:"sitemapindex"` + Xmlns string `xml:"xmlns,attr"` + Sitemaps []Sitemap `xml:"sitemap"` +} + +// Sitemap represents each entry +type Sitemap struct { + Loc string `xml:"loc"` + LastMod string `xml:"lastmod"` +} + +func ServeFeedsIndex(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + return se.Router.GET("/feeds/index.xml", func(e *core.RequestEvent) error { + index, err := makeSiteMapIndex(app) + if err != nil { + return err + } + + bytes, err := xml.MarshalIndent(index, "", " ") + if err != nil { + return err + } + + e.Response.Header().Add("content-type", "text/xml") + return e.String(http.StatusOK, xml.Header+string(bytes)) + }) +} + +func makeSiteMapIndex(app *pocketbase.PocketBase) (*SitemapIndex, error) { + index := &SitemapIndex{ + Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9", + } + + bseUrl, err := getBaseUrl(app) + if err != nil { + return nil, err + } + + langs, err := app.FindRecordsByFilter("lang", "active=true", "", 200, 0) + if err != nil { + return index, err + } + + lastMod := time.Now().Add(time.Hour * 24 * 7 * -1).Format("2006-01-02") + + for _, l := range langs { + index.Sitemaps = append(index.Sitemaps, Sitemap{ + Loc: bseUrl + path.Join("feeds/", l.Id, "sitemap.xml"), + LastMod: lastMod, + }) + } + + return index, nil +} diff --git a/backend/custom/seo/robots.go b/backend/custom/seo/robots.go new file mode 100644 index 0000000..1db5c49 --- /dev/null +++ b/backend/custom/seo/robots.go @@ -0,0 +1,50 @@ +package seo + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" +) + +type Robots struct { + Robots []string +} + +func ServeRobotsTxt(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + return se.Router.GET("/robots.txt", func(e *core.RequestEvent) error { + + text, err := getRobots(app) + if err != nil { + return err + } + return e.String(http.StatusOK, text) + }) +} + +func getRobots(app *pocketbase.PocketBase) (string, error) { + record, err := app.FindFirstRecordByFilter("settings", "key='robots_txt'", nil) + if err != nil { + return "", err + } + + settings := Robots{} + json.Unmarshal([]byte(record.GetString("value")), &settings) + + if err != nil { + return "", err + } + + baseUrl, err := getBaseUrl(app) + if err != nil { + return "", err + } + + settings.Robots = append(settings.Robots, fmt.Sprintf("\n\nSitemap: %s%s", baseUrl, "feeds/index.xml")) + + return strings.Join(settings.Robots, "\n"), nil +} diff --git a/backend/custom/supervise/supervise.go b/backend/custom/supervise/supervise.go new file mode 100644 index 0000000..3a6ad7d --- /dev/null +++ b/backend/custom/supervise/supervise.go @@ -0,0 +1,88 @@ +package supervise + +import ( + "context" + "log" + "os" + "os/exec" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" +) + +func ServeSubprocessSupervisor(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + + command, _ := app.RootCmd.PersistentFlags().GetString("subcommand") + + if len(command) > 0 { + cmdsub := strings.Split(command, " ") + if len(cmdsub) > 0 { + startNodeProcessSupervised(cmdsub[0], strings.Join(cmdsub[1:], " ")) + } + } + return nil +} + +func startNodeProcessSupervised(command string, args ...string) { + const maxRetries = 3 + const retryDelay = 30 * time.Second + + var ( + cmdMu sync.Mutex + cmd *exec.Cmd + ) + + stopChan := make(chan os.Signal, 1) + signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM) + + go func() { + retries := 0 + + for { + select { + case <-stopChan: + log.Println("Received shutdown signal. Terminating subprocess...") + cmdMu.Lock() + if cmd != nil && cmd.Process != nil { + _ = cmd.Process.Signal(syscall.SIGTERM) + } + cmdMu.Unlock() + return + + default: + ctx, cancel := context.WithCancel(context.Background()) + cmdMu.Lock() + cmd = exec.CommandContext(ctx, command, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmdMu.Unlock() + + log.Printf("Starting Process: %s %v\n", command, args) + err := cmd.Run() + cancel() // cancel the context when done + + if err != nil { + log.Printf("Process exited with error: %v\n", err) + retries++ + if retries >= maxRetries { + log.Printf("Process failed %d times. Shutting down application...", retries) + // _ = app.ResetBootstrapState() + os.Exit(1) + } + log.Printf("Retrying in %s (%d/%d)...", retryDelay, retries, maxRetries) + time.Sleep(retryDelay) + } else { + log.Printf("Process exited normally. Resetting retry count.") + retries = 0 + } + } + } + }() + +} diff --git a/backend/custom/version/version.go b/backend/custom/version/version.go new file mode 100644 index 0000000..d976c11 --- /dev/null +++ b/backend/custom/version/version.go @@ -0,0 +1,26 @@ +package version + +import ( + "net/http" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" +) + +var Version string +var BuildDate string +var Company string +var CompanyUrl string + +func ServeVersionInfo(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] { + return se.Router.GET("/api/version/send", func(e *core.RequestEvent) error { + + return e.JSON(http.StatusOK, map[string]string{ + "version": Version, + "build_date": BuildDate, + "company": Company, + "company_url": CompanyUrl, + }) + }) +} diff --git a/backend/custom/webpConverter/memFileReader.go b/backend/custom/webpConverter/memFileReader.go new file mode 100644 index 0000000..a3d0b5a --- /dev/null +++ b/backend/custom/webpConverter/memFileReader.go @@ -0,0 +1,14 @@ +package webpconverter + +import ( + "bytes" + "io" +) + +type memFileReader struct { + data []byte +} + +func (m *memFileReader) Open() (io.ReadSeekCloser, error) { + return readSeekCloser{bytes.NewReader(m.data)}, nil +} diff --git a/backend/custom/webpConverter/readSeekCloser.go b/backend/custom/webpConverter/readSeekCloser.go new file mode 100644 index 0000000..54444e1 --- /dev/null +++ b/backend/custom/webpConverter/readSeekCloser.go @@ -0,0 +1,9 @@ +package webpconverter + +import "bytes" + +type readSeekCloser struct { + *bytes.Reader +} + +func (r readSeekCloser) Close() error { return nil } diff --git a/backend/custom/webpConverter/webp_convert.go b/backend/custom/webpConverter/webp_convert.go new file mode 100644 index 0000000..d57381c --- /dev/null +++ b/backend/custom/webpConverter/webp_convert.go @@ -0,0 +1,144 @@ +package webpconverter + +import ( + "bytes" + "image" + "image/png" + "io" + "path/filepath" + "strings" + + "github.com/chai2010/webp" + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/hook" +) + +func CreateEventHandler(app *pocketbase.PocketBase) *hook.Handler[*core.RecordEvent] { + return &hook.Handler[*core.RecordEvent]{ + Func: func(e *core.RecordEvent) error { + fieldsData := e.Record.FieldsData() + for _, v := range fieldsData { + if files, ok := v.([]interface{}); ok { + for _, f := range files { + if file, ok := f.(*filesystem.File); ok { + if shouldBeReplacedWithWebp(file.Name) { + convertToWebp(file) + } + } + } + } + if f, ok := v.(interface{}); ok { + if file, ok := f.(*filesystem.File); ok { + if shouldBeReplacedWithWebp(file.Name) { + convertToWebp(file) + } + } + } + } + return e.Next() + }, + Priority: 100, + } +} + +func ThumbEventHandler(app *pocketbase.PocketBase) *hook.Handler[*core.FileDownloadRequestEvent] { + return &hook.Handler[*core.FileDownloadRequestEvent]{ + Func: func(fdre *core.FileDownloadRequestEvent) error { + if filepath.Ext(fdre.ServedName) == ".webp" { + + filename := fdre.Request.PathValue("filename") + baseFilesPath := fdre.Record.BaseFilesPath() + + fs, err := app.App.NewFilesystem() + if err != nil { + return err + } + defer fs.Close() + + blob, err := fs.GetReader(baseFilesPath + "/thumbs_" + filename + "/" + fdre.ServedName) + if err != nil { + blob, err = fs.GetReader(baseFilesPath + "/" + fdre.ServedName) + if err != nil { + return err + } + } + defer blob.Close() + + img, err := png.Decode(blob) + if err != nil { + return fdre.Next() + } + + buf := bytes.Buffer{} + err = webp.Encode(&buf, img, &webp.Options{Quality: 80}) + if err != nil { + return err + } + err = fs.Upload(buf.Bytes(), baseFilesPath+"/thumbs_"+filename+"/"+fdre.ServedName) + if err != nil { + _, err := fdre.Response.Write(buf.Bytes()) + if err != nil { + return err + } + return nil + } + return fdre.Next() + } + return fdre.Next() + }, + Priority: 99, + } +} + +func convertToWebp(file *filesystem.File) error { + file.Name = replaceExtWithWebp(file.Name) + file.OriginalName = replaceExtWithWebp(file.OriginalName) + ff, err := file.Reader.Open() + if err != nil { + return err + } + defer ff.Close() + fff, err := io.ReadAll(ff) + if err != nil { + return err + } + img, _, err := image.Decode(bytes.NewReader(fff)) + if err != nil { + return err + } + + buf := bytes.Buffer{} + err = webp.Encode(&buf, img, &webp.Options{Quality: 80}) + if err != nil { + return err + } + file.Reader = &memFileReader{data: buf.Bytes()} + file.Size = int64(buf.Len()) + return nil +} + +func replaceExtWithWebp(path string) string { + // List of image extensions to convert + exts := []string{".png", ".jpg", ".jpeg", ".bmp", ".tiff"} + + for _, ext := range exts { + if strings.HasSuffix(strings.ToLower(path), ext) { + return strings.TrimSuffix(path, ext) + ".webp" + } + } + return path +} + +func shouldBeReplacedWithWebp(path string) bool { + // List of image extensions to convert + exts := []string{".png", ".jpg", ".jpeg", ".bmp", ".tiff"} + + for _, ext := range exts { + if strings.HasSuffix(strings.ToLower(path), ext) { + return true + } + } + return false +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..2403ea2 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,42 @@ +module pocketbase + +go 1.24.0 + +require ( + github.com/chai2010/webp v1.4.0 + github.com/pocketbase/pocketbase v0.28.2 +) + +require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/disintegration/imaging v1.6.2 // indirect + github.com/domodwyer/mailyak/v3 v3.6.2 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/ganigeorgiev/fexpr v0.5.0 // indirect + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pocketbase/dbx v1.11.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/spf13/cast v1.8.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/image v0.27.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + modernc.org/libc v1.65.7 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.37.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..5c2bd85 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,127 @@ +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko= +github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= +github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk= +github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU= +github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= +github.com/pocketbase/pocketbase v0.28.2 h1:b6cfUfr5d4whvUFGFhI8oHRzx/eB76GCUQGftqgv9lM= +github.com/pocketbase/pocketbase v0.28.2/go.mod h1:ElwIYS1b5xS9w0U7AK7tsm6FuC0lzw57H8p/118Cu7g= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= +github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= +modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= +modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= +modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q= +modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= +modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..5868f2f --- /dev/null +++ b/backend/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + + "pocketbase/custom" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" +) + +func main() { + app := pocketbase.New() + + app = custom.ExtendApp(app) + + app.OnServe().BindFunc(func(se *core.ServeEvent) error { + + custom.LoadCustomCode(app, se) + + return se.Next() + }) + + if err := app.Start(); err != nil { + log.Fatal(err) + } +} diff --git a/composables/usePB.ts b/composables/usePB.ts new file mode 100644 index 0000000..3285404 --- /dev/null +++ b/composables/usePB.ts @@ -0,0 +1,15 @@ +// import { ref } from 'vue'; +import pocketbase from "pocketbase"; + +export const usePB = () => { + const nuxtApp = useNuxtApp(); + + const isServer = !!nuxtApp.ssrContext; + if (isServer) { + const pb = new pocketbase(process.env.POCKETBASE_URL || "http://127.0.0.1:8090"); + return pb; + } + + const pb = new pocketbase(window.location.origin); + return pb; +}; diff --git a/package.json b/package.json index f4bd2d5..3885c86 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@pinia/nuxt": "^0.11.0", "@tailwindcss/vite": "^4.1.7", "nuxt": "^3.17.4", + "pocketbase": "^0.26.0", "tailwindcss": "^4.1.7", "vue": "^3.5.14", "vue-router": "^4.5.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa62df7..21a71c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,10 @@ importers: version: 4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(yaml@2.8.0)) nuxt: specifier: ^3.17.4 - version: 3.17.4(@parcel/watcher@2.5.1)(@types/node@22.15.21)(db0@0.3.2)(encoding@0.1.13)(eslint@9.27.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.41.0)(terser@5.39.2)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(yaml@2.8.0))(yaml@2.8.0) + version: 3.17.4(@parcel/watcher@2.5.1)(@types/node@22.15.21)(db0@0.3.2)(eslint@9.27.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.41.0)(terser@5.39.2)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(yaml@2.8.0))(yaml@2.8.0) + pocketbase: + specifier: ^0.26.0 + version: 0.26.0 tailwindcss: specifier: ^4.1.7 version: 4.1.7 @@ -4471,6 +4474,9 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + pocketbase@0.26.0: + resolution: {integrity: sha512-WBBeOgz4Jnrd7a1KEzSBUJqpTortKKCcp16j5KoF+4tNIyQHsmynj+qRSvS56/RVacVMbAqO8Qkfj3N84fpzEw==} + portfinder@1.0.37: resolution: {integrity: sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==} engines: {node: '>= 10.12'} @@ -11405,6 +11411,8 @@ snapshots: pluralize@8.0.0: {} + pocketbase@0.26.0: {} + portfinder@1.0.37: dependencies: async: 3.2.6 diff --git a/taskfile.yml b/taskfile.yml index 6551951..1d6627a 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -13,56 +13,56 @@ tasks: - task --list silent: true - # compile_musl: - # aliases: [cm] - # desc: "compiles pocketbase for musl" - # env: - # CGO_ENABLED: "0" - # GOOS: "linux" - # GOARCH: "amd64" - # CC: "x86_64-linux-musl-gcc" - # cmds: - # - | - # mkdir -p ./.output - # cd ./backend - # {{.CompileStr}} + compile_musl: + aliases: [cm] + desc: "compiles pocketbase for musl" + env: + CGO_ENABLED: "0" + GOOS: "linux" + GOARCH: "amd64" + CC: "x86_64-linux-musl-gcc" + cmds: + - | + mkdir -p ./.output + cd ./backend + {{.CompileStr}} - # compile_gnu: - # aliases: [cg] - # desc: "compiles pocketbase for gnu" - # env: - # CGO_ENABLED: "0" - # GOOS: "linux" - # GOARCH: "amd64" - # cmds: - # - | - # mkdir -p ./.output - # cd ./backend - # {{.CompileStr}} + compile_gnu: + aliases: [cg] + desc: "compiles pocketbase for gnu" + env: + CGO_ENABLED: "0" + GOOS: "linux" + GOARCH: "amd64" + cmds: + - | + mkdir -p ./.output + cd ./backend + {{.CompileStr}} - # build_run_gnu: - # aliases: [br] - # desc: "compiles pocketbase for gnu" - # env: - # CGO_ENABLED: "0" - # GOOS: "linux" - # GOARCH: "amd64" - # cmds: - # - | - # mkdir -p ./.output - # cd ./backend - # go build -ldflags "-s -w" -o ../.pocketbase/pocketbase . - # cd .. - # ./.pocketbase/pocketbase serve --dir=./backend/pb_data + build_run_gnu: + aliases: [br] + desc: "compiles pocketbase for gnu" + env: + CGO_ENABLED: "0" + GOOS: "linux" + GOARCH: "amd64" + cmds: + - | + mkdir -p ./.output + cd ./backend + go build -ldflags "-s -w" -o ../.pocketbase/pocketbase . + cd .. + ./.pocketbase/pocketbase serve --dir=./backend/pb_data - # watch_backend: - # aliases: [wb] - # desc: "watch backend and compile" - # cmds: - # - | - # cd ./backend - # pwd - # air -build.args_bin='serve --dir=./pb_data' -build.exclude_dir=pb_data,backups -build.include_ext=go + watch_backend: + aliases: [wb] + desc: "watch backend and compile" + cmds: + - | + cd ./backend + pwd + air -build.args_bin='serve --dir=./pb_data' -build.exclude_dir=pb_data,backups -build.include_ext=go watch_front: aliases: [wf]