package frontend import ( "embed" "encoding/json" "html/template" "net/http" "os" "path/filepath" "git.sleepycat.moe/sam/mercury/frontend" "git.sleepycat.moe/sam/mercury/internal/database" "git.sleepycat.moe/sam/mercury/web/api" "git.sleepycat.moe/sam/mercury/web/app" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" vueglue "github.com/torenware/vite-go" ) //go:embed app.html var htmlTemplate string //go:embed assets var assets embed.FS type Frontend struct { *app.App glue *vueglue.VueGlue urlPrefix string fileServer http.Handler tmpl *template.Template } func New(app *app.App) *Frontend { fe := &Frontend{ App: app, } if app.AppConfig.Core.Dev { glue, err := vueglue.NewVueGlue(&vueglue.ViteConfig{ Environment: "development", AssetsPath: "frontend", EntryPoint: "src/main.tsx", Platform: "vue", FS: os.DirFS("frontend"), }) if err != nil { log.Err(err).Msg("Creating vite glue") os.Exit(1) } fe.glue = glue fe.urlPrefix = "/src/*" } else { glue, err := vueglue.NewVueGlue(&vueglue.ViteConfig{ Environment: "production", URLPrefix: "/assets/", AssetsPath: "dist", EntryPoint: "src/main.tsx", Platform: "vue", FS: frontend.Embed, }) if err != nil { log.Err(err).Msg("Creating vite glue") os.Exit(1) } fe.glue = glue fe.urlPrefix = "/assets/*" } fileServer, err := fe.glue.FileServer() if err != nil { log.Err(err).Msg("Creating vite file server") os.Exit(1) } fe.fileServer = fileServer tmpl, err := template.New("app").Parse(htmlTemplate) if err != nil { log.Err(err).Msg("Parsing frontend HTML template") os.Exit(1) } fe.tmpl = tmpl return fe } func (app *Frontend) AssetsPath() string { return app.urlPrefix } func (app *Frontend) ServeAssets(w http.ResponseWriter, r *http.Request) { app.fileServer.ServeHTTP(w, r) } func (app *Frontend) ServeFrontend(w http.ResponseWriter, r *http.Request) { _, ok := app.TokenFromContext(r.Context()) if !ok { http.Redirect(w, r, "/auth/login", http.StatusSeeOther) return } app.serveFrontend(w, r) } func (app *Frontend) ServeUser(w http.ResponseWriter, r *http.Request) { _, ok := app.TokenFromContext(r.Context()) if !ok { username := chi.URLParam(r, "username") http.Redirect(w, r, "/@"+username, http.StatusSeeOther) return } app.serveFrontend(w, r) } func (app *Frontend) ServeStatus(w http.ResponseWriter, r *http.Request) { _, ok := app.TokenFromContext(r.Context()) if !ok { username := chi.URLParam(r, "username") postID := chi.URLParam(r, "postID") http.Redirect(w, r, "/@"+username+"/posts/"+postID, http.StatusSeeOther) return } app.serveFrontend(w, r) } func (app *Frontend) serveFrontend(w http.ResponseWriter, r *http.Request) { token, ok := app.TokenFromContext(r.Context()) if !ok { log.Error().Msg("app.serveFrontend was called without a token being set") http.Redirect(w, r, "/auth/login", http.StatusSeeOther) return } acct, err := app.Account().ByID(r.Context(), token.UserID) if err != nil { log.Err(err).Msg("fetching account") app.ErrorTemplate(w, r, "Internal server error", "An internal server error occurred. Please try again later.") return } b, err := json.Marshal(api.DBAccountToSelfAccount(acct)) if err != nil { log.Err(err).Msg("marshaling account json") app.ErrorTemplate(w, r, "Internal server error", "An internal server error occurred. Please try again later.") return } w.Header().Set("Content-Type", "text/html") err = app.tmpl.Execute(w, map[string]any{ "Config": database.DefaultConfig, "Vue": app.glue, "AccountData": template.HTML(b), }) if err != nil { log.Err(err).Msg("executing frontend template") } } func (app *Frontend) ServeStaticAssets(w http.ResponseWriter, r *http.Request) { if app.AppConfig.Core.Dev { // TODO: this is unsafe path := filepath.Join("web/frontend/assets/", chi.URLParam(r, "*")) http.ServeFile(w, r, path) return } _ = assets }