2025-11-03 12:22:06 +00:00
|
|
|
package main
|
2025-11-03 12:38:47 +00:00
|
|
|
|
2025-11-03 12:22:06 +00:00
|
|
|
import (
|
2025-11-04 23:11:32 +00:00
|
|
|
"context"
|
2025-11-03 12:38:47 +00:00
|
|
|
"net/http"
|
|
|
|
|
"os"
|
2025-11-07 02:29:34 +00:00
|
|
|
"os/signal"
|
2025-11-24 19:45:37 +00:00
|
|
|
"runtime/debug"
|
2025-11-07 02:29:34 +00:00
|
|
|
"sync"
|
|
|
|
|
"syscall"
|
2025-11-03 12:38:47 +00:00
|
|
|
"time"
|
|
|
|
|
|
2025-11-24 18:08:24 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
2025-11-05 17:15:33 +00:00
|
|
|
"github.com/alexedwards/scs/pgxstore"
|
2025-11-03 12:38:47 +00:00
|
|
|
"github.com/alexedwards/scs/v2"
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
2025-11-13 20:11:00 +00:00
|
|
|
"github.com/rs/zerolog"
|
|
|
|
|
"github.com/rs/zerolog/log"
|
2025-11-03 12:22:06 +00:00
|
|
|
)
|
2025-11-03 12:38:47 +00:00
|
|
|
|
|
|
|
|
var sessionManager *scs.SessionManager
|
|
|
|
|
|
2025-12-02 00:30:08 +00:00
|
|
|
var BaseURL, ClientID, ClientSecret, Environment, FieldseekerSchemaDirectory, MapboxToken string
|
2025-11-03 12:38:47 +00:00
|
|
|
|
2025-11-03 12:22:06 +00:00
|
|
|
func main() {
|
2025-11-13 20:11:00 +00:00
|
|
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
|
|
|
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
|
|
|
|
|
2025-11-03 12:38:47 +00:00
|
|
|
ClientID = os.Getenv("ARCGIS_CLIENT_ID")
|
|
|
|
|
if ClientID == "" {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Msg("You must specify a non-empty ARCGIS_CLIENT_ID")
|
2025-11-03 12:38:47 +00:00
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
ClientSecret = os.Getenv("ARCGIS_CLIENT_SECRET")
|
|
|
|
|
if ClientSecret == "" {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Msg("You must specify a non-empty ARCGIS_CLIENT_SECRET")
|
2025-11-03 12:38:47 +00:00
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
BaseURL = os.Getenv("BASE_URL")
|
|
|
|
|
if BaseURL == "" {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Msg("You must specify a non-empty BASE_URL")
|
2025-11-03 12:38:47 +00:00
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
bind := os.Getenv("BIND")
|
|
|
|
|
if bind == "" {
|
|
|
|
|
bind = ":9001"
|
|
|
|
|
}
|
2025-11-13 15:15:35 +00:00
|
|
|
Environment = os.Getenv("ENVIRONMENT")
|
|
|
|
|
if Environment == "" {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Msg("You must specify a non-empty ENVIRONMENT")
|
2025-11-13 15:15:35 +00:00
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
if !(Environment == "PRODUCTION" || Environment == "DEVELOPMENT") {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Str("ENVIRONMENT", Environment).Msg("ENVIRONMENT should be either DEVELOPMENT or PRODUCTION")
|
2025-11-13 15:15:35 +00:00
|
|
|
os.Exit(2)
|
|
|
|
|
}
|
2025-11-13 17:48:38 +00:00
|
|
|
MapboxToken = os.Getenv("MAPBOX_TOKEN")
|
|
|
|
|
if MapboxToken == "" {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Msg("You must specify a non-empty MAPBOX_TOKEN")
|
2025-11-13 17:48:38 +00:00
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
2025-11-04 23:11:32 +00:00
|
|
|
pg_dsn := os.Getenv("POSTGRES_DSN")
|
|
|
|
|
if pg_dsn == "" {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Msg("You must specify a non-empty POSTGRES_DSN")
|
2025-11-04 23:11:32 +00:00
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
2025-12-02 00:30:08 +00:00
|
|
|
FieldseekerSchemaDirectory = os.Getenv("FIELDSEEKER_SCHEMA_DIRECTORY")
|
|
|
|
|
if FieldseekerSchemaDirectory == "" {
|
|
|
|
|
log.Error().Msg("You must specify a non-empty FIELDSEEKER_SCHEMA_DIRECTORY")
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
2025-11-03 12:38:47 +00:00
|
|
|
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Info().Msg("Starting...")
|
2025-11-24 18:08:24 +00:00
|
|
|
err := db.InitializeDatabase(context.TODO(), pg_dsn)
|
2025-11-04 23:11:32 +00:00
|
|
|
if err != nil {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Str("err", err.Error()).Msg("Failed to connect to database")
|
2025-11-04 23:11:32 +00:00
|
|
|
os.Exit(2)
|
|
|
|
|
}
|
2025-11-03 12:38:47 +00:00
|
|
|
sessionManager = scs.New()
|
2025-11-24 18:08:24 +00:00
|
|
|
sessionManager.Store = pgxstore.New(db.PGInstance.PGXPool)
|
2025-11-03 12:38:47 +00:00
|
|
|
sessionManager.Lifetime = 24 * time.Hour
|
|
|
|
|
|
2025-11-24 19:45:37 +00:00
|
|
|
router_logger := log.With().Logger()
|
2025-11-03 12:38:47 +00:00
|
|
|
r := chi.NewRouter()
|
2025-11-24 19:45:37 +00:00
|
|
|
r.Use(LoggerMiddleware(&router_logger))
|
2025-11-03 12:38:47 +00:00
|
|
|
r.Use(sessionManager.LoadAndSave)
|
|
|
|
|
|
2025-11-13 15:50:10 +00:00
|
|
|
// Root is a special endpoint that is neither authenticated nor unauthenticated
|
2025-11-03 12:38:47 +00:00
|
|
|
r.Get("/", getRoot)
|
2025-11-13 15:50:10 +00:00
|
|
|
|
|
|
|
|
// Unauthenticated endpoints
|
2025-11-06 00:23:58 +00:00
|
|
|
r.Get("/arcgis/oauth/begin", getArcgisOauthBegin)
|
|
|
|
|
r.Get("/arcgis/oauth/callback", getArcgisOauthCallback)
|
2025-11-13 15:50:10 +00:00
|
|
|
r.Get("/favicon.ico", getFavicon)
|
|
|
|
|
|
2025-11-12 21:27:51 +00:00
|
|
|
r.Get("/oauth/refresh", getOAuthRefresh)
|
2025-11-13 15:50:10 +00:00
|
|
|
|
2025-11-10 22:42:19 +00:00
|
|
|
r.Get("/phone-call", getPhoneCall)
|
2025-11-13 15:50:10 +00:00
|
|
|
r.Get("/qr-code/report/{code}", getQRCodeReport)
|
2025-11-05 21:05:10 +00:00
|
|
|
r.Get("/report", getReport)
|
2025-11-05 21:37:11 +00:00
|
|
|
r.Get("/report/{code}", getReportDetail)
|
2025-11-05 22:03:33 +00:00
|
|
|
r.Get("/report/{code}/confirm", getReportConfirmation)
|
2025-11-05 21:51:23 +00:00
|
|
|
r.Get("/report/{code}/contribute", getReportContribute)
|
|
|
|
|
r.Get("/report/{code}/evidence", getReportEvidence)
|
2025-11-05 21:57:59 +00:00
|
|
|
r.Get("/report/{code}/schedule", getReportSchedule)
|
2025-11-05 23:41:21 +00:00
|
|
|
r.Get("/report/{code}/update", getReportUpdate)
|
2025-11-08 00:04:44 +00:00
|
|
|
r.Get("/service-request", getServiceRequest)
|
|
|
|
|
r.Get("/service-request/{code}", getServiceRequestDetail)
|
|
|
|
|
r.Get("/service-request-location", getServiceRequestLocation)
|
|
|
|
|
r.Get("/service-request-mosquito", getServiceRequestMosquito)
|
|
|
|
|
r.Get("/service-request-pool", getServiceRequestPool)
|
2025-11-10 15:27:22 +00:00
|
|
|
r.Get("/service-request-quick", getServiceRequestQuick)
|
|
|
|
|
r.Get("/service-request-quick-confirmation", getServiceRequestQuickConfirmation)
|
2025-11-08 00:04:44 +00:00
|
|
|
r.Get("/service-request-updates", getServiceRequestUpdates)
|
2025-11-19 15:19:42 +00:00
|
|
|
r.Get("/signin", getSignin)
|
2025-11-05 17:15:33 +00:00
|
|
|
r.Post("/signin", postSignin)
|
2025-11-04 00:02:51 +00:00
|
|
|
r.Get("/signup", getSignup)
|
2025-11-04 23:21:13 +00:00
|
|
|
r.Post("/signup", postSignup)
|
2025-11-13 15:50:10 +00:00
|
|
|
|
|
|
|
|
// Authenticated endpoints
|
2025-11-19 15:21:06 +00:00
|
|
|
r.Method("GET", "/cell/{cell}", NewEnsureAuth(getCellDetails))
|
2025-11-13 15:50:10 +00:00
|
|
|
r.Method("GET", "/settings", NewEnsureAuth(getSettings))
|
2025-11-20 14:56:34 +00:00
|
|
|
r.Method("GET", "/source/{globalid}", NewEnsureAuth(getSource))
|
2025-11-15 21:33:01 +00:00
|
|
|
r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", NewEnsureAuth(getVectorTiles))
|
2025-11-03 22:13:11 +00:00
|
|
|
|
|
|
|
|
localFS := http.Dir("./static")
|
|
|
|
|
FileServer(r, "/static", localFS, embeddedStaticFS, "static")
|
|
|
|
|
|
2025-11-07 02:29:34 +00:00
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
2025-11-07 05:46:41 +00:00
|
|
|
NewOAuthTokenChannel = make(chan struct{}, 10)
|
2025-11-07 02:29:34 +00:00
|
|
|
|
|
|
|
|
var waitGroup sync.WaitGroup
|
|
|
|
|
|
|
|
|
|
waitGroup.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer waitGroup.Done()
|
2025-11-07 05:46:41 +00:00
|
|
|
refreshFieldseekerData(ctx, NewOAuthTokenChannel)
|
2025-11-07 02:29:34 +00:00
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
server := &http.Server{
|
|
|
|
|
Addr: bind,
|
|
|
|
|
Handler: r,
|
|
|
|
|
}
|
|
|
|
|
go func() {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Info().Str("address", bind).Msg("Serving HTTP requests")
|
2025-11-07 02:29:34 +00:00
|
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Str("err", err.Error()).Msg("HTTP Server Error")
|
2025-11-07 02:29:34 +00:00
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Wait for the interrupt signal to gracefully shut down
|
|
|
|
|
signalCh := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
|
<-signalCh
|
|
|
|
|
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Info().Msg("Received shutdown signal, shutting down...")
|
2025-11-07 02:29:34 +00:00
|
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
defer shutdownCancel()
|
|
|
|
|
|
|
|
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Error().Str("err", err.Error()).Msg("HTTP server shutdown error")
|
2025-11-07 02:29:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
|
|
waitGroup.Wait()
|
|
|
|
|
|
2025-11-13 20:11:00 +00:00
|
|
|
log.Info().Msg("Shutdown complete")
|
2025-11-03 12:22:06 +00:00
|
|
|
}
|
2025-11-13 15:15:35 +00:00
|
|
|
|
|
|
|
|
func IsProductionEnvironment() bool {
|
|
|
|
|
return Environment == "PRODUCTION"
|
|
|
|
|
}
|
2025-11-24 19:45:37 +00:00
|
|
|
|
|
|
|
|
func LoggerMiddleware(logger *zerolog.Logger) func(next http.Handler) http.Handler {
|
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
log := logger.With().Logger()
|
|
|
|
|
|
|
|
|
|
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
|
|
|
|
|
|
|
|
|
t1 := time.Now()
|
|
|
|
|
defer func() {
|
|
|
|
|
t2 := time.Now()
|
|
|
|
|
|
|
|
|
|
// Recover and record stack traces in case of a panic
|
|
|
|
|
if rec := recover(); rec != nil {
|
|
|
|
|
log.Error().
|
|
|
|
|
Str("type", "error").
|
|
|
|
|
Timestamp().
|
|
|
|
|
Interface("recover_info", rec).
|
|
|
|
|
Bytes("debug_stack", debug.Stack()).
|
|
|
|
|
Msg("log system error")
|
|
|
|
|
http.Error(ww, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
remote_addr := r.RemoteAddr
|
|
|
|
|
forwarded_for := r.Header.Get("X-Forwarded-For")
|
|
|
|
|
if forwarded_for != "" {
|
|
|
|
|
remote_addr = forwarded_for
|
|
|
|
|
}
|
|
|
|
|
// log end request
|
|
|
|
|
log.Info().
|
|
|
|
|
Str("type", "access").
|
|
|
|
|
Timestamp().
|
|
|
|
|
Fields(map[string]interface{}{
|
|
|
|
|
"remote_ip": remote_addr,
|
|
|
|
|
"url": r.URL.Path,
|
|
|
|
|
"proto": r.Proto,
|
|
|
|
|
"method": r.Method,
|
|
|
|
|
"user_agent": r.Header.Get("User-Agent"),
|
|
|
|
|
"status": ww.Status(),
|
|
|
|
|
"latency_ms": float64(t2.Sub(t1).Nanoseconds()) / 1000000.0,
|
|
|
|
|
"bytes_in": r.Header.Get("Content-Length"),
|
|
|
|
|
"bytes_out": ww.BytesWritten(),
|
|
|
|
|
}).
|
|
|
|
|
Msg("incoming_request")
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
next.ServeHTTP(ww, r)
|
|
|
|
|
}
|
|
|
|
|
return http.HandlerFunc(fn)
|
|
|
|
|
}
|
|
|
|
|
}
|