api.spaceplanner.app

Spaceplanner API
git clone git://jacobedwards.org/api.spaceplanner.app
Log | Files | Refs

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 }