add pocketbase
82
backend/custom/cloudflare/TurnStilleCaptcha.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
95
backend/custom/cloudflare/cloudflare.go
Normal file
@ -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
|
||||||
|
}
|
8
backend/custom/cloudflare/js.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package cloudflare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed TurnStilleCaptcha.js
|
||||||
|
var JS embed.FS
|
61
backend/custom/custom.go
Normal file
@ -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
|
||||||
|
}
|
56
backend/custom/gtm/gtm.go
Normal file
@ -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
|
||||||
|
}
|
29
backend/custom/gtm/gtm.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
8
backend/custom/gtm/js.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package gtm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed gtm.js
|
||||||
|
var JS embed.FS
|
175
backend/custom/mail/mail.go
Normal file
@ -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", "<br>"),
|
||||||
|
"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
|
||||||
|
}
|
50
backend/custom/manifest/manifest.go
Normal file
@ -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
|
||||||
|
}
|
121
backend/custom/proxy/proxy.go
Normal file
@ -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
|
||||||
|
}
|
194
backend/custom/seo/feeds.go
Normal file
@ -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 <sitemap> 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
|
||||||
|
}
|
50
backend/custom/seo/robots.go
Normal file
@ -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
|
||||||
|
}
|
88
backend/custom/supervise/supervise.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
26
backend/custom/version/version.go
Normal file
@ -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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
14
backend/custom/webpConverter/memFileReader.go
Normal file
@ -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
|
||||||
|
}
|
9
backend/custom/webpConverter/readSeekCloser.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package webpconverter
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
type readSeekCloser struct {
|
||||||
|
*bytes.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r readSeekCloser) Close() error { return nil }
|
144
backend/custom/webpConverter/webp_convert.go
Normal file
@ -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
|
||||||
|
}
|
42
backend/go.mod
Normal file
@ -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
|
||||||
|
)
|
127
backend/go.sum
Normal file
@ -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=
|
27
backend/main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
BIN
backend/pb_data/auxiliary.db
Normal file
BIN
backend/pb_data/auxiliary.db-shm
Normal file
BIN
backend/pb_data/auxiliary.db-wal
Normal file
BIN
backend/pb_data/backups/pb_backup_acme_20250523140124.zip
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"application/zip","user.metadata":{"original-filename":"pb_backup_acme_20250523140124.zip"},"md5":"dbtXP4WTg7TAA/DZiykQNg=="}
|
BIN
backend/pb_data/data.db
Normal file
BIN
backend/pb_data/data.db-shm
Normal file
BIN
backend/pb_data/data.db-wal
Normal file
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"Rectangle 87.webp"},"md5":"oxzc3n8PP4216yMwSMwvnw=="}
|
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"Rectangle 87.webp"},"md5":"oxzc3n8PP4216yMwSMwvnw=="}
|
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"ptezH5dtiD2YbO/0Tw6HoA=="}
|
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"ptezH5dtiD2YbO/0Tw6HoA=="}
|
After Width: | Height: | Size: 76 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"CON_06_03_00_00 Zespol kontnerow 6m i 3m 1.JPG.webp"},"md5":"o4O+AP6RKyBPkjP+oHFdCg=="}
|
After Width: | Height: | Size: 950 B |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"gkIwoHGOSQpOXXB8eADzjw=="}
|
After Width: | Height: | Size: 35 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"99eI8uDdYbm2V6RBEXBmdQ=="}
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"i8JotqJVLiFT7ksYWWx+3w=="}
|
After Width: | Height: | Size: 44 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"VeDfPHH9l+0azSaETSUOrg=="}
|
After Width: | Height: | Size: 116 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"IejPE5jTnKJYAVV9aKOWxA=="}
|
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"vBgQbwbWepxwI+0eaJ4WJg=="}
|
After Width: | Height: | Size: 92 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"Zlozenie dwoch kontnerow 6 plus 3 1.JPG.webp"},"md5":"pl77oY+XT+cxx8mhsEb8aA=="}
|
After Width: | Height: | Size: 1021 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"bike-creation-workshop.webp"},"md5":"Eqd3581S1wZ3Hg4zX5MbnA=="}
|
After Width: | Height: | Size: 2.3 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"1FwGEt0KlNu+wGlrCs6pWQ=="}
|
After Width: | Height: | Size: 57 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"GPr8lSC2IOlpmlZMuog+vg=="}
|
After Width: | Height: | Size: 103 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"u+wWpSyRYccIP8KafzAFhg=="}
|
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"EqeJrhC0/zOFQP2msVamSQ=="}
|
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"Vyiopx1+jhzdpCXp5w7uCg=="}
|
After Width: | Height: | Size: 33 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"Frame 272.webp"},"md5":"XAsOiE/X7ZEwulPOYF9NlQ=="}
|
After Width: | Height: | Size: 962 B |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"fAtnETPk9L0Obgun3e4kcg=="}
|
After Width: | Height: | Size: 37 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"GkVxfnlXP6ZX23UCqCl4xQ=="}
|
After Width: | Height: | Size: 5.1 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"BaLB5W1PMEp/uHS/BrHS5w=="}
|
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"1.webp"},"md5":"ATVnLTgl41Z1Im8uQWlA+w=="}
|
After Width: | Height: | Size: 30 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"3.webp"},"md5":"ZlpWSXdwJyLLctWbbta7dA=="}
|
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"4.webp"},"md5":"MSfnrT0K0V/J1wU7L7//BQ=="}
|
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":{"original-filename":"5.webp"},"md5":"mbDKA55L0ECCKEG5br653Q=="}
|
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"NPIquE5fkSCgSQWlXsWodQ=="}
|
After Width: | Height: | Size: 102 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"dmbS6pzyJCFvuZ83l+8QLQ=="}
|
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"nNs2RKawHZ+Ppoi8yz9ffg=="}
|
After Width: | Height: | Size: 175 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"vZnTm4aQvlMATNAOrsIdFA=="}
|
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"SK5zizJ7Hb4ocA/t71wfMA=="}
|
After Width: | Height: | Size: 114 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"IlyLtAz/rnXPED2jx0uXSw=="}
|
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"ychAenq/Lmk/Lq9KPIOMGw=="}
|
After Width: | Height: | Size: 136 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"q/Tp9skDG/0nMJhCCoYq9Q=="}
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"rpgHgFhEo4fblf9ifVS5oQ=="}
|
After Width: | Height: | Size: 105 KiB |
@ -0,0 +1 @@
|
|||||||
|
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/webp","user.metadata":null,"md5":"soCq5KHXXA5Odk+Ch5tu2Q=="}
|