sneak/integrate-di (#17)
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is failing
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	continuous-integration/drone/push Build is failing
				
			moving this to use uber/fx di framework instead of the ad hoc di setup before Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #17
This commit is contained in:
		
							parent
							
								
									0c3797ec30
								
							
						
					
					
						commit
						dd778174a7
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,3 +1,4 @@
 | 
			
		||||
/httpd
 | 
			
		||||
debug.log
 | 
			
		||||
/.env
 | 
			
		||||
/cmd/httpd/httpd
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,15 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/config"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/database"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/globals"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/handlers"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/healthcheck"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/logger"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/middleware"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/server"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@ -13,5 +19,22 @@ var (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	os.Exit(server.Run(Appname, Version, Buildarch))
 | 
			
		||||
	globals.Appname = Appname
 | 
			
		||||
	globals.Version = Version
 | 
			
		||||
	globals.Buildarch = Buildarch
 | 
			
		||||
 | 
			
		||||
	fx.New(
 | 
			
		||||
		fx.Provide(
 | 
			
		||||
			config.New,
 | 
			
		||||
			database.New,
 | 
			
		||||
			globals.New,
 | 
			
		||||
			handlers.New,
 | 
			
		||||
			logger.New,
 | 
			
		||||
			server.New,
 | 
			
		||||
			middleware.New,
 | 
			
		||||
			healthcheck.New,
 | 
			
		||||
		),
 | 
			
		||||
		fx.Invoke(func(*server.Server) {}),
 | 
			
		||||
	).Run()
 | 
			
		||||
	// os.Exit(server.Run(Appname, Version, Buildarch))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							@ -12,6 +12,8 @@ require (
 | 
			
		||||
	github.com/rs/zerolog v1.28.0
 | 
			
		||||
	github.com/slok/go-http-metrics v0.10.0
 | 
			
		||||
	github.com/spf13/viper v1.14.0
 | 
			
		||||
	go.uber.org/fx v1.18.2
 | 
			
		||||
	google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
@ -35,8 +37,14 @@ require (
 | 
			
		||||
	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 | 
			
		||||
	github.com/spf13/pflag v1.0.5 // indirect
 | 
			
		||||
	github.com/subosito/gotenv v1.4.1 // indirect
 | 
			
		||||
	go.uber.org/atomic v1.9.0 // indirect
 | 
			
		||||
	go.uber.org/dig v1.15.0 // indirect
 | 
			
		||||
	go.uber.org/multierr v1.8.0 // indirect
 | 
			
		||||
	go.uber.org/zap v1.21.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect
 | 
			
		||||
	golang.org/x/sys v0.2.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.4.0 // indirect
 | 
			
		||||
	google.golang.org/grpc v1.50.1 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.28.1 // indirect
 | 
			
		||||
	gopkg.in/ini.v1 v1.67.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v2 v2.4.0 // indirect
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								go.sum
									
									
									
									
									
								
							@ -45,6 +45,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
 | 
			
		||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 | 
			
		||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 | 
			
		||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 | 
			
		||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 | 
			
		||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 | 
			
		||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 | 
			
		||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 | 
			
		||||
@ -279,12 +280,26 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 | 
			
		||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
			
		||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
			
		||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
			
		||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 | 
			
		||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 | 
			
		||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 | 
			
		||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 | 
			
		||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 | 
			
		||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 | 
			
		||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 | 
			
		||||
go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE=
 | 
			
		||||
go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM=
 | 
			
		||||
go.uber.org/fx v1.18.2 h1:bUNI6oShr+OVFQeU8cDNbnN7VFsu+SsjHzUF51V/GAU=
 | 
			
		||||
go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY=
 | 
			
		||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
 | 
			
		||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 | 
			
		||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
 | 
			
		||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
 | 
			
		||||
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
 | 
			
		||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
@ -326,6 +341,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
@ -359,10 +375,12 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
 | 
			
		||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 | 
			
		||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 | 
			
		||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 | 
			
		||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 | 
			
		||||
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
 | 
			
		||||
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
@ -384,6 +402,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 | 
			
		||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
@ -423,8 +442,10 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
			
		||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
@ -497,6 +518,7 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
 | 
			
		||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 | 
			
		||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
@ -563,6 +585,8 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
 | 
			
		||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e h1:S9GbmC1iCgvbLyAokVCwiO6tVIrU9Y7c5oMx1V/ki/Y=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
 | 
			
		||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 | 
			
		||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 | 
			
		||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 | 
			
		||||
@ -579,6 +603,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
 | 
			
		||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 | 
			
		||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
 | 
			
		||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 | 
			
		||||
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
 | 
			
		||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 | 
			
		||||
@ -605,10 +631,12 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 | 
			
		||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										90
									
								
								internal/config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								internal/config/config.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/globals"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/logger"
 | 
			
		||||
	"github.com/rs/zerolog"
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
 | 
			
		||||
	// spooky action at a distance!
 | 
			
		||||
	// this populates the environment
 | 
			
		||||
	// from a ./.env file automatically
 | 
			
		||||
	// for development configuration.
 | 
			
		||||
	// .env contents should be things like
 | 
			
		||||
	// `DBURL=postgres://user:pass@.../`
 | 
			
		||||
	// (without the backticks, of course)
 | 
			
		||||
	_ "github.com/joho/godotenv/autoload"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ConfigParams struct {
 | 
			
		||||
	fx.In
 | 
			
		||||
	Globals *globals.Globals
 | 
			
		||||
	Logger  *logger.Logger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	DBURL           string
 | 
			
		||||
	Debug           bool
 | 
			
		||||
	MaintenanceMode bool
 | 
			
		||||
	MetricsPassword string
 | 
			
		||||
	MetricsUsername string
 | 
			
		||||
	Port            int
 | 
			
		||||
	SentryDSN       string
 | 
			
		||||
	params          *ConfigParams
 | 
			
		||||
	log             *zerolog.Logger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
 | 
			
		||||
	log := params.Logger.Get()
 | 
			
		||||
	name := params.Globals.Appname
 | 
			
		||||
 | 
			
		||||
	viper.SetConfigName(name)
 | 
			
		||||
	viper.SetConfigType("yaml")
 | 
			
		||||
	// path to look for the config file in:
 | 
			
		||||
	viper.AddConfigPath(fmt.Sprintf("/etc/%s", name))
 | 
			
		||||
	// call multiple times to add many search paths:
 | 
			
		||||
	viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", name))
 | 
			
		||||
	// viper.SetEnvPrefix(strings.ToUpper(s.appname))
 | 
			
		||||
	viper.AutomaticEnv()
 | 
			
		||||
 | 
			
		||||
	viper.SetDefault("DEBUG", "false")
 | 
			
		||||
	viper.SetDefault("MAINTENANCE_MODE", "false")
 | 
			
		||||
	viper.SetDefault("PORT", "8080")
 | 
			
		||||
	viper.SetDefault("DBURL", "")
 | 
			
		||||
	viper.SetDefault("SENTRY_DSN", "")
 | 
			
		||||
	viper.SetDefault("METRICS_USERNAME", "")
 | 
			
		||||
	viper.SetDefault("METRICS_PASSWORD", "")
 | 
			
		||||
 | 
			
		||||
	if err := viper.ReadInConfig(); err != nil {
 | 
			
		||||
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
 | 
			
		||||
			// Config file not found; ignore error if desired
 | 
			
		||||
		} else {
 | 
			
		||||
			// Config file was found but another error was produced
 | 
			
		||||
			log.Panic().
 | 
			
		||||
				Err(err).
 | 
			
		||||
				Msg("config file malformed")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s := &Config{
 | 
			
		||||
		DBURL:           viper.GetString("DBURL"),
 | 
			
		||||
		Debug:           viper.GetBool("debug"),
 | 
			
		||||
		Port:            viper.GetInt("PORT"),
 | 
			
		||||
		SentryDSN:       viper.GetString("SENTRY_DSN"),
 | 
			
		||||
		MaintenanceMode: viper.GetBool("MAINTENANCE_MODE"),
 | 
			
		||||
		MetricsUsername: viper.GetString("METRICS_USERNAME"),
 | 
			
		||||
		MetricsPassword: viper.GetString("METRICS_PASSWORD"),
 | 
			
		||||
		log:             log,
 | 
			
		||||
		params:          ¶ms,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.Debug {
 | 
			
		||||
		params.Logger.EnableDebugLogging()
 | 
			
		||||
		s.log = params.Logger.Get()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								internal/database/database.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								internal/database/database.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
package database
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/config"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/logger"
 | 
			
		||||
	"github.com/rs/zerolog"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
 | 
			
		||||
	// spooky action at a distance!
 | 
			
		||||
	// this populates the environment
 | 
			
		||||
	// from a ./.env file automatically
 | 
			
		||||
	// for development configuration.
 | 
			
		||||
	// .env contents should be things like
 | 
			
		||||
	// `DBURL=postgres://user:pass@.../`
 | 
			
		||||
	// (without the backticks, of course)
 | 
			
		||||
	_ "github.com/joho/godotenv/autoload"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DatabaseParams struct {
 | 
			
		||||
	fx.In
 | 
			
		||||
	Logger *logger.Logger
 | 
			
		||||
	Config *config.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Database struct {
 | 
			
		||||
	URL    string
 | 
			
		||||
	log    *zerolog.Logger
 | 
			
		||||
	params *DatabaseParams
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
 | 
			
		||||
	s := new(Database)
 | 
			
		||||
	s.params = ¶ms
 | 
			
		||||
	s.log = params.Logger.Get()
 | 
			
		||||
 | 
			
		||||
	s.log.Info().Msg("Database instantiated")
 | 
			
		||||
 | 
			
		||||
	lc.Append(fx.Hook{
 | 
			
		||||
		OnStart: func(ctx context.Context) error {
 | 
			
		||||
			s.log.Info().Msg("Database OnStart Hook")
 | 
			
		||||
			// FIXME connect to db
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		OnStop: func(ctx context.Context) error {
 | 
			
		||||
			// FIXME disconnect from db
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								internal/globals/globals.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								internal/globals/globals.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
package globals
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// these get populated from main() and copied into the Globals object.
 | 
			
		||||
var (
 | 
			
		||||
	Appname   string
 | 
			
		||||
	Version   string
 | 
			
		||||
	Buildarch string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Globals struct {
 | 
			
		||||
	Appname   string
 | 
			
		||||
	Version   string
 | 
			
		||||
	Buildarch string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(lc fx.Lifecycle) (*Globals, error) {
 | 
			
		||||
	n := &Globals{
 | 
			
		||||
		Appname:   Appname,
 | 
			
		||||
		Buildarch: Buildarch,
 | 
			
		||||
		Version:   Version,
 | 
			
		||||
	}
 | 
			
		||||
	return n, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								internal/handlers/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								internal/handlers/handlers.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/database"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/globals"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/healthcheck"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/logger"
 | 
			
		||||
	"github.com/rs/zerolog"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type HandlersParams struct {
 | 
			
		||||
	fx.In
 | 
			
		||||
	Logger      *logger.Logger
 | 
			
		||||
	Globals     *globals.Globals
 | 
			
		||||
	Database    *database.Database
 | 
			
		||||
	Healthcheck *healthcheck.Healthcheck
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Handlers struct {
 | 
			
		||||
	params *HandlersParams
 | 
			
		||||
	log    *zerolog.Logger
 | 
			
		||||
	hc     *healthcheck.Healthcheck
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
 | 
			
		||||
	s := new(Handlers)
 | 
			
		||||
	s.params = ¶ms
 | 
			
		||||
	s.log = params.Logger.Get()
 | 
			
		||||
	s.hc = params.Healthcheck
 | 
			
		||||
	lc.Append(fx.Hook{
 | 
			
		||||
		OnStart: func(ctx context.Context) error {
 | 
			
		||||
			// FIXME compile some templates here or something
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Handlers) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}, status int) {
 | 
			
		||||
	w.WriteHeader(status)
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	if data != nil {
 | 
			
		||||
		err := json.NewEncoder(w).Encode(data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			s.log.Error().Err(err).Msg("json encode error")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Handlers) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { // nolint
 | 
			
		||||
	return json.NewDecoder(r.Body).Decode(v)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								internal/handlers/healthcheck.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/handlers/healthcheck.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *Handlers) HandleHealthCheck() http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
		resp := s.hc.Healthcheck()
 | 
			
		||||
		s.respondJSON(w, req, resp, 200)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								internal/handlers/index.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								internal/handlers/index.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/templates"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *Handlers) HandleIndex() http.HandlerFunc {
 | 
			
		||||
	t := templates.GetParsed()
 | 
			
		||||
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		err := t.ExecuteTemplate(w, "index.html", nil)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			s.log.Error().Err(err).Msg("")
 | 
			
		||||
			http.Error(w, http.StatusText(500), 500)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								internal/handlers/login.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								internal/handlers/login.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/templates"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *Handlers) HandleLoginGET() http.HandlerFunc {
 | 
			
		||||
	t := templates.GetParsed()
 | 
			
		||||
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		err := t.ExecuteTemplate(w, "login.html", nil)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			s.log.Error().Err(err).Msg("")
 | 
			
		||||
			http.Error(w, http.StatusText(500), 500)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
package server
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *server) handleNow() http.HandlerFunc {
 | 
			
		||||
func (s *Handlers) HandleNow() http.HandlerFunc {
 | 
			
		||||
	type response struct {
 | 
			
		||||
		Now time.Time `json:"now"`
 | 
			
		||||
	}
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
package server
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
@ -7,7 +7,7 @@ import (
 | 
			
		||||
// CHANGEME you probably want to remove this,
 | 
			
		||||
// this is just a handler/route that throws a panic to test
 | 
			
		||||
// sentry events.
 | 
			
		||||
func (s *server) handlePanic() http.HandlerFunc {
 | 
			
		||||
func (s *Handlers) HandlePanic() http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		panic("y tho")
 | 
			
		||||
	}
 | 
			
		||||
							
								
								
									
										71
									
								
								internal/healthcheck/healthcheck.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								internal/healthcheck/healthcheck.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
package healthcheck
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/config"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/database"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/globals"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/logger"
 | 
			
		||||
	"github.com/rs/zerolog"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type HealthcheckParams struct {
 | 
			
		||||
	fx.In
 | 
			
		||||
	Globals  *globals.Globals
 | 
			
		||||
	Config   *config.Config
 | 
			
		||||
	Logger   *logger.Logger
 | 
			
		||||
	Database *database.Database
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Healthcheck struct {
 | 
			
		||||
	StartupTime time.Time
 | 
			
		||||
	log         *zerolog.Logger
 | 
			
		||||
	params      *HealthcheckParams
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(lc fx.Lifecycle, params HealthcheckParams) (*Healthcheck, error) {
 | 
			
		||||
	s := new(Healthcheck)
 | 
			
		||||
	s.params = ¶ms
 | 
			
		||||
	s.log = params.Logger.Get()
 | 
			
		||||
 | 
			
		||||
	lc.Append(fx.Hook{
 | 
			
		||||
		OnStart: func(ctx context.Context) error {
 | 
			
		||||
			s.StartupTime = time.Now()
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		OnStop: func(ctx context.Context) error {
 | 
			
		||||
			// FIXME do server shutdown here
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HealthcheckResponse struct {
 | 
			
		||||
	Status        string `json:"status"`
 | 
			
		||||
	Now           string `json:"now"`
 | 
			
		||||
	UptimeSeconds int64  `json:"uptime_seconds"`
 | 
			
		||||
	UptimeHuman   string `json:"uptime_human"`
 | 
			
		||||
	Version       string `json:"version"`
 | 
			
		||||
	Appname       string `json:"appname"`
 | 
			
		||||
	Maintenance   bool   `json:"maintenance_mode"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Healthcheck) uptime() time.Duration {
 | 
			
		||||
	return time.Since(s.StartupTime)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Healthcheck) Healthcheck() *HealthcheckResponse {
 | 
			
		||||
	resp := &HealthcheckResponse{
 | 
			
		||||
		Status:        "ok",
 | 
			
		||||
		Now:           time.Now().UTC().Format(time.RFC3339Nano),
 | 
			
		||||
		UptimeSeconds: int64(s.uptime().Seconds()),
 | 
			
		||||
		UptimeHuman:   s.uptime().String(),
 | 
			
		||||
		Appname:       s.params.Globals.Appname,
 | 
			
		||||
		Version:       s.params.Globals.Version,
 | 
			
		||||
	}
 | 
			
		||||
	return resp
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										97
									
								
								internal/logger/logger.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								internal/logger/logger.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,97 @@
 | 
			
		||||
package logger
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/globals"
 | 
			
		||||
	"github.com/rs/zerolog"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LoggerParams struct {
 | 
			
		||||
	fx.In
 | 
			
		||||
	Globals *globals.Globals
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Logger struct {
 | 
			
		||||
	log    *zerolog.Logger
 | 
			
		||||
	params LoggerParams
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(lc fx.Lifecycle, params LoggerParams) (*Logger, error) {
 | 
			
		||||
	l := new(Logger)
 | 
			
		||||
 | 
			
		||||
	// always log in UTC
 | 
			
		||||
	zerolog.TimestampFunc = func() time.Time {
 | 
			
		||||
		return time.Now().UTC()
 | 
			
		||||
	}
 | 
			
		||||
	zerolog.SetGlobalLevel(zerolog.InfoLevel)
 | 
			
		||||
 | 
			
		||||
	tty := false
 | 
			
		||||
	if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
 | 
			
		||||
		tty = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var writers []io.Writer
 | 
			
		||||
 | 
			
		||||
	if tty {
 | 
			
		||||
		// this does cool colorization for console/dev
 | 
			
		||||
		consoleWriter := zerolog.NewConsoleWriter(
 | 
			
		||||
			func(w *zerolog.ConsoleWriter) {
 | 
			
		||||
				// Customize time format
 | 
			
		||||
				w.TimeFormat = time.RFC3339Nano
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		writers = append(writers, consoleWriter)
 | 
			
		||||
	} else {
 | 
			
		||||
		// log json in prod for the machines
 | 
			
		||||
		writers = append(writers, os.Stdout)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		// this is how you log to a file, if you do that
 | 
			
		||||
		// sort of thing still
 | 
			
		||||
		logfile := viper.GetString("Logfile")
 | 
			
		||||
		if logfile != "" {
 | 
			
		||||
		    logfileDir := filepath.Dir(logfile)
 | 
			
		||||
		    err := goutil.Mkdirp(logfileDir)
 | 
			
		||||
		    if err != nil {
 | 
			
		||||
			    log.Error().Err(err).Msg("unable to create log dir")
 | 
			
		||||
		    }
 | 
			
		||||
 | 
			
		||||
		    hp.logfh, err = os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
 | 
			
		||||
		    if err != nil {
 | 
			
		||||
			    panic("unable to open logfile: " + err.Error())
 | 
			
		||||
		    }
 | 
			
		||||
 | 
			
		||||
		    writers = append(writers, hp.logfh)
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	multi := zerolog.MultiLevelWriter(writers...)
 | 
			
		||||
	logger := zerolog.New(multi).With().Timestamp().Logger().With().Caller().Logger()
 | 
			
		||||
 | 
			
		||||
	l.log = &logger
 | 
			
		||||
	// log.Logger = logger
 | 
			
		||||
 | 
			
		||||
	return l, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l *Logger) EnableDebugLogging() {
 | 
			
		||||
	zerolog.SetGlobalLevel(zerolog.DebugLevel)
 | 
			
		||||
	l.log.Debug().Bool("debug", true).Send()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l *Logger) Get() *zerolog.Logger {
 | 
			
		||||
	return l.log
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l *Logger) Identify() {
 | 
			
		||||
	l.log.Info().
 | 
			
		||||
		Str("appname", l.params.Globals.Appname).
 | 
			
		||||
		Str("version", l.params.Globals.Version).
 | 
			
		||||
		Str("buildarch", l.params.Globals.Buildarch).
 | 
			
		||||
		Msg("starting")
 | 
			
		||||
}
 | 
			
		||||
@ -1,19 +1,43 @@
 | 
			
		||||
package server
 | 
			
		||||
package middleware
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/config"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/globals"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/logger"
 | 
			
		||||
	basicauth "github.com/99designs/basicauth-go"
 | 
			
		||||
	"github.com/go-chi/chi/middleware"
 | 
			
		||||
	"github.com/go-chi/cors"
 | 
			
		||||
	"github.com/rs/zerolog"
 | 
			
		||||
	metrics "github.com/slok/go-http-metrics/metrics/prometheus"
 | 
			
		||||
	ghmm "github.com/slok/go-http-metrics/middleware"
 | 
			
		||||
	"github.com/slok/go-http-metrics/middleware/std"
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MiddlewareParams struct {
 | 
			
		||||
	fx.In
 | 
			
		||||
	Logger  *logger.Logger
 | 
			
		||||
	Globals *globals.Globals
 | 
			
		||||
	Config  *config.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Middleware struct {
 | 
			
		||||
	log    *zerolog.Logger
 | 
			
		||||
	params *MiddlewareParams
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(lc fx.Lifecycle, params MiddlewareParams) (*Middleware, error) {
 | 
			
		||||
	s := new(Middleware)
 | 
			
		||||
	s.params = ¶ms
 | 
			
		||||
	s.log = params.Logger.Get()
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// the following is from
 | 
			
		||||
// https://learning-cloud-native-go.github.io/docs/a6.adding_zerolog_logger/
 | 
			
		||||
 | 
			
		||||
@ -45,7 +69,7 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
 | 
			
		||||
// type Middleware func(http.Handler) http.Handler
 | 
			
		||||
// this returns a Middleware that is designed to do every request through the
 | 
			
		||||
// mux, note the signature:
 | 
			
		||||
func (s *server) LoggingMiddleware() func(http.Handler) http.Handler {
 | 
			
		||||
func (s *Middleware) Logging() func(http.Handler) http.Handler {
 | 
			
		||||
	// FIXME this should use https://github.com/google/go-cloud/blob/master/server/requestlog/requestlog.go
 | 
			
		||||
	return func(next http.Handler) http.Handler {
 | 
			
		||||
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
@ -73,7 +97,7 @@ func (s *server) LoggingMiddleware() func(http.Handler) http.Handler {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) CORSMiddleware() func(http.Handler) http.Handler {
 | 
			
		||||
func (s *Middleware) CORS() func(http.Handler) http.Handler {
 | 
			
		||||
	return cors.Handler(cors.Options{
 | 
			
		||||
		// CHANGEME! these are defaults, change them to suit your needs or
 | 
			
		||||
		// read from environment/viper.
 | 
			
		||||
@ -88,7 +112,7 @@ func (s *server) CORSMiddleware() func(http.Handler) http.Handler {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) AuthMiddleware() func(http.Handler) http.Handler {
 | 
			
		||||
func (s *Middleware) Auth() func(http.Handler) http.Handler {
 | 
			
		||||
	return func(next http.Handler) http.Handler {
 | 
			
		||||
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			// CHANGEME you'll want to change this to do stuff.
 | 
			
		||||
@ -98,7 +122,7 @@ func (s *server) AuthMiddleware() func(http.Handler) http.Handler {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) MetricsMiddleware() func(http.Handler) http.Handler {
 | 
			
		||||
func (s *Middleware) Metrics() func(http.Handler) http.Handler {
 | 
			
		||||
	mdlw := ghmm.New(ghmm.Config{
 | 
			
		||||
		Recorder: metrics.NewRecorder(metrics.Config{}),
 | 
			
		||||
	})
 | 
			
		||||
@ -107,7 +131,7 @@ func (s *server) MetricsMiddleware() func(http.Handler) http.Handler {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) MetricsAuthMiddleware() func(http.Handler) http.Handler {
 | 
			
		||||
func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler {
 | 
			
		||||
	return basicauth.New(
 | 
			
		||||
		"metrics",
 | 
			
		||||
		map[string][]string{
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *server) handleHealthCheck() http.HandlerFunc {
 | 
			
		||||
	type response struct {
 | 
			
		||||
		Status        string `json:"status"`
 | 
			
		||||
		Now           string `json:"now"`
 | 
			
		||||
		UptimeSeconds int64  `json:"uptime_seconds"`
 | 
			
		||||
		UptimeHuman   string `json:"uptime_human"`
 | 
			
		||||
		Version       string `json:"version"`
 | 
			
		||||
		Appname       string `json:"appname"`
 | 
			
		||||
		Maintenance   bool   `json:"maintenance_mode"`
 | 
			
		||||
	}
 | 
			
		||||
	return func(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
		resp := &response{
 | 
			
		||||
			Status:        "ok",
 | 
			
		||||
			Now:           time.Now().UTC().Format(time.RFC3339Nano),
 | 
			
		||||
			UptimeSeconds: int64(s.uptime().Seconds()),
 | 
			
		||||
			UptimeHuman:   s.uptime().String(),
 | 
			
		||||
			Maintenance:   s.maintenance(),
 | 
			
		||||
			Appname:       s.appname,
 | 
			
		||||
			Version:       s.version,
 | 
			
		||||
		}
 | 
			
		||||
		s.respondJSON(w, req, resp, 200)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/templates"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *server) handleIndex() http.HandlerFunc {
 | 
			
		||||
	indexTemplate := template.Must(template.New("index").Parse(templates.MustString("index.html")))
 | 
			
		||||
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		err := indexTemplate.ExecuteTemplate(w, "index", nil)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Println(err.Error())
 | 
			
		||||
			http.Error(w, http.StatusText(500), 500)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *server) handleLogin() http.HandlerFunc {
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		fmt.Fprintf(w, "hello login")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,14 +1,13 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *server) serveUntilShutdown() {
 | 
			
		||||
	listenAddr := fmt.Sprintf(":%d", s.port)
 | 
			
		||||
func (s *Server) serveUntilShutdown() {
 | 
			
		||||
	listenAddr := fmt.Sprintf(":%d", s.params.Config.Port)
 | 
			
		||||
	s.httpServer = &http.Server{
 | 
			
		||||
		Addr:           listenAddr,
 | 
			
		||||
		ReadTimeout:    10 * time.Second,
 | 
			
		||||
@ -19,7 +18,7 @@ func (s *server) serveUntilShutdown() {
 | 
			
		||||
 | 
			
		||||
	// add routes
 | 
			
		||||
	// this does any necessary setup in each handler
 | 
			
		||||
	s.routes()
 | 
			
		||||
	s.SetupRoutes()
 | 
			
		||||
 | 
			
		||||
	s.log.Info().Str("listenaddr", listenAddr).Msg("http begin listen")
 | 
			
		||||
	if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
 | 
			
		||||
@ -30,21 +29,6 @@ func (s *server) serveUntilShutdown() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}, status int) {
 | 
			
		||||
	w.WriteHeader(status)
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	if data != nil {
 | 
			
		||||
		err := json.NewEncoder(w).Encode(data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			s.log.Error().Err(err).Msg("json encode error")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { // nolint
 | 
			
		||||
	return json.NewDecoder(r.Body).Decode(v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	s.router.ServeHTTP(w, r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,269 +0,0 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/rs/zerolog"
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
 | 
			
		||||
	"github.com/getsentry/sentry-go"
 | 
			
		||||
	"github.com/go-chi/chi"
 | 
			
		||||
 | 
			
		||||
	// spooky action at a distance!
 | 
			
		||||
	// this populates the environment
 | 
			
		||||
	// from a ./.env file automatically
 | 
			
		||||
	// for development configuration.
 | 
			
		||||
	// .env contents should be things like
 | 
			
		||||
	// `DBURL=postgres://user:pass@.../`
 | 
			
		||||
	// (without the backticks, of course)
 | 
			
		||||
	_ "github.com/joho/godotenv/autoload"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type server struct {
 | 
			
		||||
	appname       string
 | 
			
		||||
	version       string
 | 
			
		||||
	buildarch     string
 | 
			
		||||
	databaseURL   string
 | 
			
		||||
	startupTime   time.Time
 | 
			
		||||
	port          int
 | 
			
		||||
	exitCode      int
 | 
			
		||||
	sentryEnabled bool
 | 
			
		||||
	log           *zerolog.Logger
 | 
			
		||||
	ctx           context.Context
 | 
			
		||||
	cancelFunc    context.CancelFunc
 | 
			
		||||
	httpServer    *http.Server
 | 
			
		||||
	router        *chi.Mux
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newServer(options ...func(s *server)) *server {
 | 
			
		||||
	n := new(server)
 | 
			
		||||
	n.startupTime = time.Now()
 | 
			
		||||
	n.version = "unknown"
 | 
			
		||||
	for _, opt := range options {
 | 
			
		||||
		opt(n)
 | 
			
		||||
	}
 | 
			
		||||
	return n
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FIXME change this to use uber/fx DI and an Invoke()
 | 
			
		||||
// this is where we come in from package main.
 | 
			
		||||
func Run(appname, version, buildarch string) int {
 | 
			
		||||
	s := newServer(func(i *server) {
 | 
			
		||||
		i.appname = appname
 | 
			
		||||
		if version != "" {
 | 
			
		||||
			i.version = version
 | 
			
		||||
		}
 | 
			
		||||
		i.buildarch = buildarch
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// this does nothing if SENTRY_DSN is unset in env.
 | 
			
		||||
 | 
			
		||||
	// TODO remove:
 | 
			
		||||
	if s.sentryEnabled {
 | 
			
		||||
		sentry.CaptureMessage("It works!")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.configure()
 | 
			
		||||
	s.setupLogging()
 | 
			
		||||
 | 
			
		||||
	// logging before sentry, because sentry logs
 | 
			
		||||
	s.enableSentry()
 | 
			
		||||
 | 
			
		||||
	s.databaseURL = viper.GetString("DBURL")
 | 
			
		||||
	s.port = viper.GetInt("PORT")
 | 
			
		||||
 | 
			
		||||
	return s.serve()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) enableSentry() {
 | 
			
		||||
	s.sentryEnabled = false
 | 
			
		||||
 | 
			
		||||
	if viper.GetString("SENTRY_DSN") == "" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := sentry.Init(sentry.ClientOptions{
 | 
			
		||||
		Dsn:     viper.GetString("SENTRY_DSN"),
 | 
			
		||||
		Release: fmt.Sprintf("%s-%s", s.appname, s.version),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal().Err(err).Msg("sentry init failure")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	s.log.Info().Msg("sentry error reporting activated")
 | 
			
		||||
	s.sentryEnabled = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) serve() int {
 | 
			
		||||
	s.ctx, s.cancelFunc = context.WithCancel(context.Background())
 | 
			
		||||
 | 
			
		||||
	// signal watcher
 | 
			
		||||
	go func() {
 | 
			
		||||
		c := make(chan os.Signal, 1)
 | 
			
		||||
		signal.Ignore(syscall.SIGPIPE)
 | 
			
		||||
		signal.Notify(c, os.Interrupt, syscall.SIGTERM)
 | 
			
		||||
		// block and wait for signal
 | 
			
		||||
		sig := <-c
 | 
			
		||||
		s.log.Info().Msgf("signal received: %+v", sig)
 | 
			
		||||
		if s.cancelFunc != nil {
 | 
			
		||||
			// cancelling the main context will trigger a clean
 | 
			
		||||
			// shutdown.
 | 
			
		||||
			s.cancelFunc()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	go s.serveUntilShutdown()
 | 
			
		||||
 | 
			
		||||
	for range s.ctx.Done() {
 | 
			
		||||
		// aforementioned clean shutdown upon main context
 | 
			
		||||
		// cancellation
 | 
			
		||||
	}
 | 
			
		||||
	s.cleanShutdown()
 | 
			
		||||
	return s.exitCode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) cleanupForExit() {
 | 
			
		||||
	s.log.Info().Msg("cleaning up")
 | 
			
		||||
	// FIXME unimplemented
 | 
			
		||||
	// close database connections or whatever
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) cleanShutdown() {
 | 
			
		||||
	// initiate clean shutdown
 | 
			
		||||
	s.exitCode = 0
 | 
			
		||||
	ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
			
		||||
	if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
 | 
			
		||||
		s.log.Error().
 | 
			
		||||
			Err(err).
 | 
			
		||||
			Msg("server clean shutdown failed")
 | 
			
		||||
	}
 | 
			
		||||
	if shutdownCancel != nil {
 | 
			
		||||
		shutdownCancel()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.cleanupForExit()
 | 
			
		||||
 | 
			
		||||
	if s.sentryEnabled {
 | 
			
		||||
		sentry.Flush(2 * time.Second)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) uptime() time.Duration {
 | 
			
		||||
	return time.Since(s.startupTime)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) maintenance() bool {
 | 
			
		||||
	return viper.GetBool("MAINTENANCE_MODE")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) configure() {
 | 
			
		||||
	viper.SetConfigName(s.appname)
 | 
			
		||||
	viper.SetConfigType("yaml")
 | 
			
		||||
	// path to look for the config file in:
 | 
			
		||||
	viper.AddConfigPath(fmt.Sprintf("/etc/%s", s.appname))
 | 
			
		||||
	// call multiple times to add many search paths:
 | 
			
		||||
	viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", s.appname))
 | 
			
		||||
	// viper.SetEnvPrefix(strings.ToUpper(s.appname))
 | 
			
		||||
	viper.AutomaticEnv()
 | 
			
		||||
 | 
			
		||||
	viper.SetDefault("DEBUG", "false")
 | 
			
		||||
	viper.SetDefault("MAINTENANCE_MODE", "false")
 | 
			
		||||
	viper.SetDefault("PORT", "8080")
 | 
			
		||||
	viper.SetDefault("DBURL", "")
 | 
			
		||||
	viper.SetDefault("SENTRY_DSN", "")
 | 
			
		||||
	viper.SetDefault("METRICS_USERNAME", "")
 | 
			
		||||
	viper.SetDefault("METRICS_PASSWORD", "")
 | 
			
		||||
 | 
			
		||||
	if err := viper.ReadInConfig(); err != nil {
 | 
			
		||||
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
 | 
			
		||||
			// Config file not found; ignore error if desired
 | 
			
		||||
		} else {
 | 
			
		||||
			// Config file was found but another error was produced
 | 
			
		||||
			log.Panic().
 | 
			
		||||
				Err(err).
 | 
			
		||||
				Msg("config file malformed")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if viper.GetBool("DEBUG") {
 | 
			
		||||
	//      pp.Print(viper.AllSettings())
 | 
			
		||||
	// }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) setupLogging() {
 | 
			
		||||
	// always log in UTC
 | 
			
		||||
	zerolog.TimestampFunc = func() time.Time {
 | 
			
		||||
		return time.Now().UTC()
 | 
			
		||||
	}
 | 
			
		||||
	zerolog.SetGlobalLevel(zerolog.InfoLevel)
 | 
			
		||||
 | 
			
		||||
	tty := false
 | 
			
		||||
	if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
 | 
			
		||||
		tty = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var writers []io.Writer
 | 
			
		||||
 | 
			
		||||
	if tty {
 | 
			
		||||
		// this does cool colorization for console/dev
 | 
			
		||||
		consoleWriter := zerolog.NewConsoleWriter(
 | 
			
		||||
			func(w *zerolog.ConsoleWriter) {
 | 
			
		||||
				// Customize time format
 | 
			
		||||
				w.TimeFormat = time.RFC3339Nano
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		writers = append(writers, consoleWriter)
 | 
			
		||||
	} else {
 | 
			
		||||
		// log json in prod for the machines
 | 
			
		||||
		writers = append(writers, os.Stdout)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		// this is how you log to a file, if you do that
 | 
			
		||||
		// sort of thing still
 | 
			
		||||
		logfile := viper.GetString("Logfile")
 | 
			
		||||
		if logfile != "" {
 | 
			
		||||
		    logfileDir := filepath.Dir(logfile)
 | 
			
		||||
		    err := goutil.Mkdirp(logfileDir)
 | 
			
		||||
		    if err != nil {
 | 
			
		||||
			    log.Error().Err(err).Msg("unable to create log dir")
 | 
			
		||||
		    }
 | 
			
		||||
 | 
			
		||||
		    hp.logfh, err = os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
 | 
			
		||||
		    if err != nil {
 | 
			
		||||
			    panic("unable to open logfile: " + err.Error())
 | 
			
		||||
		    }
 | 
			
		||||
 | 
			
		||||
		    writers = append(writers, hp.logfh)
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	multi := zerolog.MultiLevelWriter(writers...)
 | 
			
		||||
	logger := zerolog.New(multi).With().Timestamp().Logger().With().Caller().Logger()
 | 
			
		||||
 | 
			
		||||
	s.log = &logger
 | 
			
		||||
	// log.Logger = logger
 | 
			
		||||
 | 
			
		||||
	if viper.GetBool("debug") {
 | 
			
		||||
		zerolog.SetGlobalLevel(zerolog.DebugLevel)
 | 
			
		||||
		s.log.Debug().Bool("debug", true).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.identify()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *server) identify() {
 | 
			
		||||
	s.log.Info().
 | 
			
		||||
		Str("appname", s.appname).
 | 
			
		||||
		Str("version", s.version).
 | 
			
		||||
		Str("buildarch", s.buildarch).
 | 
			
		||||
		Msg("starting")
 | 
			
		||||
}
 | 
			
		||||
@ -12,7 +12,7 @@ import (
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *server) routes() {
 | 
			
		||||
func (s *Server) SetupRoutes() {
 | 
			
		||||
	s.router = chi.NewRouter()
 | 
			
		||||
 | 
			
		||||
	// the mux .Use() takes a http.Handler wrapper func, like most
 | 
			
		||||
@ -23,16 +23,16 @@ func (s *server) routes() {
 | 
			
		||||
 | 
			
		||||
	s.router.Use(middleware.Recoverer)
 | 
			
		||||
	s.router.Use(middleware.RequestID)
 | 
			
		||||
	s.router.Use(s.LoggingMiddleware())
 | 
			
		||||
	s.router.Use(s.mw.Logging())
 | 
			
		||||
 | 
			
		||||
	// add metrics middleware only if we can serve them behind auth
 | 
			
		||||
	if viper.GetString("METRICS_USERNAME") != "" {
 | 
			
		||||
		s.router.Use(s.MetricsMiddleware())
 | 
			
		||||
		s.router.Use(s.mw.Metrics())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// set up CORS headers. you'll probably want to configure that
 | 
			
		||||
	// in middlewares.go.
 | 
			
		||||
	s.router.Use(s.CORSMiddleware())
 | 
			
		||||
	s.router.Use(s.mw.CORS())
 | 
			
		||||
 | 
			
		||||
	// CHANGEME to suit your needs, or pull from config.
 | 
			
		||||
	// timeout for request context; your handlers must finish within
 | 
			
		||||
@ -57,39 +57,48 @@ func (s *server) routes() {
 | 
			
		||||
	// complete docs: https://github.com/go-chi/chi
 | 
			
		||||
	////////////////////////////////////////////////////////////////////////
 | 
			
		||||
 | 
			
		||||
	s.router.Get("/", s.handleIndex())
 | 
			
		||||
	s.router.Get("/", s.h.HandleIndex())
 | 
			
		||||
 | 
			
		||||
	s.router.Mount("/s", http.StripPrefix("/s", http.FileServer(http.FS(static.Static))))
 | 
			
		||||
 | 
			
		||||
	s.router.Route("/api/v1", func(r chi.Router) {
 | 
			
		||||
		r.Get("/now", s.handleNow())
 | 
			
		||||
		r.Get("/now", s.h.HandleNow())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// if you want to use a general purpose middleware (http.Handler
 | 
			
		||||
	// wrapper) on a specific HandleFunc route, you need to take the
 | 
			
		||||
	// .ServeHTTP of the http.Handler to get its HandleFunc, viz:
 | 
			
		||||
	authMiddleware := s.AuthMiddleware()
 | 
			
		||||
	auth := s.mw.Auth()
 | 
			
		||||
	s.router.Get(
 | 
			
		||||
		"/login",
 | 
			
		||||
		authMiddleware(s.handleLogin()).ServeHTTP,
 | 
			
		||||
		auth(s.h.HandleLoginGET()).ServeHTTP,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	s.router.Get(
 | 
			
		||||
		"/signup",
 | 
			
		||||
		auth(s.h.HandleSignupGET()).ServeHTTP,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	s.router.Post(
 | 
			
		||||
		"/signup",
 | 
			
		||||
		auth(s.h.HandleSignupPOST()).ServeHTTP,
 | 
			
		||||
	)
 | 
			
		||||
	// route that panics for testing
 | 
			
		||||
	// CHANGEME remove this
 | 
			
		||||
	s.router.Get(
 | 
			
		||||
		"/panic",
 | 
			
		||||
		s.handlePanic(),
 | 
			
		||||
		s.h.HandlePanic(),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	s.router.Get(
 | 
			
		||||
		"/.well-known/healthcheck.json",
 | 
			
		||||
		s.handleHealthCheck(),
 | 
			
		||||
		s.h.HandleHealthCheck(),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// set up authenticated /metrics route:
 | 
			
		||||
	if viper.GetString("METRICS_USERNAME") != "" {
 | 
			
		||||
		s.router.Group(func(r chi.Router) {
 | 
			
		||||
			r.Use(s.MetricsAuthMiddleware())
 | 
			
		||||
			r.Use(s.mw.MetricsAuth())
 | 
			
		||||
			r.Get("/metrics", http.HandlerFunc(promhttp.Handler().ServeHTTP))
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										178
									
								
								internal/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								internal/server/server.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,178 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/config"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/globals"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/handlers"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/logger"
 | 
			
		||||
	"git.eeqj.de/sneak/gohttpserver/internal/middleware"
 | 
			
		||||
	"github.com/rs/zerolog"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
 | 
			
		||||
	"github.com/getsentry/sentry-go"
 | 
			
		||||
	"github.com/go-chi/chi"
 | 
			
		||||
 | 
			
		||||
	// spooky action at a distance!
 | 
			
		||||
	// this populates the environment
 | 
			
		||||
	// from a ./.env file automatically
 | 
			
		||||
	// for development configuration.
 | 
			
		||||
	// .env contents should be things like
 | 
			
		||||
	// `DBURL=postgres://user:pass@.../`
 | 
			
		||||
	// (without the backticks, of course)
 | 
			
		||||
	_ "github.com/joho/godotenv/autoload"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ServerParams struct {
 | 
			
		||||
	fx.In
 | 
			
		||||
	Logger     *logger.Logger
 | 
			
		||||
	Globals    *globals.Globals
 | 
			
		||||
	Config     *config.Config
 | 
			
		||||
	Middleware *middleware.Middleware
 | 
			
		||||
	Handlers   *handlers.Handlers
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Server struct {
 | 
			
		||||
	startupTime   time.Time
 | 
			
		||||
	port          int
 | 
			
		||||
	exitCode      int
 | 
			
		||||
	sentryEnabled bool
 | 
			
		||||
	log           *zerolog.Logger
 | 
			
		||||
	ctx           context.Context
 | 
			
		||||
	cancelFunc    context.CancelFunc
 | 
			
		||||
	httpServer    *http.Server
 | 
			
		||||
	router        *chi.Mux
 | 
			
		||||
	params        ServerParams
 | 
			
		||||
	mw            *middleware.Middleware
 | 
			
		||||
	h             *handlers.Handlers
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
 | 
			
		||||
	s := new(Server)
 | 
			
		||||
	s.params = params
 | 
			
		||||
	s.mw = params.Middleware
 | 
			
		||||
	s.h = params.Handlers
 | 
			
		||||
	s.log = params.Logger.Get()
 | 
			
		||||
 | 
			
		||||
	lc.Append(fx.Hook{
 | 
			
		||||
		OnStart: func(ctx context.Context) error {
 | 
			
		||||
			s.startupTime = time.Now()
 | 
			
		||||
			go s.Run() // background FIXME
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		OnStop: func(ctx context.Context) error {
 | 
			
		||||
			// FIXME do server shutdown here
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FIXME change this to use uber/fx DI and an Invoke()
 | 
			
		||||
// this is where we come in from package main.
 | 
			
		||||
func (s *Server) Run() {
 | 
			
		||||
	// this does nothing if SENTRY_DSN is unset in env.
 | 
			
		||||
	// TODO remove:
 | 
			
		||||
	if s.sentryEnabled {
 | 
			
		||||
		sentry.CaptureMessage("It works!")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.configure()
 | 
			
		||||
 | 
			
		||||
	// logging before sentry, because sentry logs
 | 
			
		||||
	s.enableSentry()
 | 
			
		||||
 | 
			
		||||
	s.serve() // FIXME deal with return value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) enableSentry() {
 | 
			
		||||
	s.sentryEnabled = false
 | 
			
		||||
 | 
			
		||||
	if s.params.Config.SentryDSN == "" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := sentry.Init(sentry.ClientOptions{
 | 
			
		||||
		Dsn:     s.params.Config.SentryDSN,
 | 
			
		||||
		Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.log.Fatal().Err(err).Msg("sentry init failure")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	s.log.Info().Msg("sentry error reporting activated")
 | 
			
		||||
	s.sentryEnabled = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) serve() int {
 | 
			
		||||
	// FIXME fx will handle this for us
 | 
			
		||||
	s.ctx, s.cancelFunc = context.WithCancel(context.Background())
 | 
			
		||||
 | 
			
		||||
	// signal watcher
 | 
			
		||||
	go func() {
 | 
			
		||||
		c := make(chan os.Signal, 1)
 | 
			
		||||
		signal.Ignore(syscall.SIGPIPE)
 | 
			
		||||
		signal.Notify(c, os.Interrupt, syscall.SIGTERM)
 | 
			
		||||
		// block and wait for signal
 | 
			
		||||
		sig := <-c
 | 
			
		||||
		s.log.Info().Msgf("signal received: %+v", sig)
 | 
			
		||||
		if s.cancelFunc != nil {
 | 
			
		||||
			// cancelling the main context will trigger a clean
 | 
			
		||||
			// shutdown.
 | 
			
		||||
			s.cancelFunc()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	go s.serveUntilShutdown()
 | 
			
		||||
 | 
			
		||||
	for range s.ctx.Done() {
 | 
			
		||||
		// aforementioned clean shutdown upon main context
 | 
			
		||||
		// cancellation
 | 
			
		||||
	}
 | 
			
		||||
	s.cleanShutdown()
 | 
			
		||||
	return s.exitCode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) cleanupForExit() {
 | 
			
		||||
	s.log.Info().Msg("cleaning up")
 | 
			
		||||
	// FIXME unimplemented
 | 
			
		||||
	// close database connections or whatever
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) cleanShutdown() {
 | 
			
		||||
	// initiate clean shutdown
 | 
			
		||||
	s.exitCode = 0
 | 
			
		||||
	ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
			
		||||
	if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
 | 
			
		||||
		s.log.Error().
 | 
			
		||||
			Err(err).
 | 
			
		||||
			Msg("server clean shutdown failed")
 | 
			
		||||
	}
 | 
			
		||||
	if shutdownCancel != nil {
 | 
			
		||||
		shutdownCancel()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.cleanupForExit()
 | 
			
		||||
 | 
			
		||||
	if s.sentryEnabled {
 | 
			
		||||
		sentry.Flush(2 * time.Second)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) MaintenanceMode() bool {
 | 
			
		||||
	return s.params.Config.MaintenanceMode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) configure() {
 | 
			
		||||
	// FIXME move most of this to dedicated places
 | 
			
		||||
	// if viper.GetBool("DEBUG") {
 | 
			
		||||
	//      pp.Print(viper.AllSettings())
 | 
			
		||||
	// }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								templates/htmlfooter.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								templates/htmlfooter.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<script src="/s/js/jquery-3.5.1.slim.min.js"></script>
 | 
			
		||||
<script src="/s/js/bootstrap-4.5.3.bundle.min.js"></script>
 | 
			
		||||
<script src="/s/js/main.js"></script>
 | 
			
		||||
							
								
								
									
										4
									
								
								templates/htmlheader.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								templates/htmlheader.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
    <title>{{ .HTMLTitle }}</title>
 | 
			
		||||
    <link rel="stylesheet" href="/s/css/bootstrap-4.5.3.min.css" />
 | 
			
		||||
    <link rel="stylesheet" href="/s/css/style.css" />
 | 
			
		||||
@ -1,84 +1,29 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
    <title>Changeme: Go HTTP Server Boilerplate</title>
 | 
			
		||||
    <link rel="stylesheet" href="/s/css/bootstrap-4.5.3.min.css" />
 | 
			
		||||
    <link rel="stylesheet" href="/s/css/style.css" />
 | 
			
		||||
      {{ template "htmlheader.html" . }}
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
 | 
			
		||||
      <a class="navbar-brand" href="#">Quickstart</a>
 | 
			
		||||
      <button
 | 
			
		||||
        class="navbar-toggler"
 | 
			
		||||
        type="button"
 | 
			
		||||
        data-toggle="collapse"
 | 
			
		||||
        data-target="#navbarsExampleDefault"
 | 
			
		||||
        aria-controls="navbarsExampleDefault"
 | 
			
		||||
        aria-expanded="false"
 | 
			
		||||
        aria-label="Toggle navigation"
 | 
			
		||||
      >
 | 
			
		||||
        <span class="navbar-toggler-icon"></span>
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      <div class="collapse navbar-collapse" id="navbarsExampleDefault">
 | 
			
		||||
        <ul class="navbar-nav mr-auto">
 | 
			
		||||
          <li class="nav-item active">
 | 
			
		||||
            <a class="nav-link" href="#"
 | 
			
		||||
              >Home <span class="sr-only">(current)</span></a
 | 
			
		||||
            >
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="nav-item">
 | 
			
		||||
            <a class="nav-link" href="#">Link</a>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="nav-item">
 | 
			
		||||
            <a
 | 
			
		||||
              class="nav-link disabled"
 | 
			
		||||
              href="#"
 | 
			
		||||
              tabindex="-1"
 | 
			
		||||
              aria-disabled="true"
 | 
			
		||||
              >Disabled</a
 | 
			
		||||
            >
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="nav-item dropdown">
 | 
			
		||||
            <a
 | 
			
		||||
              class="nav-link dropdown-toggle"
 | 
			
		||||
              href="#"
 | 
			
		||||
              id="dropdown01"
 | 
			
		||||
              data-toggle="dropdown"
 | 
			
		||||
              aria-haspopup="true"
 | 
			
		||||
              aria-expanded="false"
 | 
			
		||||
              >Dropdown</a
 | 
			
		||||
            >
 | 
			
		||||
            <div class="dropdown-menu" aria-labelledby="dropdown01">
 | 
			
		||||
              <a class="dropdown-item" href="#">Action</a>
 | 
			
		||||
              <a class="dropdown-item" href="#">Another action</a>
 | 
			
		||||
              <a class="dropdown-item" href="#">Something else here</a>
 | 
			
		||||
            </div>
 | 
			
		||||
          </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        <form class="form-inline my-2 my-lg-0">
 | 
			
		||||
          <input
 | 
			
		||||
            class="form-control mr-sm-2"
 | 
			
		||||
            type="text"
 | 
			
		||||
            placeholder="Search"
 | 
			
		||||
            aria-label="Search"
 | 
			
		||||
          />
 | 
			
		||||
          <button class="btn btn-outline-success my-2 my-sm-0" type="submit">
 | 
			
		||||
            Search
 | 
			
		||||
          </button>
 | 
			
		||||
        </form>
 | 
			
		||||
      </div>
 | 
			
		||||
    </nav>
 | 
			
		||||
 | 
			
		||||
    {{ template "navbar.html" .}}
 | 
			
		||||
    <main role="main">
 | 
			
		||||
      <div class="jumbotron">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
          <h1 class="display-3">Hello, world!</h1>
 | 
			
		||||
          <h2><a
 | 
			
		||||
                      href="https://git.eeqj.de/sneak/gohttpserver">gohttpserver</a></h2>
 | 
			
		||||
          <p>
 | 
			
		||||
            This is a boilerplate application for you to use as a base for your
 | 
			
		||||
            own sites and services.
 | 
			
		||||
          </p>
 | 
			
		||||
          <p>
 | 
			
		||||
          Find more info at <a
 | 
			
		||||
                                    href="https://git.eeqj.de/sneak/gohttpserver">https://git.eeqj.de/sneak/gohttpserver</a>.
 | 
			
		||||
          </p>
 | 
			
		||||
          <p>
 | 
			
		||||
          This software is provided by <a
 | 
			
		||||
                                               href="https://sneak.berlin">@sneak</a>
 | 
			
		||||
          and is released unconditionally into the public domain.
 | 
			
		||||
          </p>
 | 
			
		||||
          <p>
 | 
			
		||||
            <a class="btn btn-primary btn-lg" href="#" role="button"
 | 
			
		||||
              >Learn more »</a
 | 
			
		||||
@ -130,11 +75,7 @@
 | 
			
		||||
      <!-- /container -->
 | 
			
		||||
    </main>
 | 
			
		||||
 | 
			
		||||
    <footer class="container">
 | 
			
		||||
      <p>© No rights reserved - This is in the public domain!</p>
 | 
			
		||||
    </footer>
 | 
			
		||||
    <script src="/s/js/jquery-3.5.1.slim.min.js"></script>
 | 
			
		||||
    <script src="/s/js/bootstrap-4.5.3.bundle.min.js"></script>
 | 
			
		||||
    <script src="/s/js/main.js"></script>
 | 
			
		||||
    {{ template "pagefooter.html" . }}
 | 
			
		||||
  </body>
 | 
			
		||||
  {{ template "htmlfooter.html" . }}
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,7 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
    <title>Changeme: Go HTTP Server Boilerplate</title>
 | 
			
		||||
    <link rel="stylesheet" href="/s/css/bootstrap-4.5.3.min.css" />
 | 
			
		||||
 | 
			
		||||
    {{ template "htmlheader.html" . }}
 | 
			
		||||
    <style>
 | 
			
		||||
      body {
 | 
			
		||||
        padding-top: 3.5rem;
 | 
			
		||||
@ -28,73 +25,9 @@
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
 | 
			
		||||
    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
 | 
			
		||||
      <a class="navbar-brand" href="#">Quickstart</a>
 | 
			
		||||
      <button
 | 
			
		||||
        class="navbar-toggler"
 | 
			
		||||
        type="button"
 | 
			
		||||
        data-toggle="collapse"
 | 
			
		||||
        data-target="#navbarsExampleDefault"
 | 
			
		||||
        aria-controls="navbarsExampleDefault"
 | 
			
		||||
        aria-expanded="false"
 | 
			
		||||
        aria-label="Toggle navigation"
 | 
			
		||||
      >
 | 
			
		||||
        <span class="navbar-toggler-icon"></span>
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      <div class="collapse navbar-collapse" id="navbarsExampleDefault">
 | 
			
		||||
        <ul class="navbar-nav mr-auto">
 | 
			
		||||
          <li class="nav-item active">
 | 
			
		||||
            <a class="nav-link" href="#"
 | 
			
		||||
              >Home <span class="sr-only">(current)</span></a
 | 
			
		||||
            >
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="nav-item">
 | 
			
		||||
            <a class="nav-link" href="#">Link</a>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="nav-item">
 | 
			
		||||
            <a
 | 
			
		||||
              class="nav-link disabled"
 | 
			
		||||
              href="#"
 | 
			
		||||
              tabindex="-1"
 | 
			
		||||
              aria-disabled="true"
 | 
			
		||||
              >Disabled</a
 | 
			
		||||
            >
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="nav-item dropdown">
 | 
			
		||||
            <a
 | 
			
		||||
              class="nav-link dropdown-toggle"
 | 
			
		||||
              href="#"
 | 
			
		||||
              id="dropdown01"
 | 
			
		||||
              data-toggle="dropdown"
 | 
			
		||||
              aria-haspopup="true"
 | 
			
		||||
              aria-expanded="false"
 | 
			
		||||
              >Dropdown</a
 | 
			
		||||
            >
 | 
			
		||||
            <div class="dropdown-menu" aria-labelledby="dropdown01">
 | 
			
		||||
              <a class="dropdown-item" href="#">Action</a>
 | 
			
		||||
              <a class="dropdown-item" href="#">Another action</a>
 | 
			
		||||
              <a class="dropdown-item" href="#">Something else here</a>
 | 
			
		||||
            </div>
 | 
			
		||||
          </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        <form class="form-inline my-2 my-lg-0">
 | 
			
		||||
          <input
 | 
			
		||||
            class="form-control mr-sm-2"
 | 
			
		||||
            type="text"
 | 
			
		||||
            placeholder="Search"
 | 
			
		||||
            aria-label="Search"
 | 
			
		||||
          />
 | 
			
		||||
          <button class="btn btn-outline-success my-2 my-sm-0" type="submit">
 | 
			
		||||
            Search
 | 
			
		||||
          </button>
 | 
			
		||||
        </form>
 | 
			
		||||
      </div>
 | 
			
		||||
    </nav>
 | 
			
		||||
  {{ template "navbar.html" .}}
 | 
			
		||||
 | 
			
		||||
    <main role="main">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <div class="login-form">
 | 
			
		||||
    <form action="/login" method="post">
 | 
			
		||||
        <h2 class="text-center">Log in</h2>
 | 
			
		||||
@ -118,11 +51,6 @@
 | 
			
		||||
 | 
			
		||||
    </main>
 | 
			
		||||
 | 
			
		||||
    <footer class="container">
 | 
			
		||||
      <p>© No rights reserved - This is in the public domain!</p>
 | 
			
		||||
    </footer>
 | 
			
		||||
    <script src="/s/js/jquery-3.5.1.slim.min.js"></script>
 | 
			
		||||
    <script src="/s/js/bootstrap-4.5.3.bundle.min.js"></script>
 | 
			
		||||
    <script src="/s/js/main.js"></script>
 | 
			
		||||
    {{ template "pagefooter.html" . }}
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										64
									
								
								templates/navbar.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								templates/navbar.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
 | 
			
		||||
    <a class="navbar-brand" href="/">{{.SiteName}}</a>
 | 
			
		||||
    <button
 | 
			
		||||
        class="navbar-toggler"
 | 
			
		||||
        type="button"
 | 
			
		||||
        data-toggle="collapse"
 | 
			
		||||
        data-target="#navbarsExampleDefault"
 | 
			
		||||
        aria-controls="navbarsExampleDefault"
 | 
			
		||||
        aria-expanded="false"
 | 
			
		||||
        aria-label="Toggle navigation"
 | 
			
		||||
    >
 | 
			
		||||
        <span class="navbar-toggler-icon"> </span>
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
    <div class="collapse navbar-collapse" id="navbarsExampleDefault">
 | 
			
		||||
        <ul class="navbar-nav mr-auto">
 | 
			
		||||
            <li class="nav-item active">
 | 
			
		||||
                <a class="nav-link" href="#"
 | 
			
		||||
                    >Home <span class="sr-only">(current)</span></a
 | 
			
		||||
                >
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link" href="#">Link</a>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link disabled" href="#" aria-disabled="true"
 | 
			
		||||
                    >Disabled</a
 | 
			
		||||
                >
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item dropdown">
 | 
			
		||||
                <a
 | 
			
		||||
                    class="nav-link dropdown-toggle"
 | 
			
		||||
                    href="#"
 | 
			
		||||
                    id="dropdown01"
 | 
			
		||||
                    data-toggle="dropdown"
 | 
			
		||||
                    aria-haspopup="true"
 | 
			
		||||
                    aria-expanded="false"
 | 
			
		||||
                    >Dropdown</a
 | 
			
		||||
                >
 | 
			
		||||
                <div class="dropdown-menu" aria-labelledby="dropdown01">
 | 
			
		||||
                    <a class="dropdown-item" href="#">Action</a>
 | 
			
		||||
                    <a class="dropdown-item" href="#">Another action</a>
 | 
			
		||||
                    <a class="dropdown-item" href="#"
 | 
			
		||||
                        >Something else here</a
 | 
			
		||||
                    >
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        <form action="POST" class="form-inline my-2 my-lg-0">
 | 
			
		||||
            <input
 | 
			
		||||
                class="form-control mr-sm-2"
 | 
			
		||||
                type="text"
 | 
			
		||||
                placeholder="Search"
 | 
			
		||||
                aria-label="Search"
 | 
			
		||||
            />
 | 
			
		||||
            <button
 | 
			
		||||
                class="btn btn-outline-success my-2 my-sm-0"
 | 
			
		||||
                type="submit"
 | 
			
		||||
            >
 | 
			
		||||
                Search
 | 
			
		||||
            </button>
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
</nav>
 | 
			
		||||
							
								
								
									
										3
									
								
								templates/pagefooter.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								templates/pagefooter.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<footer class="container">
 | 
			
		||||
  <p>© No rights reserved - This is in the public domain!</p>
 | 
			
		||||
</footer>
 | 
			
		||||
							
								
								
									
										76
									
								
								templates/signup.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								templates/signup.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    {{ template "htmlheader.html" .HTMLHeader }}
 | 
			
		||||
    <style>
 | 
			
		||||
      body {
 | 
			
		||||
        padding-top: 3.5rem;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .bd-placeholder-img {
 | 
			
		||||
        font-size: 1.125rem;
 | 
			
		||||
        text-anchor: middle;
 | 
			
		||||
        -webkit-user-select: none;
 | 
			
		||||
        -moz-user-select: none;
 | 
			
		||||
        -ms-user-select: none;
 | 
			
		||||
        user-select: none;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      @media (min-width: 768px) {
 | 
			
		||||
        .bd-placeholder-img-lg {
 | 
			
		||||
          font-size: 3.5rem;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
 | 
			
		||||
  {{ template "navbar.html" .Navbar}}
 | 
			
		||||
 | 
			
		||||
    <main role="main">
 | 
			
		||||
    <div class="signup-form">
 | 
			
		||||
    <form action="/signup" method="post">
 | 
			
		||||
        <h2 class="text-center">Create New Account</h2>
 | 
			
		||||
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
            <span>Email:</span>
 | 
			
		||||
            <input type="text" class="form-control" name="email"
 | 
			
		||||
            placeholder="user@domain.com" required="required">
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
            <span>Desired Username:</span>
 | 
			
		||||
            <input type="text" class="form-control" name="desiredUsername"  placeholder="Username" required="required">
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
            <p>Please use a unique password that you don't use anywhere
 | 
			
		||||
            else.  Minimum 12 characters.</p>
 | 
			
		||||
            <span>New Password:</span>
 | 
			
		||||
            <input type="password" class="form-control"
 | 
			
		||||
                         name="desiredPassword1" placeholder="Password" required="required">
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
            <span>New Password (again):</span>
 | 
			
		||||
            <input type="password"  class="form-control"
 | 
			
		||||
                         name="desiredPassword2" placeholder="Password
 | 
			
		||||
                         (again)" required="required">
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
            <button type="submit" class="btn btn-primary btn-block">Create
 | 
			
		||||
                New Account</button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- <div class="clearfix">
 | 
			
		||||
            <label class="float-left form-check-label"><input type="checkbox"> Remember me</label>
 | 
			
		||||
            <a href="#" class="float-right">Forgot Password?</a>
 | 
			
		||||
        </div>
 | 
			
		||||
        -->
 | 
			
		||||
    </form>
 | 
			
		||||
    <p class="text-center"><a href="#">Create an Account</a></p>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    </main>
 | 
			
		||||
 | 
			
		||||
    {{ template "pagefooter.html" .PageFooter }}
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
@ -2,12 +2,21 @@ package templates
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"text/template"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
//go:embed *.html
 | 
			
		||||
var Templates embed.FS
 | 
			
		||||
var TemplatesRaw embed.FS
 | 
			
		||||
var TemplatesParsed *template.Template
 | 
			
		||||
 | 
			
		||||
func GetParsed() *template.Template {
 | 
			
		||||
	if TemplatesParsed == nil {
 | 
			
		||||
		TemplatesParsed = template.Must(template.ParseFS(TemplatesRaw, "*"))
 | 
			
		||||
	}
 | 
			
		||||
	return TemplatesParsed
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
func MustString(filename string) string {
 | 
			
		||||
	bytes, error := Templates.ReadFile(filename)
 | 
			
		||||
	if error != nil {
 | 
			
		||||
@ -17,3 +26,4 @@ func MustString(filename string) string {
 | 
			
		||||
	out.Write(bytes)
 | 
			
		||||
	return out.String()
 | 
			
		||||
}
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user