Remove a bunch of unused cruft
This commit is contained in:
@@ -180,8 +180,6 @@ func main() {
|
||||
internal.WithBaseURL(baseURL),
|
||||
|
||||
// Administration
|
||||
internal.WithAdminUser(adminUser),
|
||||
internal.WithAdminPass(adminPass),
|
||||
internal.WithAdminName(adminName),
|
||||
internal.WithAdminEmail(adminEmail),
|
||||
|
||||
@@ -189,9 +187,7 @@ func main() {
|
||||
internal.WithResultsPerPage(resultsPerPage),
|
||||
|
||||
// Secrets
|
||||
internal.WithAPISigningKey(apiSigningKey),
|
||||
internal.WithCookieSecret(cookieSecret),
|
||||
internal.WithMagicLinkSecret(magiclinkSecret),
|
||||
|
||||
// Email Setitngs
|
||||
internal.WithSMTPHost(smtpHost),
|
||||
@@ -202,8 +198,6 @@ func main() {
|
||||
|
||||
// Timeouts
|
||||
internal.WithSessionExpiry(sessionExpiry),
|
||||
internal.WithSessionCacheTTL(sessionCacheTTL),
|
||||
internal.WithAPISessionTime(apiSessionTime),
|
||||
)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("error creating server")
|
||||
|
||||
4
go.mod
4
go.mod
@@ -17,14 +17,11 @@ require (
|
||||
github.com/apex/log v1.9.0
|
||||
github.com/blevesearch/bleve/v2 v2.0.1
|
||||
github.com/creasty/defaults v1.5.1
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/elithrar/simple-scrypt v1.3.0
|
||||
github.com/gabstv/merger v1.0.1
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20210130063903-47dfef350d96 // indirect
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/go-shiori/go-readability v0.0.0-20201011032228-bdc871772408
|
||||
github.com/goccy/go-yaml v1.8.8
|
||||
github.com/gocolly/colly/v2 v2.1.0
|
||||
github.com/golang/snappy v0.0.2 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20201113031856-722100d81a8e
|
||||
@@ -57,6 +54,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20210201131500-d352d2db2ceb // indirect
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
|
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
|
||||
golang.org/x/text v0.3.5
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||
)
|
||||
|
||||
22
go.sum
22
go.sum
@@ -181,7 +181,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
@@ -191,8 +190,6 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/elithrar/simple-scrypt v1.3.0 h1:KIlOlxdoQf9JWKl5lMAJ28SY2URB0XTRDn2TckyzAZg=
|
||||
github.com/elithrar/simple-scrypt v1.3.0/go.mod h1:U2XQRI95XHY0St410VE3UjT7vuKb1qPwrl/EJwEqnZo=
|
||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -203,8 +200,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@@ -228,14 +223,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM=
|
||||
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-shiori/dom v0.0.0-20201011032054-d6b74a54fe52 h1:wEe9mu6BOmGYT5yQ9ag5E38LHUMUv7/AFx0J8YNR8HI=
|
||||
github.com/go-shiori/dom v0.0.0-20201011032054-d6b74a54fe52/go.mod h1:aLEd5DGjh1qYKnJJ/tC5OL0f3CV4CMcreDOn4RpCmUc=
|
||||
github.com/go-shiori/go-readability v0.0.0-20201011032228-bdc871772408 h1:xq7Sck0bwvgp/WWw6tHFDn3dUTCQwWRWLudr+inH/gs=
|
||||
@@ -245,8 +232,6 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-yaml v1.8.8 h1:MGfRB1GeSn/hWXYWS2Pt67iC2GJNnebdIro01ddyucA=
|
||||
github.com/goccy/go-yaml v1.8.8/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||
github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoGs=
|
||||
@@ -430,8 +415,6 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
@@ -441,14 +424,10 @@ github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA=
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
@@ -876,7 +855,6 @@ golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
158
internal/api.go
158
internal/api.go
@@ -1,158 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"git.mills.io/prologic/spyda/internal/passwords"
|
||||
)
|
||||
|
||||
// ContextKey ...
|
||||
type ContextKey int
|
||||
|
||||
const (
|
||||
TokenContextKey ContextKey = iota
|
||||
UserContextKey
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidCredentials is returned for invalid credentials against /auth
|
||||
ErrInvalidCredentials = errors.New("error: invalid credentials")
|
||||
|
||||
// ErrInvalidToken is returned for expired or invalid tokens used in Authorizeation headers
|
||||
ErrInvalidToken = errors.New("error: invalid token")
|
||||
)
|
||||
|
||||
// API ...
|
||||
type API struct {
|
||||
router *Router
|
||||
config *Config
|
||||
db Store
|
||||
pm passwords.Passwords
|
||||
}
|
||||
|
||||
// NewAPI ...
|
||||
func NewAPI(router *Router, config *Config, db Store, pm passwords.Passwords) *API {
|
||||
api := &API{router, config, db, pm}
|
||||
|
||||
api.initRoutes()
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
func (a *API) initRoutes() {
|
||||
router := a.router.Group("/api/v1")
|
||||
|
||||
router.GET("/ping", a.PingEndpoint())
|
||||
}
|
||||
|
||||
// CreateToken ...
|
||||
func (a *API) CreateToken(user *User, r *http.Request) (*Token, error) {
|
||||
claims := jwt.MapClaims{}
|
||||
claims["username"] = user.Username
|
||||
createdAt := time.Now()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(a.config.APISigningKey))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error creating signed token")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedToken, err := jwt.Parse(tokenString, a.jwtKeyFunc)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error creating signed token")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkn := &Token{
|
||||
Signature: signedToken.Signature,
|
||||
Value: tokenString,
|
||||
UserAgent: r.UserAgent(),
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
func (a *API) jwtKeyFunc(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("There was an error")
|
||||
}
|
||||
return []byte(a.config.APISigningKey), nil
|
||||
}
|
||||
|
||||
func (a *API) getLoggedInUser(r *http.Request) *User {
|
||||
token, err := jwt.Parse(r.Header.Get("Token"), a.jwtKeyFunc)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
|
||||
username := claims["username"].(string)
|
||||
|
||||
user, err := a.db.GetUser(username)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error loading user object")
|
||||
return nil
|
||||
}
|
||||
|
||||
return user
|
||||
|
||||
}
|
||||
|
||||
func (a *API) isAuthorized(endpoint httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
if r.Header.Get("Token") == "" {
|
||||
http.Error(w, "No Token Provided", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(r.Header.Get("Token"), a.jwtKeyFunc)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error parsing token")
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if token.Valid {
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
|
||||
username := claims["username"].(string)
|
||||
|
||||
user, err := a.db.GetUser(username)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error loading user object")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), TokenContextKey, token)
|
||||
ctx = context.WithValue(ctx, UserContextKey, user)
|
||||
|
||||
endpoint(w, r.WithContext(ctx), p)
|
||||
} else {
|
||||
http.Error(w, "Invalid Token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PingEndpoint ...
|
||||
func (a *API) PingEndpoint() httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"git.mills.io/prologic/spyda/internal/session"
|
||||
)
|
||||
|
||||
// Options ...
|
||||
type Options struct {
|
||||
login string
|
||||
register string
|
||||
}
|
||||
|
||||
// NewOptions ...
|
||||
func NewOptions(login, register string) *Options {
|
||||
return &Options{login, register}
|
||||
}
|
||||
|
||||
// Manager ...
|
||||
type Manager struct {
|
||||
options *Options
|
||||
}
|
||||
|
||||
// NewManager ...
|
||||
func NewManager(options *Options) *Manager {
|
||||
return &Manager{options}
|
||||
}
|
||||
|
||||
// MustAuth ...
|
||||
func (m *Manager) MustAuth(next httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
if sess := r.Context().Value(session.SessionKey); sess != nil {
|
||||
if _, ok := sess.(*session.Session).Get("username"); ok {
|
||||
next(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, m.options.login, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldAuth ...
|
||||
func (m *Manager) ShouldAuth(next httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
if sess := r.Context().Value(session.SessionKey); sess != nil {
|
||||
if _, ok := sess.(*session.Session).Get("username"); ok {
|
||||
next(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, m.options.login, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) HasAuth(next httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
if sess := r.Context().Value(session.SessionKey); sess != nil {
|
||||
if _, ok := sess.(*session.Session).Get("username"); ok {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
next(w, r, p)
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"git.mills.io/prologic/spyda/internal/session"
|
||||
)
|
||||
|
||||
// LoginHandler ...
|
||||
func (s *Server) LoginHandler() httprouter.Handle {
|
||||
// #239: Throttle failed login attempts and lock user account.
|
||||
failures := NewTTLCache(5 * time.Minute)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
ctx := NewContext(s.config, s.db, r)
|
||||
|
||||
if r.Method == "GET" {
|
||||
s.render("login", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
username := NormalizeUsername(r.FormValue("username"))
|
||||
password := r.FormValue("password")
|
||||
rememberme := r.FormValue("rememberme") == "on"
|
||||
|
||||
// Error: no username or password provided
|
||||
if username == "" || password == "" {
|
||||
log.Warn("no username or password provided")
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Lookup user
|
||||
user, err := s.db.GetUser(username)
|
||||
if err != nil {
|
||||
ctx.Error = true
|
||||
ctx.Message = "Invalid username! Hint: Register an account?"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// #239: Throttle failed login attempts and lock user account.
|
||||
if failures.Get(user.Username) > MaxFailedLogins {
|
||||
ctx.Error = true
|
||||
ctx.Message = "Too many failed login attempts. Account temporarily locked! Please try again later."
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate cleartext password against KDF hash
|
||||
err = s.pm.CheckPassword(user.Password, password)
|
||||
if err != nil {
|
||||
// #239: Throttle failed login attempts and lock user account.
|
||||
failed := failures.Inc(user.Username)
|
||||
time.Sleep(time.Duration(IntPow(2, failed)) * time.Second)
|
||||
|
||||
ctx.Error = true
|
||||
ctx.Message = "Invalid password! Hint: Reset your password?"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// #239: Throttle failed login attempts and lock user account.
|
||||
failures.Reset(user.Username)
|
||||
|
||||
// Login successful
|
||||
log.Infof("login successful: %s", username)
|
||||
|
||||
// Lookup session
|
||||
sess := r.Context().Value(session.SessionKey)
|
||||
if sess == nil {
|
||||
log.Warn("no session found")
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Authorize session
|
||||
_ = sess.(*session.Session).Set("username", username)
|
||||
|
||||
// Persist session?
|
||||
if rememberme {
|
||||
_ = sess.(*session.Session).Set("persist", "1")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, RedirectRefererURL(r, s.config, "/"), http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// LogoutHandler ...
|
||||
func (s *Server) LogoutHandler() httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
s.sm.Delete(w, r)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,13 @@ package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.mills.io/prologic/bitcask"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"git.mills.io/prologic/spyda/internal/session"
|
||||
)
|
||||
|
||||
const (
|
||||
feedsKeyPrefix = "/feeds"
|
||||
sessionsKeyPrefix = "/sessions"
|
||||
usersKeyPrefix = "/users"
|
||||
urlsKeyPrefix = "/urls"
|
||||
tokensKeyPrefix = "/tokens"
|
||||
)
|
||||
|
||||
// BitcaskStore ...
|
||||
@@ -68,89 +61,6 @@ func (bs *BitcaskStore) Merge() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) HasUser(username string) bool {
|
||||
key := []byte(fmt.Sprintf("%s/%s", usersKeyPrefix, username))
|
||||
return bs.db.Has(key)
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) DelUser(username string) error {
|
||||
key := []byte(fmt.Sprintf("%s/%s", usersKeyPrefix, username))
|
||||
return bs.db.Delete(key)
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) GetUser(username string) (*User, error) {
|
||||
key := []byte(fmt.Sprintf("%s/%s", usersKeyPrefix, username))
|
||||
data, err := bs.db.Get(key)
|
||||
if err == bitcask.ErrKeyNotFound {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return LoadUser(data)
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) SetUser(username string, user *User) error {
|
||||
data, err := user.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := []byte(fmt.Sprintf("%s/%s", usersKeyPrefix, username))
|
||||
if err := bs.db.Put(key, data); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) LenUsers() int64 {
|
||||
var count int64
|
||||
|
||||
if err := bs.db.Scan([]byte(usersKeyPrefix), func(_ []byte) error {
|
||||
count++
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.WithError(err).Error("error scanning")
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) SearchUsers(prefix string) []string {
|
||||
var keys []string
|
||||
|
||||
if err := bs.db.Scan([]byte(usersKeyPrefix), func(key []byte) error {
|
||||
if strings.HasPrefix(strings.ToLower(string(key)), prefix) {
|
||||
keys = append(keys, strings.TrimPrefix(string(key), "/users/"))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.WithError(err).Error("error scanning")
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) GetAllUsers() ([]*User, error) {
|
||||
var users []*User
|
||||
|
||||
err := bs.db.Scan([]byte(usersKeyPrefix), func(key []byte) error {
|
||||
data, err := bs.db.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := LoadUser(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users = append(users, user)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) HasURL(hash string) bool {
|
||||
key := []byte(fmt.Sprintf("%s/%s", urlsKeyPrefix, hash))
|
||||
return bs.db.Has(key)
|
||||
@@ -216,145 +126,3 @@ func (bs *BitcaskStore) ForEachURL(f func(url *URL) error) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) GetSession(sid string) (*session.Session, error) {
|
||||
key := []byte(fmt.Sprintf("%s/%s", sessionsKeyPrefix, sid))
|
||||
data, err := bs.db.Get(key)
|
||||
if err != nil {
|
||||
if err == bitcask.ErrKeyNotFound {
|
||||
return nil, session.ErrSessionNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
sess := session.NewSession(bs)
|
||||
if err := session.LoadSession(data, sess); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) SetSession(sid string, sess *session.Session) error {
|
||||
key := []byte(fmt.Sprintf("%s/%s", sessionsKeyPrefix, sid))
|
||||
|
||||
data, err := sess.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bs.db.Put(key, data)
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) HasSession(sid string) bool {
|
||||
key := []byte(fmt.Sprintf("%s/%s", sessionsKeyPrefix, sid))
|
||||
return bs.db.Has(key)
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) DelSession(sid string) error {
|
||||
key := []byte(fmt.Sprintf("%s/%s", sessionsKeyPrefix, sid))
|
||||
return bs.db.Delete(key)
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) SyncSession(sess *session.Session) error {
|
||||
// Only persist sessions with a logged in user associated with an account
|
||||
// This saves resources as we don't need to keep session keys around for
|
||||
// sessions we may never load from the store again.
|
||||
if sess.Has("username") {
|
||||
return bs.SetSession(sess.ID, sess)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) LenSessions() int64 {
|
||||
var count int64
|
||||
|
||||
if err := bs.db.Scan([]byte(sessionsKeyPrefix), func(_ []byte) error {
|
||||
count++
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.WithError(err).Error("error scanning")
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) GetAllSessions() ([]*session.Session, error) {
|
||||
var sessions []*session.Session
|
||||
|
||||
err := bs.db.Scan([]byte(sessionsKeyPrefix), func(key []byte) error {
|
||||
data, err := bs.db.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := session.NewSession(bs)
|
||||
if err := session.LoadSession(data, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
sessions = append(sessions, sess)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) GetUserTokens(user *User) ([]*Token, error) {
|
||||
tokens := []*Token{}
|
||||
for _, signature := range user.Tokens {
|
||||
tkn, err := bs.GetToken(signature)
|
||||
if err != nil {
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
tokens = append(tokens, tkn)
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) GetToken(signature string) (*Token, error) {
|
||||
key := []byte(fmt.Sprintf("%s/%s", tokensKeyPrefix, signature))
|
||||
data, err := bs.db.Get(key)
|
||||
if err == bitcask.ErrKeyNotFound {
|
||||
return nil, ErrTokenNotFound
|
||||
}
|
||||
tkn, err := LoadToken(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) SetToken(signature string, tkn *Token) error {
|
||||
data, err := tkn.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := []byte(fmt.Sprintf("%s/%s", tokensKeyPrefix, signature))
|
||||
if err := bs.db.Put(key, data); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) DelToken(signature string) error {
|
||||
key := []byte(fmt.Sprintf("%s/%s", tokensKeyPrefix, signature))
|
||||
return bs.db.Delete(key)
|
||||
}
|
||||
|
||||
func (bs *BitcaskStore) LenTokens() int64 {
|
||||
var count int64
|
||||
|
||||
if err := bs.db.Scan([]byte(tokensKeyPrefix), func(_ []byte) error {
|
||||
count++
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.WithError(err).Error("error scanning")
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -3,15 +3,12 @@ package internal
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gabstv/merger"
|
||||
"github.com/goccy/go-yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -35,21 +32,15 @@ type Config struct {
|
||||
Store string
|
||||
Theme string
|
||||
BaseURL string
|
||||
AdminUser string
|
||||
AdminPass string
|
||||
AdminName string
|
||||
AdminEmail string
|
||||
|
||||
SearchPrompts []string
|
||||
ResultsPerPage int
|
||||
|
||||
APISessionTime time.Duration
|
||||
SessionExpiry time.Duration
|
||||
SessionCacheTTL time.Duration
|
||||
|
||||
APISigningKey string
|
||||
CookieSecret string
|
||||
MagicLinkSecret string
|
||||
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
@@ -94,55 +85,8 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
|
||||
if c.CookieSecret == InvalidConfigValue {
|
||||
return fmt.Errorf("error: COOKIE_SECRET is not configured!")
|
||||
}
|
||||
|
||||
if c.MagicLinkSecret == InvalidConfigValue {
|
||||
return fmt.Errorf("error: MAGICLINK_SECRET is not configured!")
|
||||
}
|
||||
|
||||
if c.APISigningKey == InvalidConfigValue {
|
||||
return fmt.Errorf("error: API_SIGNING_KEY is not configured!")
|
||||
return fmt.Errorf("error: COOKIE_SECRET is not configured")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadSettings loads pod settings from the given path
|
||||
func LoadSettings(path string) (*Settings, error) {
|
||||
var settings Settings
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &settings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// Save saves the pod settings to the given path
|
||||
func (s *Settings) Save(path string) error {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := yaml.MarshalWithOptions(s, yaml.Indent(4))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = f.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = f.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vcraescu/go-paginator"
|
||||
|
||||
"github.com/justinas/nosurf"
|
||||
|
||||
"git.mills.io/prologic/spyda"
|
||||
"git.mills.io/prologic/spyda/internal/session"
|
||||
)
|
||||
|
||||
type Meta struct {
|
||||
@@ -92,34 +90,5 @@ func NewContext(conf *Config, db Store, req *http.Request) *Context {
|
||||
|
||||
ctx.CSRFToken = nosurf.Token(req)
|
||||
|
||||
if sess := req.Context().Value(session.SessionKey); sess != nil {
|
||||
if username, ok := sess.(*session.Session).Get("username"); ok {
|
||||
ctx.Authenticated = true
|
||||
ctx.Username = username
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Authenticated && ctx.Username != "" {
|
||||
user, err := db.GetUser(ctx.Username)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("error loading user object for %s", ctx.Username)
|
||||
}
|
||||
|
||||
ctx.User = user
|
||||
|
||||
tokens, err := db.GetUserTokens(user)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("error loading tokens for %s", ctx.Username)
|
||||
}
|
||||
ctx.Tokens = tokens
|
||||
|
||||
} else {
|
||||
ctx.User = &User{}
|
||||
}
|
||||
|
||||
if ctx.Username == conf.AdminUser {
|
||||
ctx.IsAdmin = true
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -29,13 +29,7 @@ func NewCrawler(conf *Config, tasks *Dispatcher, db Store, indexer Indexer) (Cra
|
||||
}
|
||||
|
||||
func (c *crawler) loop() {
|
||||
for {
|
||||
select {
|
||||
case url, ok := <-c.queue:
|
||||
if !ok {
|
||||
log.Debugf("crawler shutting down...")
|
||||
return
|
||||
}
|
||||
for url := range c.queue {
|
||||
log.Debugf("crawling %s", url)
|
||||
uuid, err := c.tasks.Dispatch(NewCrawlTask(c.conf, c.db, c.indexer, url))
|
||||
if err != nil {
|
||||
@@ -45,7 +39,6 @@ func (c *crawler) loop() {
|
||||
log.WithField("uuid", uuid).Infof("successfully created crawl task for %s: %s", url, taskURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *crawler) Crawl(url string) error {
|
||||
|
||||
@@ -14,24 +14,9 @@ import (
|
||||
var (
|
||||
ErrSendingEmail = errors.New("error: unable to send email")
|
||||
|
||||
passwordResetEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .Username }},
|
||||
supportRequestEmailTemplate = template.Must(template.New("email").Parse(`Hello,
|
||||
|
||||
You have requested to have your password on {{ .Pod }} reset for your account.
|
||||
|
||||
**IMPORTANT:** If this was __NOT__ initiated by you, please ignore this email and contract support!
|
||||
|
||||
To reset your password, please visit the following link:
|
||||
|
||||
{{ .BaseURL}}/newPassword?token={{ .Token }}
|
||||
|
||||
Kind regards,
|
||||
|
||||
{{ .Pod}} Support
|
||||
`))
|
||||
|
||||
supportRequestEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .AdminUser }},
|
||||
|
||||
{{ .Name }} <{{ .Email }} from {{ .Pod }} has sent the following support request:
|
||||
{{ .Name }} <{{ .Email }} from {{ .Instance }} has sent the following support request:
|
||||
|
||||
> Subject: {{ .Subject }}
|
||||
>
|
||||
@@ -39,21 +24,12 @@ Kind regards,
|
||||
|
||||
Kind regards,
|
||||
|
||||
{{ .Pod}} Support
|
||||
{{ .Instance }} Support
|
||||
`))
|
||||
)
|
||||
|
||||
type PasswordResetEmailContext struct {
|
||||
Pod string
|
||||
BaseURL string
|
||||
|
||||
Token string
|
||||
Username string
|
||||
}
|
||||
|
||||
type SupportRequestEmailContext struct {
|
||||
Pod string
|
||||
AdminUser string
|
||||
Instance string
|
||||
|
||||
Name string
|
||||
Email string
|
||||
@@ -96,34 +72,6 @@ func SendEmail(conf *Config, recipients []string, replyTo, subject string, body
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendPasswordResetEmail(conf *Config, user *User, email, token string) error {
|
||||
recipients := []string{email}
|
||||
subject := fmt.Sprintf(
|
||||
"[%s]: Password Reset Request for %s",
|
||||
conf.Name, user.Username,
|
||||
)
|
||||
ctx := PasswordResetEmailContext{
|
||||
Pod: conf.Name,
|
||||
BaseURL: conf.BaseURL,
|
||||
|
||||
Token: token,
|
||||
Username: user.Username,
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := passwordResetEmailTemplate.Execute(buf, ctx); err != nil {
|
||||
log.WithError(err).Error("error rendering email template")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := SendEmail(conf, recipients, conf.SMTPFrom, subject, buf.String()); err != nil {
|
||||
log.WithError(err).Errorf("error sending new token to %s", recipients[0])
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendSupportRequestEmail(conf *Config, name, email, subject, message string) error {
|
||||
recipients := []string{conf.AdminEmail, email}
|
||||
emailSubject := fmt.Sprintf(
|
||||
@@ -131,8 +79,7 @@ func SendSupportRequestEmail(conf *Config, name, email, subject, message string)
|
||||
conf.Name, subject,
|
||||
)
|
||||
ctx := SupportRequestEmailContext{
|
||||
Pod: conf.Name,
|
||||
AdminUser: conf.AdminUser,
|
||||
Instance: conf.Name,
|
||||
|
||||
Name: name,
|
||||
Email: email,
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -99,7 +99,7 @@ func (s *Server) CacheHandler() httprouter.Handle {
|
||||
|
||||
var entry Entry
|
||||
|
||||
data, err := ioutil.ReadFile(fn)
|
||||
data, err := os.ReadFile(fn)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error reading cached entry")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/renstrom/shortuuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ManageHandler ...
|
||||
func (s *Server) ManageHandler() httprouter.Handle {
|
||||
isAdminUser := IsAdminUserFactory(s.config)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
ctx := NewContext(s.config, s.db, r)
|
||||
|
||||
if !isAdminUser(ctx.User) {
|
||||
ctx.Error = true
|
||||
ctx.Message = "You are not a Pod Owner!"
|
||||
s.render("403", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
s.render("managePod", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(r.FormValue("podName"))
|
||||
description := strings.TrimSpace(r.FormValue("podDescription"))
|
||||
|
||||
// Update name
|
||||
if name != "" {
|
||||
s.config.Name = name
|
||||
} else {
|
||||
ctx.Error = true
|
||||
ctx.Message = ""
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Update pod description
|
||||
if description != "" {
|
||||
s.config.Description = description
|
||||
} else {
|
||||
ctx.Error = true
|
||||
ctx.Message = ""
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Save config file
|
||||
if err := s.config.Settings().Save(filepath.Join(s.config.Data, "settings.yaml")); err != nil {
|
||||
log.WithError(err).Error("error saving config")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx.Error = false
|
||||
ctx.Message = "Pod updated successfully"
|
||||
s.render("error", w, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// ManageUsersHandler ...
|
||||
func (s *Server) ManageUsersHandler() httprouter.Handle {
|
||||
isAdminUser := IsAdminUserFactory(s.config)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
ctx := NewContext(s.config, s.db, r)
|
||||
|
||||
if !isAdminUser(ctx.User) {
|
||||
ctx.Error = true
|
||||
ctx.Message = "You are not a Pod Owner!"
|
||||
s.render("403", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
s.render("manageUsers", w, ctx)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// AddUserHandler ...
|
||||
func (s *Server) AddUserHandler() httprouter.Handle {
|
||||
isAdminUser := IsAdminUserFactory(s.config)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
ctx := NewContext(s.config, s.db, r)
|
||||
|
||||
if !isAdminUser(ctx.User) {
|
||||
ctx.Error = true
|
||||
ctx.Message = "You are not a Pod Owner!"
|
||||
s.render("403", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
username := NormalizeUsername(r.FormValue("username"))
|
||||
// XXX: We DO NOT store this! (EVER)
|
||||
email := strings.TrimSpace(r.FormValue("email"))
|
||||
|
||||
// Random password -- User is expected to user "Password Reset"
|
||||
password := shortuuid.New()
|
||||
|
||||
if err := ValidateUsername(username); err != nil {
|
||||
ctx.Error = true
|
||||
ctx.Message = fmt.Sprintf("Username validation failed: %s", err.Error())
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if s.db.HasUser(username) {
|
||||
ctx.Error = true
|
||||
ctx.Message = "User or Feed with that name already exists! Please pick another!"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := s.pm.CreatePassword(password)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error creating password hash")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
recoveryHash := fmt.Sprintf("email:%s", FastHash(email))
|
||||
|
||||
user := NewUser()
|
||||
user.Username = username
|
||||
user.Recovery = recoveryHash
|
||||
user.Password = hash
|
||||
user.CreatedAt = time.Now()
|
||||
|
||||
if err := s.db.SetUser(username, user); err != nil {
|
||||
log.WithError(err).Error("error saving user object for new user")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Error = false
|
||||
ctx.Message = "User successfully created"
|
||||
s.render("error", w, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// DelUserHandler ...
|
||||
func (s *Server) DelUserHandler() httprouter.Handle {
|
||||
isAdminUser := IsAdminUserFactory(s.config)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
ctx := NewContext(s.config, s.db, r)
|
||||
|
||||
if !isAdminUser(ctx.User) {
|
||||
ctx.Error = true
|
||||
ctx.Message = "You are not a Pod Owner!"
|
||||
s.render("403", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
username := NormalizeUsername(r.FormValue("username"))
|
||||
|
||||
user, err := s.db.GetUser(username)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("error loading user object for %s", username)
|
||||
ctx.Error = true
|
||||
ctx.Message = "Error deleting account"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete user
|
||||
if err := s.db.DelUser(user.Username); err != nil {
|
||||
ctx.Error = true
|
||||
ctx.Message = "An error occured whilst deleting your account"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
s.sm.Delete(w, r)
|
||||
|
||||
ctx.Error = false
|
||||
ctx.Message = "Successfully deleted account"
|
||||
s.render("error", w, ctx)
|
||||
}
|
||||
}
|
||||
@@ -91,11 +91,8 @@ func NewConfig() *Config {
|
||||
Store: DefaultStore,
|
||||
Theme: DefaultTheme,
|
||||
BaseURL: DefaultBaseURL,
|
||||
AdminUser: DefaultAdminUser,
|
||||
AdminPass: DefaultAdminPass,
|
||||
|
||||
CookieSecret: DefaultCookieSecret,
|
||||
MagicLinkSecret: DefaultMagicLinkSecret,
|
||||
|
||||
SearchPrompts: DefaultSearchPrompts,
|
||||
ResultsPerPage: DefaultResultsPerPage,
|
||||
@@ -149,22 +146,6 @@ func WithBaseURL(baseURL string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdminUser sets the Admin username
|
||||
func WithAdminUser(adminUser string) Option {
|
||||
return func(cfg *Config) error {
|
||||
cfg.AdminUser = adminUser
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdminPass sets the Admin password
|
||||
func WithAdminPass(adminPass string) Option {
|
||||
return func(cfg *Config) error {
|
||||
cfg.AdminPass = adminPass
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdminName sets the Admin name used to identify the pod operator
|
||||
func WithAdminName(adminName string) Option {
|
||||
return func(cfg *Config) error {
|
||||
@@ -221,14 +202,6 @@ func WithResultsPerPage(resultsPerPage int) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithSessionCacheTTL sets the server's session cache ttl
|
||||
func WithSessionCacheTTL(cacheTTL time.Duration) Option {
|
||||
return func(cfg *Config) error {
|
||||
cfg.SessionCacheTTL = cacheTTL
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSessionExpiry sets the server's session expiry time
|
||||
func WithSessionExpiry(expiry time.Duration) Option {
|
||||
return func(cfg *Config) error {
|
||||
@@ -237,14 +210,6 @@ func WithSessionExpiry(expiry time.Duration) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithMagicLinkSecret sets the MagicLinkSecert used to create password reset tokens
|
||||
func WithMagicLinkSecret(secret string) Option {
|
||||
return func(cfg *Config) error {
|
||||
cfg.MagicLinkSecret = secret
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSMTPHost sets the SMTPHost to use for sending email
|
||||
func WithSMTPHost(host string) Option {
|
||||
return func(cfg *Config) error {
|
||||
@@ -284,19 +249,3 @@ func WithSMTPFrom(from string) Option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAPISessionTime sets the API session time for tokens
|
||||
func WithAPISessionTime(duration time.Duration) Option {
|
||||
return func(cfg *Config) error {
|
||||
cfg.APISessionTime = duration
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAPISigningKey sets the API JWT signing key for tokens
|
||||
func WithAPISigningKey(key string) Option {
|
||||
return func(cfg *Config) error {
|
||||
cfg.APISigningKey = key
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
@@ -20,6 +19,8 @@ import (
|
||||
"github.com/julienschmidt/httprouter"
|
||||
sync "github.com/sasha-s/go-deadlock"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
const pagesDir = "pages"
|
||||
@@ -104,6 +105,8 @@ func (s *Server) PageHandler(name string) httprouter.Handle {
|
||||
return page, nil
|
||||
}
|
||||
|
||||
caser := cases.Title(language.English)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
ctx := NewContext(s.config, s.db, r)
|
||||
|
||||
@@ -157,7 +160,7 @@ func (s *Server) PageHandler(name string) httprouter.Handle {
|
||||
if frontmatter.Title != "" {
|
||||
title = frontmatter.Title
|
||||
} else {
|
||||
title = strings.Title(name)
|
||||
title = caser.String(name)
|
||||
}
|
||||
ctx.Title = title
|
||||
ctx.Meta.Description = frontmatter.Description
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ResetPasswordHandler ...
|
||||
func (s *Server) ResetPasswordHandler() httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
ctx := NewContext(s.config, s.db, r)
|
||||
|
||||
if r.Method == "GET" {
|
||||
ctx.Title = "Reset password"
|
||||
s.render("resetPassword", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
username := NormalizeUsername(r.FormValue("username"))
|
||||
email := strings.TrimSpace(r.FormValue("email"))
|
||||
recovery := fmt.Sprintf("email:%s", FastHash(email))
|
||||
|
||||
// Check if user exist
|
||||
if !s.db.HasUser(username) {
|
||||
ctx.Error = true
|
||||
ctx.Message = "User not found!"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user object from DB
|
||||
user, err := s.db.GetUser(username)
|
||||
if err != nil {
|
||||
ctx.Error = true
|
||||
ctx.Message = "Error loading user"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if recovery != user.Recovery {
|
||||
ctx.Error = true
|
||||
ctx.Message = "Error! The email address you supplied does not match what you registered with :/"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Create magic link expiry time
|
||||
now := time.Now()
|
||||
secs := now.Unix()
|
||||
expiresAfterSeconds := int64(600) // Link expires after 10 minutes
|
||||
|
||||
expiryTime := secs + expiresAfterSeconds
|
||||
|
||||
// Create magic link
|
||||
token := jwt.NewWithClaims(
|
||||
jwt.SigningMethodHS256,
|
||||
jwt.MapClaims{"username": username, "expiresAt": expiryTime},
|
||||
)
|
||||
tokenString, err := token.SignedString([]byte(s.config.MagicLinkSecret))
|
||||
if err != nil {
|
||||
ctx.Error = true
|
||||
ctx.Message = err.Error()
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if err := SendPasswordResetEmail(s.config, user, email, tokenString); err != nil {
|
||||
log.WithError(err).Errorf("unable to send reset password email to %s", user.Username)
|
||||
ctx.Error = true
|
||||
ctx.Message = err.Error()
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("reset password email sent for %s", user.Username)
|
||||
|
||||
// Show success msg
|
||||
ctx.Error = false
|
||||
ctx.Message = "Password request request sent! Please check your email and follow the instructions"
|
||||
s.render("error", w, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// ResetPasswordMagicLinkHandler ...
|
||||
func (s *Server) ResetPasswordMagicLinkHandler() httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
ctx := NewContext(s.config, s.db, r)
|
||||
|
||||
// Get token from query string
|
||||
tokens, ok := r.URL.Query()["token"]
|
||||
|
||||
// Check if valid token
|
||||
if !ok || len(tokens[0]) < 1 {
|
||||
ctx.Error = true
|
||||
ctx.Message = "Invalid token"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
tokenEmail := tokens[0]
|
||||
ctx.PasswordResetToken = tokenEmail
|
||||
|
||||
// Show newPassword page
|
||||
s.render("newPassword", w, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// NewPasswordHandler ...
|
||||
func (s *Server) NewPasswordHandler() httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
ctx := NewContext(s.config, s.db, r)
|
||||
|
||||
if r.Method == "GET" {
|
||||
return
|
||||
}
|
||||
|
||||
password := r.FormValue("password")
|
||||
tokenEmail := r.FormValue("token")
|
||||
|
||||
// Check if token is valid
|
||||
token, err := jwt.Parse(tokenEmail, func(token *jwt.Token) (interface{}, error) {
|
||||
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(s.config.MagicLinkSecret), nil
|
||||
})
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
|
||||
var username = fmt.Sprintf("%v", claims["username"])
|
||||
var expiresAt int = int(claims["expiresAt"].(float64))
|
||||
|
||||
now := time.Now()
|
||||
secs := now.Unix()
|
||||
|
||||
// Check token expiry
|
||||
if secs > int64(expiresAt) {
|
||||
ctx.Error = true
|
||||
ctx.Message = "Token expires"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.db.GetUser(username)
|
||||
if err != nil {
|
||||
ctx.Error = true
|
||||
ctx.Message = "Error loading user"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Reset password
|
||||
if password != "" {
|
||||
hash, err := s.pm.CreatePassword(password)
|
||||
if err != nil {
|
||||
ctx.Error = true
|
||||
ctx.Message = "Error loading user"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = hash
|
||||
|
||||
// Save user
|
||||
if err := s.db.SetUser(username, user); err != nil {
|
||||
ctx.Error = true
|
||||
ctx.Message = "Error loading user"
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("password changed: %v", user)
|
||||
|
||||
// Show success msg
|
||||
ctx.Error = false
|
||||
ctx.Message = "Password reset successfully."
|
||||
s.render("error", w, ctx)
|
||||
} else {
|
||||
ctx.Error = true
|
||||
ctx.Message = err.Error()
|
||||
s.render("error", w, ctx)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package passwords
|
||||
|
||||
// Passwords is an interface for creating and verifying secure passwords
|
||||
// An implementation must implement all methods and it is up to the impl
|
||||
// which underlying crypto to use for hasing cleartext passwrods.
|
||||
type Passwords interface {
|
||||
CreatePassword(password string) (string, error)
|
||||
CheckPassword(hash, password string) error
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package passwords
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
scrypt "github.com/elithrar/simple-scrypt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultMaxTimeout default max timeout in ms
|
||||
DefaultMaxTimeout = 500 * time.Millisecond
|
||||
|
||||
// DefaultMaxMemory default max memory in MB
|
||||
DefaultMaxMemory = 64
|
||||
)
|
||||
|
||||
// Options ...
|
||||
type Options struct {
|
||||
maxTimeout time.Duration
|
||||
maxMemory int
|
||||
}
|
||||
|
||||
// NewOptions ...
|
||||
func NewOptions(maxTimeout time.Duration, maxMemory int) *Options {
|
||||
return &Options{maxTimeout, maxMemory}
|
||||
}
|
||||
|
||||
// ScryptPasswords ...
|
||||
type ScryptPasswords struct {
|
||||
options *Options
|
||||
params scrypt.Params
|
||||
}
|
||||
|
||||
// NewScryptPasswords ...
|
||||
func NewScryptPasswords(options *Options) Passwords {
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
|
||||
if options.maxTimeout == 0 {
|
||||
options.maxTimeout = DefaultMaxTimeout
|
||||
}
|
||||
if options.maxMemory == 0 {
|
||||
options.maxMemory = DefaultMaxMemory
|
||||
}
|
||||
|
||||
log.Info("Calibrating scrypt parameters ...")
|
||||
params, err := scrypt.Calibrate(
|
||||
options.maxTimeout,
|
||||
options.maxMemory,
|
||||
scrypt.DefaultParams,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("error calibrating scrypt params: %s", err)
|
||||
}
|
||||
|
||||
log.WithField("params", params).Info("scrypt params")
|
||||
|
||||
return &ScryptPasswords{options, params}
|
||||
}
|
||||
|
||||
// CreatePassword ...
|
||||
func (sp *ScryptPasswords) CreatePassword(password string) (string, error) {
|
||||
hash, err := scrypt.GenerateFromPassword([]byte(password), sp.params)
|
||||
return string(hash), err
|
||||
}
|
||||
|
||||
// CheckPassword ...
|
||||
func (sp *ScryptPasswords) CheckPassword(hash, password string) error {
|
||||
return scrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -67,7 +66,7 @@ func Scrape(conf *Config, url string) (*Entry, error) {
|
||||
return nil, fmt.Errorf("error serializing entry: %s", err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(fn, data, 0644); err != nil {
|
||||
if err := os.WriteFile(fn, data, 0644); err != nil {
|
||||
log.WithError(err).Error("error persisting entry")
|
||||
return nil, fmt.Errorf("error persisting entry: %w", err)
|
||||
}
|
||||
|
||||
@@ -6,21 +6,17 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.mills.io/prologic/observe"
|
||||
"github.com/NYTimes/gziphandler"
|
||||
"github.com/gabstv/merger"
|
||||
"github.com/justinas/nosurf"
|
||||
"github.com/robfig/cron"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/unrolled/logger"
|
||||
|
||||
"git.mills.io/prologic/spyda"
|
||||
"git.mills.io/prologic/spyda/internal/auth"
|
||||
"git.mills.io/prologic/spyda/internal/passwords"
|
||||
"git.mills.io/prologic/spyda/internal/session"
|
||||
"git.mills.io/prologic/spyda/internal/static"
|
||||
)
|
||||
@@ -56,18 +52,9 @@ type Server struct {
|
||||
// Dispatcher
|
||||
tasks *Dispatcher
|
||||
|
||||
// Auth
|
||||
am *auth.Manager
|
||||
|
||||
// Sessions
|
||||
sc *SessionStore
|
||||
sc session.Store
|
||||
sm *session.Manager
|
||||
|
||||
// API
|
||||
api *API
|
||||
|
||||
// Passwords
|
||||
pm passwords.Passwords
|
||||
}
|
||||
|
||||
func (s *Server) render(name string, w http.ResponseWriter, ctx *Context) {
|
||||
@@ -164,37 +151,6 @@ func (s *Server) setupMetrics() {
|
||||
},
|
||||
)
|
||||
|
||||
// sessions
|
||||
metrics.NewGaugeFunc(
|
||||
"server", "sessions",
|
||||
"Number of active in-memory sessions (non-persistent)",
|
||||
func() float64 {
|
||||
return float64(s.sc.Count())
|
||||
},
|
||||
)
|
||||
|
||||
// database keys
|
||||
metrics.NewGaugeFunc(
|
||||
"db", "sessions",
|
||||
"Number of database /sessions keys",
|
||||
func() float64 {
|
||||
return float64(s.db.LenSessions())
|
||||
},
|
||||
)
|
||||
metrics.NewGaugeFunc(
|
||||
"db", "users",
|
||||
"Number of database /users keys",
|
||||
func() float64 {
|
||||
return float64(s.db.LenUsers())
|
||||
},
|
||||
)
|
||||
metrics.NewGaugeFunc(
|
||||
"db", "tokens",
|
||||
"Number of database /tokens keys",
|
||||
func() float64 {
|
||||
return float64(s.db.LenTokens())
|
||||
},
|
||||
)
|
||||
metrics.NewGaugeFunc(
|
||||
"db", "urls",
|
||||
"Number of database /urls keys",
|
||||
@@ -311,18 +267,6 @@ func (s *Server) initRoutes() {
|
||||
s.router.GET("/cache/:hash", s.CacheHandler())
|
||||
s.router.HEAD("/cache/:hash", s.CacheHandler())
|
||||
|
||||
s.router.GET("/login", s.am.HasAuth(s.LoginHandler()))
|
||||
s.router.POST("/login", s.LoginHandler())
|
||||
|
||||
s.router.GET("/logout", s.LogoutHandler())
|
||||
s.router.POST("/logout", s.LogoutHandler())
|
||||
|
||||
// Reset Password
|
||||
s.router.GET("/pwreset", s.ResetPasswordHandler())
|
||||
s.router.POST("/pwreset", s.ResetPasswordHandler())
|
||||
s.router.GET("/chpasswd", s.ResetPasswordMagicLinkHandler())
|
||||
s.router.POST("/chpasswd", s.NewPasswordHandler())
|
||||
|
||||
// Task State
|
||||
s.router.GET("/tasks", s.TasksHandler())
|
||||
s.router.GET("/task/:uuid", s.TaskHandler())
|
||||
@@ -330,13 +274,6 @@ func (s *Server) initRoutes() {
|
||||
s.router.GET("/add", s.AddHandler())
|
||||
s.router.POST("/add", s.AddHandler())
|
||||
|
||||
s.router.GET("/manage", s.ManageHandler())
|
||||
s.router.POST("/manage", s.ManageHandler())
|
||||
|
||||
s.router.GET("/manage/users", s.ManageUsersHandler())
|
||||
s.router.POST("/manage/adduser", s.AddUserHandler())
|
||||
s.router.POST("/manage/deluser", s.DelUserHandler())
|
||||
|
||||
// Support
|
||||
s.router.GET("/support", s.SupportHandler())
|
||||
s.router.POST("/support", s.SupportHandler())
|
||||
@@ -353,16 +290,6 @@ func NewServer(bind string, options ...Option) (*Server, error) {
|
||||
}
|
||||
}
|
||||
|
||||
settings, err := LoadSettings(filepath.Join(config.Data, "settings.yaml"))
|
||||
if err != nil {
|
||||
log.Warnf("error loading pod settings: %s", err)
|
||||
} else {
|
||||
if err := merger.MergeOverwrite(config, settings); err != nil {
|
||||
log.WithError(err).Error("error merging pod settings")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
log.WithError(err).Error("error validating config")
|
||||
return nil, fmt.Errorf("error validating config: %w", err)
|
||||
@@ -387,14 +314,9 @@ func NewServer(bind string, options ...Option) (*Server, error) {
|
||||
|
||||
router := NewRouter()
|
||||
|
||||
am := auth.NewManager(auth.NewOptions("/login", "/register"))
|
||||
|
||||
tasks := NewDispatcher(2, 10) // TODO: Make this configurable?
|
||||
|
||||
pm := passwords.NewScryptPasswords(nil)
|
||||
|
||||
sc := NewSessionStore(db, config.SessionCacheTTL)
|
||||
|
||||
sc := session.NewMemoryStore(config.SessionExpiry)
|
||||
sm := session.NewManager(
|
||||
session.NewOptions(
|
||||
config.Name,
|
||||
@@ -417,8 +339,6 @@ func NewServer(bind string, options ...Option) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := NewAPI(router, config, db, pm)
|
||||
|
||||
csrfHandler := nosurf.New(router)
|
||||
csrfHandler.ExemptGlob("/api/v1/*")
|
||||
|
||||
@@ -440,9 +360,6 @@ func NewServer(bind string, options ...Option) (*Server, error) {
|
||||
),
|
||||
},
|
||||
|
||||
// API
|
||||
api: api,
|
||||
|
||||
// Indexer
|
||||
indexer: indexer,
|
||||
|
||||
@@ -458,15 +375,9 @@ func NewServer(bind string, options ...Option) (*Server, error) {
|
||||
// Dispatcher
|
||||
tasks: tasks,
|
||||
|
||||
// Auth Manager
|
||||
am: am,
|
||||
|
||||
// Session Manager
|
||||
sc: sc,
|
||||
sm: sm,
|
||||
|
||||
// Password Manager
|
||||
pm: pm,
|
||||
}
|
||||
|
||||
if err := server.setupCronJobs(); err != nil {
|
||||
@@ -488,17 +399,14 @@ func NewServer(bind string, options ...Option) (*Server, error) {
|
||||
// Log interesting configuration options
|
||||
log.Infof("Instance Name: %s", server.config.Name)
|
||||
log.Infof("Base URL: %s", server.config.BaseURL)
|
||||
log.Infof("Admin User: %s", server.config.AdminUser)
|
||||
log.Infof("Admin Name: %s", server.config.AdminName)
|
||||
log.Infof("Admin Email: %s", server.config.AdminEmail)
|
||||
log.Infof("SMTP Host: %s", server.config.SMTPHost)
|
||||
log.Infof("SMTP Port: %d", server.config.SMTPPort)
|
||||
log.Infof("SMTP User: %s", server.config.SMTPUser)
|
||||
log.Infof("SMTP From: %s", server.config.SMTPFrom)
|
||||
log.Infof("API Session Time: %s", server.config.APISessionTime)
|
||||
|
||||
server.initRoutes()
|
||||
api.initRoutes()
|
||||
|
||||
go server.runStartupJobs()
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"git.mills.io/prologic/spyda/internal/session"
|
||||
)
|
||||
|
||||
// SessionStore ...
|
||||
type SessionStore struct {
|
||||
store Store
|
||||
cached *cache.Cache
|
||||
}
|
||||
|
||||
func NewSessionStore(store Store, sessionCacheTTL time.Duration) *SessionStore {
|
||||
return &SessionStore{
|
||||
store: store,
|
||||
cached: cache.New(sessionCacheTTL, time.Minute*5),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SessionStore) Count() int {
|
||||
return s.cached.ItemCount()
|
||||
}
|
||||
|
||||
func (s *SessionStore) GetSession(sid string) (*session.Session, error) {
|
||||
val, found := s.cached.Get(sid)
|
||||
if found {
|
||||
return val.(*session.Session), nil
|
||||
}
|
||||
|
||||
return s.store.GetSession(sid)
|
||||
}
|
||||
|
||||
func (s *SessionStore) SetSession(sid string, sess *session.Session) error {
|
||||
s.cached.Set(sid, sess, cache.DefaultExpiration)
|
||||
if persist, ok := sess.Get("persist"); !ok || persist != "1" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.store.SetSession(sid, sess)
|
||||
}
|
||||
|
||||
func (s *SessionStore) HasSession(sid string) bool {
|
||||
_, ok := s.cached.Get(sid)
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return s.store.HasSession(sid)
|
||||
}
|
||||
|
||||
func (s *SessionStore) DelSession(sid string) error {
|
||||
if s.store.HasSession(sid) {
|
||||
if err := s.store.DelSession(sid); err != nil {
|
||||
log.WithError(err).Errorf("error deleting persistent session %s", sid)
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.cached.Delete(sid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionStore) SyncSession(sess *session.Session) error {
|
||||
if persist, ok := sess.Get("persist"); ok && persist == "1" {
|
||||
if err := s.store.SetSession(sess.ID, sess); err != nil {
|
||||
log.WithError(err).Errorf("error persisting session %s", sess.ID)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.SetSession(sess.ID, sess)
|
||||
}
|
||||
|
||||
func (s *SessionStore) GetAllSessions() ([]*session.Session, error) {
|
||||
var sessions []*session.Session
|
||||
for _, item := range s.cached.Items() {
|
||||
sess := item.Object.(*session.Session)
|
||||
sessions = append(sessions, sess)
|
||||
}
|
||||
persistedSessions, err := s.store.GetAllSessions()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error getting all persisted sessions")
|
||||
return sessions, err
|
||||
}
|
||||
return append(sessions, persistedSessions...), nil
|
||||
}
|
||||
@@ -3,16 +3,11 @@ package internal
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.mills.io/prologic/spyda/internal/session"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidStore = errors.New("error: invalid store")
|
||||
ErrUserNotFound = errors.New("error: user not found")
|
||||
ErrTokenNotFound = errors.New("error: token not found")
|
||||
ErrURLNotFound = errors.New("error: url not found")
|
||||
ErrInvalidSession = errors.New("error: invalid session")
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
@@ -20,33 +15,12 @@ type Store interface {
|
||||
Close() error
|
||||
Sync() error
|
||||
|
||||
DelUser(username string) error
|
||||
HasUser(username string) bool
|
||||
GetUser(username string) (*User, error)
|
||||
SetUser(username string, user *User) error
|
||||
LenUsers() int64
|
||||
SearchUsers(prefix string) []string
|
||||
GetAllUsers() ([]*User, error)
|
||||
|
||||
DelURL(hash string) error
|
||||
HasURL(hash string) bool
|
||||
GetURL(hash string) (*URL, error)
|
||||
SetURL(hash string, url *URL) error
|
||||
URLCount() int64
|
||||
ForEachURL(f func(*URL) error) error
|
||||
|
||||
GetSession(sid string) (*session.Session, error)
|
||||
SetSession(sid string, sess *session.Session) error
|
||||
HasSession(sid string) bool
|
||||
DelSession(sid string) error
|
||||
SyncSession(sess *session.Session) error
|
||||
LenSessions() int64
|
||||
GetAllSessions() ([]*session.Session, error)
|
||||
|
||||
GetUserTokens(user *User) ([]*Token, error)
|
||||
SetToken(signature string, token *Token) error
|
||||
DelToken(signature string) error
|
||||
LenTokens() int64
|
||||
}
|
||||
|
||||
func NewStore(store string) (Store, error) {
|
||||
|
||||
@@ -42,7 +42,6 @@ func NewTemplateManager(conf *Config) (*TemplateManager, error) {
|
||||
funcMap["prettyURL"] = PrettyURL
|
||||
funcMap["isLocalURL"] = IsLocalURLFactory(conf)
|
||||
funcMap["formatForDateTime"] = FormatForDateTime
|
||||
funcMap["isAdminUser"] = IsAdminUserFactory(conf)
|
||||
|
||||
m := &TemplateManager{debug: conf.Debug, templates: templates, funcMap: funcMap}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
{{define "content"}}
|
||||
<article>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>Sign in</h2>
|
||||
<p>Login to your Spyda account on {{ .InstanceName }}</p>
|
||||
</hgroup>
|
||||
<form action="/login" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
|
||||
<input type="text" name="username" placeholder="Username" aria-label="Username" autocomplete="nickname" required="required" autofocus />
|
||||
<input type="password" name="password" placeholder="Password" aria-label="Password" autocomplete="current-password" required="required" />
|
||||
<fieldset>
|
||||
<label for="rememberme">
|
||||
<input type="checkbox" id="rememberme" name="rememberme">
|
||||
Remember me?
|
||||
</label>
|
||||
</fieldset>
|
||||
<input type="submit" class="contrast" value="Login" />
|
||||
<p>
|
||||
<a href="/resetPassword">Forgotten your password?</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<hgroup>
|
||||
<h2>How to login to your account</h2>
|
||||
</hgroup>
|
||||
<p>
|
||||
Login to your Spyda account on {{ .InstanceName }} by filling in
|
||||
the Username and Password you used when you created your account.
|
||||
</p>
|
||||
<p>
|
||||
Check the "Remember Me" box if you don't want to have to keep logging in
|
||||
every few hours.
|
||||
</p>
|
||||
<p>
|
||||
If you have forgotten your password you can request a
|
||||
<a href="/resetPassword">Password Reset</a> as long as you remember
|
||||
your username and email address you signed up with and retain access to
|
||||
your email (<i>We <b>NEVER</b> store your email address!</i>).
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
@@ -357,14 +357,6 @@ func PrettyURL(uri string) string {
|
||||
return fmt.Sprintf("%s/%s", u.Hostname(), strings.TrimPrefix(u.EscapedPath(), "/"))
|
||||
}
|
||||
|
||||
// IsAdminUserFactory returns a function that returns true if the user provided
|
||||
// is the configured pod administrator, false otherwise.
|
||||
func IsAdminUserFactory(conf *Config) func(user *User) bool {
|
||||
return func(user *User) bool {
|
||||
return NormalizeUsername(conf.AdminUser) == NormalizeUsername(user.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func URLForPage(baseURL, page string) string {
|
||||
return fmt.Sprintf(
|
||||
"%s/%s",
|
||||
|
||||
Reference in New Issue
Block a user