diff --git a/html/func.go b/html/func.go index 2d5c9890..7a590cc3 100644 --- a/html/func.go +++ b/html/func.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "github.com/Gleipnir-Technology/nidus-sync/static" + //"github.com/Gleipnir-Technology/nidus-sync/static" "github.com/aarondl/opt/null" "github.com/google/uuid" "github.com/rs/zerolog/log" @@ -63,10 +63,12 @@ func bigNumber(n int) string { return result.String() } func bundlePathCSS() string { - return static.BundlePathCSS + //return static.BundlePathCSS + return "nowhere" } func bundlePathJS() string { - return static.BundlePathJS + //return static.BundlePathJS + return "nowhere" } func displayUploadStatus(s string) string { switch s { diff --git a/package.json b/package.json index 8c79cac0..fd8262b6 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "dev-rmo": "vite serve vite/rmo", "dev-sync": "vite serve vite/sync", "generate-icons": "node generate-icons.js", + "preview-rmo": "vite preview vite/rmo --port 9001", + "preview-sync": "vite preview vite/sync --port 9001", "typecheck": "vue-tsc --noEmit", "typecheck:watch": "vue-tsc --noEmit --watch" } diff --git a/rmo/routes.go b/rmo/routes.go index 170a98b1..0c47fc32 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -1,54 +1,56 @@ package rmo import ( - "github.com/Gleipnir-Technology/nidus-sync/html" + //"github.com/Gleipnir-Technology/nidus-sync/html" "github.com/Gleipnir-Technology/nidus-sync/static" "github.com/gorilla/mux" ) func Router(r *mux.Router) { - r.HandleFunc("/", getRoot).Methods("GET") - r.HandleFunc("/submit-complete", getSubmitComplete).Methods("GET") + /* + r.HandleFunc("/submit-complete", getSubmitComplete).Methods("GET") - r.HandleFunc("/district", getDistrictList).Methods("GET") - r.HandleFunc("/district/{slug}", getRootDistrict).Methods("GET") - r.HandleFunc("/district/{slug}/compliance", getDistrictCompliance).Methods("GET") - r.HandleFunc("/district/{slug}/compliance/address", getDistrictComplianceAddress).Methods("GET") - r.HandleFunc("/district/{slug}/compliance/complete", getDistrictComplianceComplete).Methods("GET") - r.HandleFunc("/district/{slug}/compliance/concern", getDistrictComplianceConcern).Methods("GET") - r.HandleFunc("/district/{slug}/compliance/contact", getDistrictComplianceContact).Methods("GET") - r.HandleFunc("/district/{slug}/compliance/evidence", getDistrictComplianceEvidence).Methods("GET") - r.HandleFunc("/district/{slug}/compliance/permission", getDistrictCompliancePermission).Methods("GET") - r.HandleFunc("/district/{slug}/compliance/process", getDistrictComplianceProcess).Methods("GET") - r.HandleFunc("/district/{slug}/compliance/submit", getDistrictComplianceSubmit).Methods("GET") - r.HandleFunc("/district/{slug}/nuisance", getNuisanceDistrict).Methods("GET") - //r.HandleFunc("/district/{slug}/nuisance-submit-complete", renderMock(mockNuisanceSubmitCompleteT)).Methods("GET") - //r.HandleFunc("/district/{slug}/status", renderMock(mockStatusT)).Methods("GET") - r.HandleFunc("/district/{slug}/water", getWaterDistrict).Methods("GET") - //r.HandleFunc("/district/{slug}/water", postWaterDistrict).Methods("POST") - r.HandleFunc("/error", getError).Methods("GET") + r.HandleFunc("/district", getDistrictList).Methods("GET") + r.HandleFunc("/district/{slug}", getRootDistrict).Methods("GET") + r.HandleFunc("/district/{slug}/compliance", getDistrictCompliance).Methods("GET") + r.HandleFunc("/district/{slug}/compliance/address", getDistrictComplianceAddress).Methods("GET") + r.HandleFunc("/district/{slug}/compliance/complete", getDistrictComplianceComplete).Methods("GET") + r.HandleFunc("/district/{slug}/compliance/concern", getDistrictComplianceConcern).Methods("GET") + r.HandleFunc("/district/{slug}/compliance/contact", getDistrictComplianceContact).Methods("GET") + r.HandleFunc("/district/{slug}/compliance/evidence", getDistrictComplianceEvidence).Methods("GET") + r.HandleFunc("/district/{slug}/compliance/permission", getDistrictCompliancePermission).Methods("GET") + r.HandleFunc("/district/{slug}/compliance/process", getDistrictComplianceProcess).Methods("GET") + r.HandleFunc("/district/{slug}/compliance/submit", getDistrictComplianceSubmit).Methods("GET") + r.HandleFunc("/district/{slug}/nuisance", getNuisanceDistrict).Methods("GET") + //r.HandleFunc("/district/{slug}/nuisance-submit-complete", renderMock(mockNuisanceSubmitCompleteT)).Methods("GET") + //r.HandleFunc("/district/{slug}/status", renderMock(mockStatusT)).Methods("GET") + r.HandleFunc("/district/{slug}/water", getWaterDistrict).Methods("GET") + //r.HandleFunc("/district/{slug}/water", postWaterDistrict).Methods("POST") + r.HandleFunc("/error", getError).Methods("GET") - r.HandleFunc("/privacy", getPrivacy).Methods("GET") - r.HandleFunc("/robots.txt", getRobots).Methods("GET") - r.HandleFunc("/email/render/{code}", getEmailByCode).Methods("GET") - r.HandleFunc("/email/confirm", getEmailConfirm).Methods("GET") - r.HandleFunc("/email/confirm", postEmailConfirm).Methods("POST") - r.HandleFunc("/email/confirm/complete", getEmailConfirmComplete).Methods("GET") - r.HandleFunc("/email/unsubscribe", getEmailUnsubscribe).Methods("GET") - r.HandleFunc("/email/unsubscribe/report/{report_id}", getEmailReportUnsubscribe).Methods("GET") - r.HandleFunc("/image/{uuid}", getImageByUUID).Methods("GET") - r.HandleFunc("/mailer/{public_id}", html.MakeGet(getMailer)).Methods("GET") - r.HandleFunc("/mailer/{public_id}/confirm", html.MakePost(postMailerConfirm)).Methods("POST") - r.HandleFunc("/mailer/{public_id}/contribute", html.MakeGet(getMailerContribute)).Methods("GET") - r.HandleFunc("/mailer/{public_id}/evidence", html.MakeGet(getMailerEvidence)).Methods("GET") - r.HandleFunc("/mailer/{public_id}/schedule", html.MakeGet(getMailerSchedule)).Methods("GET") - r.HandleFunc("/mailer/{public_id}/update", html.MakeGet(getMailerUpdate)).Methods("GET") - r.HandleFunc("/register-notifications", postRegisterNotifications).Methods("POST") - r.HandleFunc("/register-notifications-complete", getRegisterNotificationsComplete).Methods("GET") - r.HandleFunc("/report/suggest", getReportSuggestion).Methods("GET") - r.HandleFunc("/scss/*", getScssDebug).Methods("GET") - r.HandleFunc("/status", getStatus).Methods("GET") - r.HandleFunc("/status/{report_id}", getStatusByID).Methods("GET") - r.HandleFunc("/terms-of-service", getTerms).Methods("GET") + r.HandleFunc("/privacy", getPrivacy).Methods("GET") + r.HandleFunc("/robots.txt", getRobots).Methods("GET") + r.HandleFunc("/email/render/{code}", getEmailByCode).Methods("GET") + r.HandleFunc("/email/confirm", getEmailConfirm).Methods("GET") + r.HandleFunc("/email/confirm", postEmailConfirm).Methods("POST") + r.HandleFunc("/email/confirm/complete", getEmailConfirmComplete).Methods("GET") + r.HandleFunc("/email/unsubscribe", getEmailUnsubscribe).Methods("GET") + r.HandleFunc("/email/unsubscribe/report/{report_id}", getEmailReportUnsubscribe).Methods("GET") + r.HandleFunc("/image/{uuid}", getImageByUUID).Methods("GET") + r.HandleFunc("/mailer/{public_id}", html.MakeGet(getMailer)).Methods("GET") + r.HandleFunc("/mailer/{public_id}/confirm", html.MakePost(postMailerConfirm)).Methods("POST") + r.HandleFunc("/mailer/{public_id}/contribute", html.MakeGet(getMailerContribute)).Methods("GET") + r.HandleFunc("/mailer/{public_id}/evidence", html.MakeGet(getMailerEvidence)).Methods("GET") + r.HandleFunc("/mailer/{public_id}/schedule", html.MakeGet(getMailerSchedule)).Methods("GET") + r.HandleFunc("/mailer/{public_id}/update", html.MakeGet(getMailerUpdate)).Methods("GET") + r.HandleFunc("/register-notifications", postRegisterNotifications).Methods("POST") + r.HandleFunc("/register-notifications-complete", getRegisterNotificationsComplete).Methods("GET") + r.HandleFunc("/report/suggest", getReportSuggestion).Methods("GET") + r.HandleFunc("/scss/*", getScssDebug).Methods("GET") + r.HandleFunc("/status", getStatus).Methods("GET") + r.HandleFunc("/status/{report_id}", getStatusByID).Methods("GET") + r.HandleFunc("/terms-of-service", getTerms).Methods("GET") + */ static.AddStaticRoute(r, "/static") + r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/rmo")) } diff --git a/static/static.go b/static/static.go index 6d4a824e..2f7ee2e7 100644 --- a/static/static.go +++ b/static/static.go @@ -2,6 +2,7 @@ package static import ( "embed" + "errors" "fmt" "io/fs" "net/http" @@ -22,90 +23,122 @@ var embeddedStaticFS embed.FS // static files from a http.FileSystem. var startedTime time.Time = time.Now() -var localFS http.Dir +var localFS = http.Dir("./static") func AddStaticRoute(r *mux.Router, path string) { - if localFS == "" { - localFS = http.Dir("./static") - // Useful for debugging embedded file issues - if config.IsProductionEnvironment() { - fs.WalkDir(embeddedStaticFS, ".", func(path string, d fs.DirEntry, err error) error { - log.Debug().Str("path", path).Send() - return nil - }) + fileServer(r, "/static/", localFS, embeddedStaticFS) +} + +func SinglePageApp(gen_path string) http.Handler { + // Accept the path as relative from project root, but + // fix it to actually be relative to static filesystem root + path := strings.TrimPrefix(gen_path, "static/") + return spaHandler{ + genRoot: path, + } + +} + +type spaHandler struct { + genRoot string +} + +func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + request_path := r.URL.Path + path := h.genRoot + request_path + fileToServe, err := fileFromFilesystem(path) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // default to index file + fileToServe, err = fileFromFilesystem(h.genRoot + "/index.html") + + if err != nil { + log.Error().Err(err).Msg("failed to open embedded index file") + http.Error(w, err.Error(), http.StatusInternalServerError) + return } } - fileServer(r, "/static/", localFS, embeddedStaticFS) + serveFileMaybeEmbedded(w, r, *fileToServe, path) } func fileServer(r *mux.Router, path string, root http.FileSystem, embeddedFS embed.FS) { log.Debug().Str("path", path).Msg("adding file server") r.PathPrefix(path).HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - //rctx := chi.RouteContext(r.Context()) - - //pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*") - //pathPrefix := strings.TrimPrefix(r.URL.Path, "/static") path := strings.TrimPrefix(r.URL.Path, "/static/") - //log.Debug().Str("path", path).Msg("handling request") - - var err error - var fileToServe http.File - found := false - - // For dev, try the current filesystem - if !config.IsProductionEnvironment() { - // Try to open from local filesystem for development - fileToServe, err = root.Open(path) - if err != nil { - log.Warn().Err(err).Str("path", path).Msg("Failed to read static file for dev") - found = false - } else { - found = true - } - } - // For production use the embedded filesystem - if !found { - // Requested paths start with - embeddedFile, err := embeddedFS.Open(path) - - if err != nil { - log.Debug().Err(err).Str("embedded path", path).Msg("Failed to find resource") + fileToServe, err := fileFromFilesystem(path) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { http.NotFound(w, r) - return - } - - // Wrap the embedded file to implement http.File interface - fileToServe = &embeddedFileWrapper{embeddedFile} - } - - // Create a custom ResponseWriter that allows us to modify headers - crw := &customResponseWriter{ResponseWriter: w} - - // Add caching headers - if config.IsProductionEnvironment() { - ext := filepath.Ext(path) - switch ext { - case ".css", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".woff", ".woff2", ".ttf": - // Cache for 1 week (604800 seconds) - crw.Header().Set("Cache-Control", "public, max-age=604800, stale-while-revalidate=86400") - default: - // If it's a generated file, cache it essentially forever (1 year) - if strings.HasPrefix(path, "/static/gen/") { - crw.Header().Set("Cache-Control", "public, max-age=31536000, immutable") - } else { - // Other files, 1 hour - crw.Header().Set("Cache-Control", "public, max-age=3600") - } } } - // Serve the file - http.ServeContent(crw, r, path, startedTime, fileToServe) - - // Close the file - fileToServe.Close() + serveFileMaybeEmbedded(w, r, *fileToServe, path) }) } +func fileFromFilesystem(path string) (*http.File, error) { + var err error + var fileToServe http.File + found := false + + // For dev, try the current filesystem + if !config.IsProductionEnvironment() { + // Try to open from local filesystem for development + fileToServe, err = localFS.Open(path) + if err != nil { + log.Warn().Err(err).Str("path", path).Msg("Failed to read static file for dev") + found = false + } else { + found = true + } + } + // For production use the embedded filesystem + if !found { + // Requested paths start with + embeddedFile, err := embeddedStaticFS.Open(path) + + if err != nil { + return nil, fmt.Errorf("open embedded file: %w", err) + } + + // Wrap the embedded file to implement http.File interface + fileToServe = &embeddedFileWrapper{embeddedFile} + } + return &fileToServe, nil +} + +// Serve a file from the filesystem if we're in development mode or from the +// embedded filesystem if we aren't +func serveFileMaybeEmbedded(w http.ResponseWriter, r *http.Request, fileToServe http.File, path string) { + // Create a custom ResponseWriter that allows us to modify headers + crw := &customResponseWriter{ResponseWriter: w} + + // Add caching headers + if config.IsProductionEnvironment() { + ext := filepath.Ext(path) + switch ext { + case ".css", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".woff", ".woff2", ".ttf": + // Cache for 1 week (604800 seconds) + crw.Header().Set("Cache-Control", "public, max-age=604800, stale-while-revalidate=86400") + default: + // If it's a generated file, cache it essentially forever (1 year) + if strings.HasPrefix(path, "/static/gen/") { + crw.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + } else { + // Other files, 1 hour + crw.Header().Set("Cache-Control", "public, max-age=3600") + } + } + } + // Serve the file + http.ServeContent(crw, r, path, startedTime, fileToServe) + + // Close the file + fileToServe.Close() +} + type embeddedFileWrapper struct { file fs.File } diff --git a/sync/routes.go b/sync/routes.go index 5a046caa..94100928 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -35,8 +35,9 @@ func Router(r *mux.Router) { r.HandleFunc("/qr-code/mailer/{code}", getQRCodeMailer) r.HandleFunc("/template-test", getTemplateTest) - r.HandleFunc("/", getRoot) - r.HandleFunc("/_/*", getRoot) + //r.HandleFunc("/", getRoot) + //r.HandleFunc("/_/*", getRoot) static.AddStaticRoute(r, "/static") + r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/sync")) } diff --git a/vite/rmo/index.html b/vite/rmo/index.html index 3671bd4a..e61159fa 100644 --- a/vite/rmo/index.html +++ b/vite/rmo/index.html @@ -8,6 +8,6 @@
- + diff --git a/vite/rmo/vite.config.js b/vite/rmo/vite.config.js index 64e0a4e4..2b0efd96 100644 --- a/vite/rmo/vite.config.js +++ b/vite/rmo/vite.config.js @@ -33,12 +33,12 @@ export default defineConfig({ }, build: { - manifest: true, + manifest: false, outDir: "static/gen/rmo", emptyOutDir: true, rollupOptions: { input: { - main: path.resolve(__dirname, "./main.ts"), + main: path.resolve(__dirname, "./index.html"), }, output: { entryFileNames: "js/bundle.[hash].js", diff --git a/vite/sync/index.html b/vite/sync/index.html index ec8cb97c..45111335 100644 --- a/vite/sync/index.html +++ b/vite/sync/index.html @@ -8,6 +8,6 @@
- + diff --git a/vite/sync/vite.config.js b/vite/sync/vite.config.js index e9e14aca..131f53cd 100644 --- a/vite/sync/vite.config.js +++ b/vite/sync/vite.config.js @@ -33,12 +33,12 @@ export default defineConfig({ }, build: { - manifest: true, + manifest: false, outDir: "static/gen/sync", emptyOutDir: true, rollupOptions: { input: { - main: path.resolve(__dirname, "./main.ts"), + main: path.resolve(__dirname, "./index.html"), }, output: { entryFileNames: "js/bundle.[hash].js",