From 01ed66d6dfd64acffe52cb389095f44619752562 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 8 Feb 2021 12:57:14 +0100 Subject: [PATCH] basic auth middleware setup --- admindb/interface.go | 29 ++++++ admindb/mockdb/alias.go | 37 +++++++ admindb/mockdb/auth.go | 37 +++++++ admindb/mockdb/auth_fallback.go | 118 +++++++++++++++++++++++ admindb/mockdb/room.go | 37 +++++++ cmd/server/main.go | 36 +++++-- go.mod | 5 +- go.sum | 16 +-- web/embedded_dev.go | 2 + web/handlers/admin/handler.go | 30 ++++++ web/handlers/auth/handler.go | 23 +++++ web/handlers/auth/handlers.go | 1 - web/handlers/http.go | 98 +++++++++++++++++-- web/handlers/http_test.go | 80 +++++++++++++++ web/handlers/news/app_test.go | 4 +- web/handlers/setup_test.go | 57 +++++++++++ web/i18n/helper.go | 6 +- web/production.go | 6 ++ web/router/admin.go | 21 ++++ web/router/auth.go | 17 +++- web/router/complete.go | 6 +- web/templates/admin/dashboard.tmpl | 11 +++ web/templates/auth/fallback_sign_in.tmpl | 13 +++ web/templates/landing/index.tmpl | 4 +- web/templates_vfsdata.go | 30 +++++- 25 files changed, 679 insertions(+), 45 deletions(-) create mode 100644 admindb/interface.go create mode 100644 admindb/mockdb/alias.go create mode 100644 admindb/mockdb/auth.go create mode 100644 admindb/mockdb/auth_fallback.go create mode 100644 admindb/mockdb/room.go create mode 100644 web/handlers/admin/handler.go create mode 100644 web/handlers/auth/handler.go delete mode 100644 web/handlers/auth/handlers.go create mode 100644 web/handlers/http_test.go create mode 100644 web/handlers/setup_test.go create mode 100644 web/production.go create mode 100644 web/router/admin.go create mode 100644 web/templates/admin/dashboard.tmpl create mode 100644 web/templates/auth/fallback_sign_in.tmpl diff --git a/admindb/interface.go b/admindb/interface.go new file mode 100644 index 0000000..a000ce8 --- /dev/null +++ b/admindb/interface.go @@ -0,0 +1,29 @@ +package admindb + +import ( + "go.mindeco.de/http/auth" +) + +// FallbackAuth might be helpful for scenarios where one lost access to his ssb device or key +type FallbackAuth interface { + auth.Auther +} + +// AuthService defines functions needed for the challange/response system of sign-in with ssb +type AuthService interface{} + +// RoomService deals with changing the privacy modes and managing the allow/deny lists of the room +type RoomService interface{} + +// AliasService manages alias handle registration and lookup +type AliasService interface{} + +// for tests we use generated mocks from these interfaces created with https://github.com/maxbrunsfeld/counterfeiter + +//go:generate counterfeiter -o mockdb/auth.go . AuthService + +//go:generate counterfeiter -o mockdb/auth_fallback.go . FallbackAuth + +//go:generate counterfeiter -o mockdb/room.go . RoomService + +//go:generate counterfeiter -o mockdb/alias.go . AliasService diff --git a/admindb/mockdb/alias.go b/admindb/mockdb/alias.go new file mode 100644 index 0000000..34c2f1b --- /dev/null +++ b/admindb/mockdb/alias.go @@ -0,0 +1,37 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mockdb + +import ( + "sync" + + "github.com/ssb-ngi-pointer/gossb-rooms/admindb" +) + +type FakeAliasService struct { + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeAliasService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeAliasService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ admindb.AliasService = new(FakeAliasService) diff --git a/admindb/mockdb/auth.go b/admindb/mockdb/auth.go new file mode 100644 index 0000000..60ec6c0 --- /dev/null +++ b/admindb/mockdb/auth.go @@ -0,0 +1,37 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mockdb + +import ( + "sync" + + "github.com/ssb-ngi-pointer/gossb-rooms/admindb" +) + +type FakeAuthService struct { + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeAuthService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeAuthService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ admindb.AuthService = new(FakeAuthService) diff --git a/admindb/mockdb/auth_fallback.go b/admindb/mockdb/auth_fallback.go new file mode 100644 index 0000000..43fbb7c --- /dev/null +++ b/admindb/mockdb/auth_fallback.go @@ -0,0 +1,118 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mockdb + +import ( + "sync" + + "github.com/ssb-ngi-pointer/gossb-rooms/admindb" +) + +type FakeFallbackAuth struct { + CheckStub func(string, string) (interface{}, error) + checkMutex sync.RWMutex + checkArgsForCall []struct { + arg1 string + arg2 string + } + checkReturns struct { + result1 interface{} + result2 error + } + checkReturnsOnCall map[int]struct { + result1 interface{} + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeFallbackAuth) Check(arg1 string, arg2 string) (interface{}, error) { + fake.checkMutex.Lock() + ret, specificReturn := fake.checkReturnsOnCall[len(fake.checkArgsForCall)] + fake.checkArgsForCall = append(fake.checkArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.CheckStub + fakeReturns := fake.checkReturns + fake.recordInvocation("Check", []interface{}{arg1, arg2}) + fake.checkMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeFallbackAuth) CheckCallCount() int { + fake.checkMutex.RLock() + defer fake.checkMutex.RUnlock() + return len(fake.checkArgsForCall) +} + +func (fake *FakeFallbackAuth) CheckCalls(stub func(string, string) (interface{}, error)) { + fake.checkMutex.Lock() + defer fake.checkMutex.Unlock() + fake.CheckStub = stub +} + +func (fake *FakeFallbackAuth) CheckArgsForCall(i int) (string, string) { + fake.checkMutex.RLock() + defer fake.checkMutex.RUnlock() + argsForCall := fake.checkArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeFallbackAuth) CheckReturns(result1 interface{}, result2 error) { + fake.checkMutex.Lock() + defer fake.checkMutex.Unlock() + fake.CheckStub = nil + fake.checkReturns = struct { + result1 interface{} + result2 error + }{result1, result2} +} + +func (fake *FakeFallbackAuth) CheckReturnsOnCall(i int, result1 interface{}, result2 error) { + fake.checkMutex.Lock() + defer fake.checkMutex.Unlock() + fake.CheckStub = nil + if fake.checkReturnsOnCall == nil { + fake.checkReturnsOnCall = make(map[int]struct { + result1 interface{} + result2 error + }) + } + fake.checkReturnsOnCall[i] = struct { + result1 interface{} + result2 error + }{result1, result2} +} + +func (fake *FakeFallbackAuth) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.checkMutex.RLock() + defer fake.checkMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeFallbackAuth) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ admindb.FallbackAuth = new(FakeFallbackAuth) diff --git a/admindb/mockdb/room.go b/admindb/mockdb/room.go new file mode 100644 index 0000000..103067a --- /dev/null +++ b/admindb/mockdb/room.go @@ -0,0 +1,37 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mockdb + +import ( + "sync" + + "github.com/ssb-ngi-pointer/gossb-rooms/admindb" +) + +type FakeRoomService struct { + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeRoomService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeRoomService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ admindb.RoomService = new(FakeRoomService) diff --git a/cmd/server/main.go b/cmd/server/main.go index d46e435..7e1ae8b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -166,6 +166,16 @@ func runroomsrv() error { return fmt.Errorf("failed to instantiate ssb server: %w", err) } + // open the HTTP listener + httpLis, err := net.Listen("tcp", listenAddrHTTP) + if err != nil { + return fmt.Errorf("failed to open listener for HTTPdashboard: %w", err) + } + + r := repo.New(repoDir) + + db, err := sqlite.OpenOrCreate(r.GetPath("roomdb")) + c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { @@ -173,6 +183,8 @@ func runroomsrv() error { level.Warn(log).Log("event", "killed", "msg", "received signal, shutting down", "signal", sig.String()) cancel() roomsrv.Shutdown() + + httpLis.Close() time.Sleep(2 * time.Second) err := roomsrv.Close() @@ -183,17 +195,14 @@ func runroomsrv() error { }() // setup web dashboard handlers - dashboardH, err := handlers.New(nil, repo.New(repoDir)) + dashboardH, err := handlers.New( + nil, + repo.New(repoDir), + ) if err != nil { return fmt.Errorf("failed to create HTTPdashboard handler: %w", err) } - // open the HTTP listener - httpLis, err := net.Listen("tcp", listenAddrHTTP) - if err != nil { - return fmt.Errorf("failed to open listener for HTTPdashboard: %w", err) - } - // TODO: setup other http goodies (such as CSRF and CSP) level.Info(log).Log( @@ -207,7 +216,18 @@ func runroomsrv() error { // start serving http connections go func() { - err = http.Serve(httpLis, dashboardH) + srv := http.Server{ + Addr: httpLis.Addr().String(), + + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + + Handler: dashboardH, + } + + err = srv.Serve(httpLis) if err != nil { level.Error(log).Log("event", "http serve failed", "err", err) } diff --git a/go.mod b/go.mod index d1c28f2..d5138cc 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.15 require ( github.com/BurntSushi/toml v0.3.1 github.com/go-kit/kit v0.10.0 - github.com/gorilla/mux v1.7.3 + github.com/gorilla/mux v1.8.0 + github.com/gorilla/securecookie v1.1.1 + github.com/gorilla/sessions v1.2.1 github.com/gorilla/websocket v1.4.2 github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472 github.com/nicksnyder/go-i18n/v2 v2.1.2 @@ -22,6 +24,7 @@ require ( golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect golang.org/x/text v0.3.3 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) exclude go.cryptoscope.co/ssb v0.0.0-20201207161753-31d0f24b7a79 diff --git a/go.sum b/go.sum index 65fa39a..b6549b3 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,13 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -206,7 +211,6 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc= github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c= github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= @@ -267,7 +271,6 @@ github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxr github.com/shurcooL/go v0.0.0-20190121191506-3fef8c783dec/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go v0.0.0-20190330031554-6713ea532688/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b h1:4kg1wyftSKxLtnPAvcRWakIPpokB9w780/KwrNLnfPA= github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= @@ -286,7 +289,6 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3 github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -325,10 +327,6 @@ go.cryptoscope.co/secretstream v1.2.2 h1:kPxsgWrTDFyS9ZklcD0si1KGljPLz6mmPKnFQjG go.cryptoscope.co/secretstream v1.2.2/go.mod h1:7nRGZ7fTqSgQAnv2Y4m8xQsS3MFxvB7I0C19reUNlXg= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.mindeco.de v0.0.0-20191122233605-f02621a5bca7 h1:si0O0lnNT3SHzEBwuFfqWBaZlME/FnXWlxyZGdri3D8= -go.mindeco.de v0.0.0-20191122233605-f02621a5bca7/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE= -go.mindeco.de v1.6.0 h1:mrO5+eaJou/5a7ob+lMfH+DudT1TJ7oSjYqMTGaoYUA= -go.mindeco.de v1.6.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE= go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870 h1:TCI3AefMAaOYECvppn30+CfEB0Fn8IES1SKvvacc3/c= go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870/go.mod h1:OnBnV02ux4lLsZ39LID6yYLqSDp+dqTHb/3miYPkQFs= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -456,9 +454,11 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 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.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/web/embedded_dev.go b/web/embedded_dev.go index 993a9e3..aa6518a 100644 --- a/web/embedded_dev.go +++ b/web/embedded_dev.go @@ -15,6 +15,8 @@ import ( "go.mindeco.de/goutils" ) +const Production = false + // absolute path of where this package is located var pkgDir = goutils.MustLocatePackage("github.com/ssb-ngi-pointer/gossb-rooms/web") diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go new file mode 100644 index 0000000..e4b4c09 --- /dev/null +++ b/web/handlers/admin/handler.go @@ -0,0 +1,30 @@ +package admin + +import ( + "net/http" + + "github.com/gorilla/mux" + "go.mindeco.de/http/render" + + "github.com/ssb-ngi-pointer/gossb-rooms/web/router" +) + +var HTMLTemplates = []string{ + "/admin/dashboard.tmpl", +} + +func Handler(m *mux.Router, r *render.Renderer) http.Handler { + if m == nil { + m = router.Admin(nil) + } + + m.Get(router.AdminDashboard).HandlerFunc(r.HTML("/admin/dashboard.tmpl", dashboard)) + + return m +} + +func dashboard(rw http.ResponseWriter, req *http.Request) (interface{}, error) { + return struct { + Name string + }{"test"}, nil +} diff --git a/web/handlers/auth/handler.go b/web/handlers/auth/handler.go new file mode 100644 index 0000000..63f6b4d --- /dev/null +++ b/web/handlers/auth/handler.go @@ -0,0 +1,23 @@ +package auth + +import ( + "net/http" + + "github.com/gorilla/mux" + "go.mindeco.de/http/auth" + "go.mindeco.de/http/render" + + "github.com/ssb-ngi-pointer/gossb-rooms/web/router" +) + +func Handler(m *mux.Router, r *render.Renderer, a *auth.Handler) http.Handler { + if m == nil { + m = router.Auth(nil) + } + + m.Get(router.AuthFallbackSignInForm).Handler(r.StaticHTML("/auth/fallback_sign_in.tmpl")) + m.Get(router.AuthFallbackSignIn).HandlerFunc(a.Authorize) + m.Get(router.AuthFallbackSignOut).HandlerFunc(a.Logout) + + return m +} diff --git a/web/handlers/auth/handlers.go b/web/handlers/auth/handlers.go deleted file mode 100644 index 8832b06..0000000 --- a/web/handlers/auth/handlers.go +++ /dev/null @@ -1 +0,0 @@ -package auth diff --git a/web/handlers/http.go b/web/handlers/http.go index 66cd330..f6723bd 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -1,21 +1,33 @@ package handlers import ( + "bytes" "fmt" "net/http" "github.com/gorilla/mux" + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" + "go.mindeco.de/http/auth" "go.mindeco.de/http/render" + "github.com/ssb-ngi-pointer/gossb-rooms/admindb" "github.com/ssb-ngi-pointer/gossb-rooms/internal/repo" "github.com/ssb-ngi-pointer/gossb-rooms/web" + "github.com/ssb-ngi-pointer/gossb-rooms/web/handlers/admin" + roomsAuth "github.com/ssb-ngi-pointer/gossb-rooms/web/handlers/auth" "github.com/ssb-ngi-pointer/gossb-rooms/web/handlers/news" "github.com/ssb-ngi-pointer/gossb-rooms/web/i18n" "github.com/ssb-ngi-pointer/gossb-rooms/web/router" ) // New initializes the whole web stack for rooms, with all the sub-modules and routing. -func New(m *mux.Router, repo repo.Interface) (http.Handler, error) { +func New( + m *mux.Router, + repo repo.Interface, + as admindb.AuthService, + fs admindb.FallbackAuth, +) (http.Handler, error) { if m == nil { m = router.CompleteApp() } @@ -27,10 +39,15 @@ func New(m *mux.Router, repo repo.Interface) (http.Handler, error) { r, err := render.New(web.Templates, render.BaseTemplates("/base.tmpl"), - render.AddTemplates(append(news.HTMLTemplates, - "/landing/index.tmpl", - "/landing/about.tmpl", - "/error.tmpl")...), + render.AddTemplates(concatTemplates( + []string{ + "/landing/index.tmpl", + "/landing/about.tmpl", + "/error.tmpl", + }, + news.HTMLTemplates, + admin.HTMLTemplates, + )...), render.FuncMap(web.TemplateFuncs(m)), // TODO: add plural and template data variants // TODO: move these to the i18n helper pkg @@ -45,7 +62,48 @@ func New(m *mux.Router, repo repo.Interface) (http.Handler, error) { return nil, fmt.Errorf("web Handler: failed to create renderer: %w", err) } + // TODO: generate & persist me + // repo.GetPath("web-cookie") + store := &sessions.CookieStore{ + Codecs: securecookie.CodecsFromPairs( + bytes.Repeat([]byte("acab"), 8), + bytes.Repeat([]byte("beef"), 8), + // securecookie.GenerateRandomKey(32), // new key every time we startup + // securecookie.GenerateRandomKey(32), + ), + Options: &sessions.Options{ + Path: "/", + MaxAge: 30, + }, + } + + notAuthorizedH := r.HTML("/error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) { + statusCode := http.StatusUnauthorized + rw.WriteHeader(statusCode) + return errorTemplateData{ + statusCode, + "Unauthorized", + "you are not authorized to access the requested site", + }, nil + }) + + a, err := auth.NewHandler(fs, + auth.SetStore(store), + auth.SetNotAuthorizedHandler(notAuthorizedH), + ) + if err != nil { + return nil, fmt.Errorf("web Handler: failed to init fallback auth system: %w", err) + } + // hookup handlers to the router + roomsAuth.Handler(m, r, a) + + adminRouter := m.PathPrefix("/admin").Subrouter() + adminRouter.Use(a.Authenticate) + + // we dont strip path here because it somehow fucks with the middleware setup + adminRouter.PathPrefix("/").Handler(admin.Handler(adminRouter, r)) + m.PathPrefix("/news").Handler(http.StripPrefix("/news", news.Handler(m, r))) m.Get(router.CompleteIndex).Handler(r.StaticHTML("/landing/index.tmpl")) @@ -53,10 +111,34 @@ func New(m *mux.Router, repo repo.Interface) (http.Handler, error) { m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets))) - m.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - fmt.Fprintf(rw, "404: url not found") + m.NotFoundHandler = r.HTML("/error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) { + rw.WriteHeader(http.StatusNotFound) + return errorTemplateData{http.StatusNotFound, "Not Found", "the requested page wasnt found.."}, nil }) - // TODO: disable in non-dev + if web.Production { + return m, nil + } + return r.GetReloader()(m), nil } + +// utils + +type errorTemplateData struct { + StatusCode int + Status string + Err string +} + +func concatTemplates(lst ...[]string) []string { + var catted []string + + for _, tpls := range lst { + for _, t := range tpls { + catted = append(catted, t) + } + + } + return catted +} diff --git a/web/handlers/http_test.go b/web/handlers/http_test.go new file mode 100644 index 0000000..5993f5b --- /dev/null +++ b/web/handlers/http_test.go @@ -0,0 +1,80 @@ +package handlers + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ssb-ngi-pointer/gossb-rooms/web/router" +) + +func TestIndex(t *testing.T) { + setup(t) + t.Cleanup(teardown) + + a := assert.New(t) + r := require.New(t) + + url, err := testRouter.Get(router.CompleteIndex).URL() + r.Nil(err) + html, resp := testClient.GetHTML(url.String(), nil) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + a.Equal("Index landing page", html.Find("#welcome").Text()) + val, has := html.Find("#logo").Attr("src") + a.True(has, "logo src attribute not found") + a.Equal("/assets/img/test-hermie.png", val) +} + +func TestAbout(t *testing.T) { + setup(t) + t.Cleanup(teardown) + + a := assert.New(t) + r := require.New(t) + + url, err := testRouter.Get(router.CompleteAbout).URL() + r.Nil(err) + html, resp := testClient.GetHTML(url.String(), nil) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + found := html.Find("h1").Text() + a.Equal("The about page", found) +} + +func TestNotFound(t *testing.T) { + setup(t) + t.Cleanup(teardown) + + a := assert.New(t) + + html, resp := testClient.GetHTML("/some/random/ASDKLANZXC", nil) + a.Equal(http.StatusNotFound, resp.Code, "wrong HTTP status code") + found := html.Find("h1").Text() + a.Equal("Error #404 - Not Found", found) +} + +func TestNewsRegisterd(t *testing.T) { + setup(t) + t.Cleanup(teardown) + + a := assert.New(t) + + html, resp := testClient.GetHTML("/news/", nil) + a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") + found := html.Find("h1").Text() + t.Log(found) + // a.Equal("fooo", found) +} + +func TestRestricted(t *testing.T) { + setup(t) + t.Cleanup(teardown) + + a := assert.New(t) + + html, resp := testClient.GetHTML("/admin/", nil) + a.Equal(http.StatusUnauthorized, resp.Code, "wrong HTTP status code") + found := html.Find("h1").Text() + a.Equal("Error #401 - Unauthorized", found) +} diff --git a/web/handlers/news/app_test.go b/web/handlers/news/app_test.go index ddbabf5..9783f4b 100644 --- a/web/handlers/news/app_test.go +++ b/web/handlers/news/app_test.go @@ -17,8 +17,6 @@ var ( testMux *http.ServeMux testClient *tester.Tester testRouter = router.News(nil) - - testAssets = http.Dir("../../templates") ) func setup(t *testing.T) { @@ -27,7 +25,7 @@ func setup(t *testing.T) { testFuncs["i18n"] = func(msgID string) string { return msgID } log, _ := logtest.KitLogger("feed", t) - r, err := render.New(testAssets, //TODO: embedd web.Assets, + r, err := render.New(web.Templates, render.SetLogger(log), render.BaseTemplates("/testing/base.tmpl"), render.AddTemplates(append(HTMLTemplates, "/error.tmpl")...), diff --git a/web/handlers/setup_test.go b/web/handlers/setup_test.go new file mode 100644 index 0000000..f694412 --- /dev/null +++ b/web/handlers/setup_test.go @@ -0,0 +1,57 @@ +package handlers + +import ( + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/pkg/errors" + "go.mindeco.de/http/tester" + + "github.com/ssb-ngi-pointer/gossb-rooms/admindb/mockdb" + "github.com/ssb-ngi-pointer/gossb-rooms/internal/repo" + "github.com/ssb-ngi-pointer/gossb-rooms/web/router" +) + +var ( + testMux *http.ServeMux + testClient *tester.Tester + testRouter = router.CompleteApp() + + // mocked dbs + testAuthDB *mockdb.FakeAuthService + testAuthFallbackDB *mockdb.FakeFallbackAuth +) + +func setup(t *testing.T) { + + testRepoPath := filepath.Join("testrun", t.Name()) + os.RemoveAll(testRepoPath) + testRepo := repo.New(testRepoPath) + + testAuthDB = new(mockdb.FakeAuthService) + testAuthFallbackDB = new(mockdb.FakeFallbackAuth) + h, err := New( + testRouter, + testRepo, + testAuthDB, + testAuthFallbackDB, + ) + if err != nil { + t.Fatal(errors.Wrap(err, "setup: handler init failed")) + } + + // log, _ := logtest.KitLogger("complete", t) + + testMux = http.NewServeMux() + testMux.Handle("/", h) + testClient = tester.New(testMux, t) +} + +func teardown() { + testMux = nil + testClient = nil + testAuthFallbackDB = nil + testAuthFallbackDB = nil +} diff --git a/web/i18n/helper.go b/web/i18n/helper.go index 3c273d9..2602235 100644 --- a/web/i18n/helper.go +++ b/web/i18n/helper.go @@ -22,7 +22,7 @@ func New(r repo.Interface) (*Helper, error) { bundle := i18n.NewBundle(language.English) bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) - // TODO: could additionally embedd the defaults together with the html assets and templates + // TODO: could additionally embedd the defaults like we do with the html assets and templates err := filepath.Walk(r.GetPath("i18n"), func(path string, info os.FileInfo, err error) error { if err != nil { @@ -44,8 +44,8 @@ func New(r repo.Interface) (*Helper, error) { return nil }) - if err != nil { - return nil, err + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("i18n: failed to iterate localizations: %w", err) } return &Helper{bundle: bundle}, nil diff --git a/web/production.go b/web/production.go new file mode 100644 index 0000000..60a5fc5 --- /dev/null +++ b/web/production.go @@ -0,0 +1,6 @@ +// +build !dev + +package web + +// Production can be used to determain different aspects at compile time (like hot template reloading) +const Production = true diff --git a/web/router/admin.go b/web/router/admin.go new file mode 100644 index 0000000..863c83e --- /dev/null +++ b/web/router/admin.go @@ -0,0 +1,21 @@ +package router + +import "github.com/gorilla/mux" + +// constant names for the named routes +const ( + AdminDashboard = "admin:dashboard" +) + +// Admin constructs a mux.Router containing the routes for the admin dashboard and settings pages +func Admin(m *mux.Router) *mux.Router { + if m == nil { + m = mux.NewRouter() + } + + // we dont strip path here because it somehow fucks with the middleware setup + m.Path("/admin").Methods("GET").Name(AdminDashboard) + // m.Path("/admin/settings").Methods("GET").Name(AdminSettings) + + return m +} diff --git a/web/router/auth.go b/web/router/auth.go index 1e279a0..3f16bf9 100644 --- a/web/router/auth.go +++ b/web/router/auth.go @@ -4,8 +4,12 @@ import "github.com/gorilla/mux" // constant names for the named routes const ( - AuthSignIn = "Auth:SignIn" - AuthSignOut = "Auth:SignOut" + AuthFallbackSignInForm = "Auth:Fallback:Form:SignIn" + AuthFallbackSignIn = "Auth:Fallback:SignIn" + AuthFallbackSignOut = "Auth:Fallback:SignOut" + + AuthWithSSBSignIn = "Auth:WithSSB:SignIn" + AuthWithSSBSignOut = "Auth:WithSSB:SignOut" ) // NewSignin constructs a mux.Router containing the routes for sign-in and -out @@ -14,8 +18,13 @@ func Auth(m *mux.Router) *mux.Router { m = mux.NewRouter() } - m.Path("/signIn").Methods("GET").Name(AuthSignIn) - m.Path("/signOut").Methods("GET").Name(AuthSignOut) + // register fallback + m.Path("/fallback/signin").Methods("GET").Name(AuthFallbackSignInForm) + m.Path("/fallback/signin").Methods("POST").Name(AuthFallbackSignIn) + m.Path("/fallback/signOut").Methods("GET").Name(AuthFallbackSignOut) + + m.Path("/withssb/signIn").Methods("GET").Name(AuthWithSSBSignIn) + m.Path("/withssb/signOut").Methods("GET").Name(AuthWithSSBSignOut) return m } diff --git a/web/router/complete.go b/web/router/complete.go index f047b65..0627c52 100644 --- a/web/router/complete.go +++ b/web/router/complete.go @@ -1,6 +1,8 @@ package router -import "github.com/gorilla/mux" +import ( + "github.com/gorilla/mux" +) // constant names for the named routes const ( @@ -13,7 +15,7 @@ func CompleteApp() *mux.Router { m := mux.NewRouter() Auth(m.PathPrefix("/auth").Subrouter()) - // Admin(m.PathPrefix("/profile").Subrouter()) + Admin(m.PathPrefix("/admin").Subrouter()) News(m.PathPrefix("/news").Subrouter()) m.Path("/").Methods("GET").Name(CompleteIndex) diff --git a/web/templates/admin/dashboard.tmpl b/web/templates/admin/dashboard.tmpl new file mode 100644 index 0000000..9012eca --- /dev/null +++ b/web/templates/admin/dashboard.tmpl @@ -0,0 +1,11 @@ +{{ define "title" }}{{i18n "AdminOverview"}}{{ end }} +{{ define "content" }} + +
+
+

super cool dashboard!

+
+
+{{end}} \ No newline at end of file diff --git a/web/templates/auth/fallback_sign_in.tmpl b/web/templates/auth/fallback_sign_in.tmpl new file mode 100644 index 0000000..0a486f8 --- /dev/null +++ b/web/templates/auth/fallback_sign_in.tmpl @@ -0,0 +1,13 @@ +{{ define "title" }}fallback sign-in{{ end }} +{{ define "content" }} + +
+
+
+ +
+
+
+{{end}} \ No newline at end of file diff --git a/web/templates/landing/index.tmpl b/web/templates/landing/index.tmpl index 42872fc..3971b31 100644 --- a/web/templates/landing/index.tmpl +++ b/web/templates/landing/index.tmpl @@ -1,9 +1,9 @@ {{ define "title" }}Landing - Index{{ end }} {{ define "content" }}