main.go (5281B)
1 package main 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "flag" 7 "io" 8 "io/ioutil" 9 "log" 10 "log/slog" 11 "net/http" 12 "net/http/fcgi" 13 "os" 14 15 "jacobedwards.org/spaceplanner.app/internal/backend" 16 17 "github.com/gin-contrib/cors" 18 "github.com/gin-gonic/gin" 19 ) 20 21 /* 22 * WARNING: This is just a temporary measure. In the future 23 * this file will either be readable only by root (and have 24 * privledge dropping in this server) or outside the chroot 25 * (by figuring out a solution with kfcgi(8). 26 */ 27 const configFile = "/etc/spaceplanner.app/api.config" 28 29 func main() { 30 var config Config 31 var useStdin bool 32 var testing bool 33 var bindTo string 34 var logTo string 35 36 flag.BoolVar(&testing, "t", false, "Test mode") 37 flag.BoolVar(&useStdin, "s", false, "Read config from stdin") 38 flag.StringVar(&bindTo, "b", "", "Bind address and port") 39 flag.StringVar(&logTo, "l", "-", "Log file") 40 flag.Parse() 41 42 var logFile io.Writer 43 if logTo == "-" { 44 logFile = os.Stderr 45 } else { 46 var err error 47 logFile, err = os.Open(logTo) 48 if err != nil { 49 log.Fatalf("%s: Could not open log file: %s", logTo, err.Error()) 50 } 51 } 52 slog.SetDefault(slog.New(slog.NewJSONHandler(logFile, nil))) 53 54 var f io.Reader 55 if useStdin { 56 f = os.Stdin 57 } else { 58 var err error 59 f, err = os.Open(configFile) 60 if err != nil { 61 log.Fatalf("%s: Could not load configuration: %s", configFile, err.Error()) 62 } 63 } 64 if err := json.NewDecoder(f).Decode(&config); err != nil { 65 log.Fatalf("Unable to read config from stdin: %s", err.Error()) 66 } 67 68 e, err := NewEnv(config) 69 if err != nil { 70 log.Fatalf("Unable to initialize environment: %s", err.Error()) 71 } 72 defer e.Free() 73 74 e.backend.AddSetting("email", backend.SettingDef{ 75 Primitive: "string", 76 Validator: validateEmail, 77 Memo: "Email address", 78 }) 79 e.backend.AddSetting("email_strict", backend.SettingDef{ 80 Primitive: "bool", 81 DefaultValue: false, 82 Memo: "Whether to only send necessary emails", 83 }) 84 85 corsConfig := cors.DefaultConfig() 86 corsConfig.AllowAllOrigins = true 87 corsConfig.AddAllowHeaders("Authorization") 88 89 if !testing { 90 gin.SetMode(gin.ReleaseMode) 91 } 92 engine := gin.Default() 93 engine.NoRoute(noRoute) 94 engine.Use(RequestLoggerMiddleware()) 95 engine.Use(cors.New(corsConfig)) 96 engine.Use(AuthMiddleware(e.Auth)) 97 setRoutes(e, engine.Group("/v0")) 98 99 // Update subscriptions in another thread 100 go e.backend.StripeUpdateSubs() 101 102 if len(bindTo) > 0 { 103 engine.Run(bindTo) 104 } else { 105 fcgi.Serve(nil, engine) 106 } 107 } 108 109 func setRoutes(env *Env, r *gin.RouterGroup) { 110 // This endpoint uses a separate authorization scheme 111 // enforced within stripeEventHandler 112 r.POST("/stripe", env.StripeEventHandler) 113 114 r.POST("/tokens", env.Auth.LoginHandler) 115 r.GET("/tokens", env.Auth.RefreshHandler) 116 r.GET("/settings", env.GetSettings) 117 r.GET("/services", env.GetServices) 118 r.GET("/furniture", env.FurnitureTypes) 119 r.GET("/pointmaps", env.PointmapTypes) 120 121 users := r.Group("/users") 122 users.GET("/:user", env.GetUser) 123 users.POST("", env.CreateUser) 124 125 setAuthenticatedRoutes(env, r.Group("", env.Auth.MiddlewareFunc())) 126 } 127 128 func setAuthenticatedRoutes(env *Env, r *gin.RouterGroup) { 129 r.DELETE("/users/:user", env.DeleteUser) 130 131 user := r.Group("/users/:user/") 132 user.PATCH("/settings", env.UpdateUserSettings) 133 user.GET("/settings", env.GetUserSettings) 134 user.POST("/services/checkout", env.CreateCheckout) 135 user.POST("/services/billingportal", env.CreatePortal) 136 user.GET("/services/subscribed", env.UserService) 137 138 email := user.Group("/email") 139 email.GET("/code", env.SendUserEmailCode) 140 email.POST("/code", env.VerifyUserEmailCode) 141 email.GET("/verified", env.VerifiedUserEmail) 142 143 payed := r.Group("", env.VerifyPayment) 144 setPayedRoutes(env, payed) 145 } 146 147 func setPayedRoutes(env *Env, r *gin.RouterGroup) { 148 fp := r.Group("/floorplans/:user") 149 fp.GET("", env.GetFloorplans) 150 fp.POST("", env.CreateFloorplan) 151 152 fp = fp.Group("/:floorplan") 153 fp.PUT("", env.UpdateFloorplan) 154 fp.DELETE("", env.DeleteFloorplan) 155 fp.GET("", env.GetFloorplan) 156 157 fpdata := fp.Group("/data") 158 fpdata.GET("", env.GetFloorplanData) 159 fpdata.PUT("", env.ReplaceFloorplanData) 160 fpdata.PATCH("", env.PatchFloorplanData) 161 } 162 163 func (e *Env) VerifyPayment(c *gin.Context) { 164 service, err := e.backend.UserService(c.Param("user")) 165 if err != nil { 166 RespondError(c, 500, "Unable to get subscription status") 167 } else if service == nil { 168 RespondError(c, 401, "You must be subscribed to access this resource") 169 } else { 170 c.Set("service_id", service) 171 c.Next() 172 } 173 } 174 175 func noRoute(c *gin.Context) { 176 RespondError(c, http.StatusNotFound, "Endpoint does not exist") 177 } 178 179 func RequestLoggerMiddleware() gin.HandlerFunc { 180 return func(c *gin.Context) { 181 var buf bytes.Buffer 182 183 tee := io.TeeReader(c.Request.Body, &buf) 184 req, _ := ioutil.ReadAll(tee) 185 c.Request.Body = ioutil.NopCloser(&buf) 186 187 c.Next() 188 189 s := c.Writer.Status() 190 if s >= 200 && s < 300 { 191 slog.Info("Request", "endpoint", c.Request.URL.Path, slog.Group("response", slog.Int("code", s))) 192 return 193 } 194 195 log := slog.Warn 196 if s >= 500 && s < 600 { 197 log = slog.Error 198 } 199 200 resp, _ := c.Get("error") 201 if resp == nil { 202 resp = "" 203 } 204 log("Request", 205 slog.String("endpoint", c.Request.URL.Path), 206 slog.Group("response", 207 slog.Int("code", s), 208 slog.String("message", resp.(string)), 209 ), 210 slog.String("body", string(req)), 211 ) 212 } 213 }