1
0
mirror of https://github.com/volatiletech/authboss.git synced 2024-11-28 08:58:38 +02:00

Re(move) swaths of code

- Document more things
- Remove module code
- Remove callbacks code
- Remove data makers, flash messages, and context providers in exchange
  for middlewares that use context (unwritten)
- Move more implementations (responses, redirector, router) to defaults
  package
- Rename key interfaces (again), Storer -> User, StoreLoader ->
  ServerStorer (opposite of ClientStateStorer) if this is the last time
  I rename these I'll be shocked
This commit is contained in:
Aaron L 2018-01-31 17:07:11 -08:00
parent 59b2874bcd
commit b33e47a97c
18 changed files with 684 additions and 814 deletions

View File

@ -6,60 +6,47 @@ races without having to think about how to store passwords or remember tokens.
*/
package authboss
import (
"fmt"
"net/http"
"github.com/pkg/errors"
)
import "github.com/pkg/errors"
// Authboss contains a configuration and other details for running.
type Authboss struct {
Config
Callbacks *Callbacks
loadedModules map[string]bool
loadedModules map[string]Modularizer
mux *http.ServeMux
templateNames []string
renderer Renderer
viewRenderer Renderer
mailRenderer Renderer
}
// New makes a new instance of authboss with a default
// configuration.
func New() *Authboss {
ab := &Authboss{
Callbacks: NewCallbacks(),
loadedModules: make(map[string]Modularizer),
}
ab := &Authboss{}
ab.Config.Defaults()
return ab
}
// Init authboss and the requested modules. modulesToLoad is left empty
// all registered modules will be loaded.
func (a *Authboss) Init(modulesToLoad ...string) error {
if len(modulesToLoad) == 0 {
modulesToLoad = RegisteredModules()
}
// Init authboss, modules, renderers
func (a *Authboss) Init() error {
//TODO(aarondl): Figure the template names out along with new "module" loading.
views := []string{"all"}
for _, name := range modulesToLoad {
fmt.Fprintf(a.LogWriter, "%-10s loading\n", "["+name+"]")
if err := a.loadModule(name); err != nil {
return errors.Wrapf(err, "[%s] error initializing", name)
}
}
renderer, err := a.ViewLoader.Init(a.templateNames)
var err error
a.viewRenderer, err = a.Config.ViewLoader.Init(views)
if err != nil {
return errors.Wrap(err, "failed to init view loader")
return errors.Wrap(err, "failed to load the view renderer")
}
a.mailRenderer, err = a.Config.MailViewLoader.Init(views)
if err != nil {
return errors.Wrap(err, "failed to load the mail view renderer")
}
a.renderer = renderer
return nil
}
/*
TODO(aarondl): Fixup
UpdatePassword should be called to recalculate hashes and do any cleanup
that should occur on password resets. Updater should return an error if the
update to the user failed (for reasons say like validation, duplicate
@ -78,7 +65,6 @@ will be returned.
The error returned is returned either from the updater if that produced an error
or from the cleanup routines.
*/
func (a *Authboss) UpdatePassword(w http.ResponseWriter, r *http.Request,
ptPassword string, user Storer, updater func() error) error {
@ -101,7 +87,9 @@ func (a *Authboss) UpdatePassword(w http.ResponseWriter, r *http.Request,
return nil
}
return a.Callbacks.FireAfter(EventPasswordReset, r.Context())*/
return a.Callbacks.FireAfter(EventPasswordReset, r.Context())
// TODO(aarondl): Fix
return errors.New("not implemented")
}
*/

View File

@ -32,6 +32,7 @@ const (
// ClientStateEventKind is an enum.
type ClientStateEventKind int
// ClientStateEvent kinds
const (
ClientStateEventPut ClientStateEventKind = iota
ClientStateEventDel
@ -71,7 +72,7 @@ type ClientState interface {
Get(key string) (string, bool)
}
// clientStateResponseWriter is used to write out the client state at the last
// ClientStateResponseWriter is used to write out the client state at the last
// moment before the response code is written.
type ClientStateResponseWriter struct {
ab *Authboss
@ -83,7 +84,8 @@ type ClientStateResponseWriter struct {
cookieStateEvents []ClientStateEvent
}
func (a *Authboss) NewResponse(w http.ResponseWriter, r *http.Request) http.ResponseWriter {
// NewResponse wraps the ResponseWriter with a ClientStateResponseWriter
func (a *Authboss) NewResponse(w http.ResponseWriter, r *http.Request) *ClientStateResponseWriter {
return &ClientStateResponseWriter{
ab: a,
ResponseWriter: w,
@ -91,6 +93,7 @@ func (a *Authboss) NewResponse(w http.ResponseWriter, r *http.Request) http.Resp
}
}
// LoadClientState loads the state from sessions and cookies into the request context
func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*http.Request, error) {
if a.SessionStateStorer != nil {
state, err := a.SessionStateStorer.ReadState(w, r)

View File

@ -1,9 +1,7 @@
package authboss
import (
"context"
"io"
"net/http"
"time"
)
@ -27,22 +25,11 @@ type Config struct {
// MailViewLoader loads the templates for mail. If this is nil, it will
// fall back to using the Renderer created from the ViewLoader instead.
MailViewLoader RenderLoader
// LayoutDataMaker is a function that can provide authboss with the layout's
// template data. It will be merged with the data being provided for the current
// view in order to render the templates.
LayoutDataMaker ViewDataMaker
// OAuth2Providers lists all providers that can be used. See
// OAuthProvider documentation for more details.
OAuth2Providers map[string]OAuth2Provider
// ErrorHandler handles would be 500 errors.
ErrorHandler http.Handler
// BadRequestHandler handles would be 400 errors.
BadRequestHandler http.Handler
// NotFoundHandler handles would be 404 errors.
NotFoundHandler http.Handler
// AuthLoginOKPath is the redirect path after a successful authentication.
AuthLoginOKPath string
// AuthLoginFailPath is the redirect path after a failed authentication.
@ -86,13 +73,9 @@ type Config struct {
// email subjects.
EmailSubjectPrefix string
// XSRFName is the name of the xsrf token to put in the hidden form fields.
XSRFName string
// XSRFMaker is a function that returns an xsrf token for the current non-POST request.
XSRFMaker XSRF
// Storer is the interface through which Authboss accesses the web apps database.
StoreLoader StoreLoader
// Storer is the interface through which Authboss accesses the web apps database
// for user operations.
Storer ServerStorer
// CookieStateStorer must be defined to provide an interface capapable of
// storing cookies for the given response, and reading them from the request.
@ -108,9 +91,6 @@ type Config struct {
// Mailer is the mailer being used to send e-mails out. Authboss defines two loggers for use
// LogMailer and SMTPMailer, the default is a LogMailer to io.Discard.
Mailer Mailer
// ContextProvider provides a context for a given request
ContextProvider func(*http.Request) context.Context
}
// Defaults sets the configuration's default values.

View File

@ -13,6 +13,11 @@ const (
ctxKeySessionState contextKey = "session"
ctxKeyCookieState contextKey = "cookie"
// CTXKeyData is a context key for the accumulating
// map[string]interface{} (authboss.HTMLData) to pass to the
// renderer
CTXKeyData contextKey = "data"
)
func (c contextKey) String() string {
@ -25,11 +30,6 @@ func (a *Authboss) CurrentUserID(w http.ResponseWriter, r *http.Request) (string
return pid.(string), nil
}
_, err := a.Callbacks.FireBefore(EventGetUserSession, r.Context())
if err != nil {
return "", err
}
pid, _ := GetSession(r, SessionKey)
return pid, nil
}
@ -48,9 +48,9 @@ func (a *Authboss) CurrentUserIDP(w http.ResponseWriter, r *http.Request) string
}
// CurrentUser retrieves the current user from the session and the database.
func (a *Authboss) CurrentUser(w http.ResponseWriter, r *http.Request) (Storer, error) {
func (a *Authboss) CurrentUser(w http.ResponseWriter, r *http.Request) (User, error) {
if user := r.Context().Value(ctxKeyUser); user != nil {
return user.(Storer), nil
return user.(User), nil
}
pid, err := a.CurrentUserID(w, r)
@ -65,7 +65,7 @@ func (a *Authboss) CurrentUser(w http.ResponseWriter, r *http.Request) (Storer,
// CurrentUserP retrieves the current user but panics if it's not available for
// any reason.
func (a *Authboss) CurrentUserP(w http.ResponseWriter, r *http.Request) Storer {
func (a *Authboss) CurrentUserP(w http.ResponseWriter, r *http.Request) User {
i, err := a.CurrentUser(w, r)
if err != nil {
panic(err)
@ -75,19 +75,8 @@ func (a *Authboss) CurrentUserP(w http.ResponseWriter, r *http.Request) Storer {
return i
}
func (a *Authboss) currentUser(ctx context.Context, pid string) (Storer, error) {
_, err := a.Callbacks.FireBefore(EventGetUser, ctx)
if err != nil {
return nil, err
}
user, err := a.StoreLoader.Load(ctx, pid)
if err != nil {
return nil, err
}
ctx = context.WithValue(ctx, ctxKeyUser, user)
err = a.Callbacks.FireAfter(EventGetUser, ctx)
func (a *Authboss) currentUser(ctx context.Context, pid string) (User, error) {
user, err := a.Storer.Load(ctx, pid)
if err != nil {
return nil, err
}
@ -95,7 +84,7 @@ func (a *Authboss) currentUser(ctx context.Context, pid string) (Storer, error)
return user, nil
}
// LoadCurrentUser takes a pointer to a pointer to the request in order to
// LoadCurrentUserID takes a pointer to a pointer to the request in order to
// change the current method's request pointer itself to the new request that
// contains the new context that has the pid in it.
func (a *Authboss) LoadCurrentUserID(w http.ResponseWriter, r **http.Request) (string, error) {
@ -118,6 +107,7 @@ func (a *Authboss) LoadCurrentUserID(w http.ResponseWriter, r **http.Request) (s
return pid, nil
}
// LoadCurrentUserIDP loads the current user id and panics if it's not found
func (a *Authboss) LoadCurrentUserIDP(w http.ResponseWriter, r **http.Request) string {
pid, err := a.LoadCurrentUserID(w, r)
if err != nil {
@ -133,9 +123,9 @@ func (a *Authboss) LoadCurrentUserIDP(w http.ResponseWriter, r **http.Request) s
// change the current method's request pointer itself to the new request that
// contains the new context that has the user in it. Calls LoadCurrentUserID
// so the primary id is also put in the context.
func (a *Authboss) LoadCurrentUser(w http.ResponseWriter, r **http.Request) (Storer, error) {
func (a *Authboss) LoadCurrentUser(w http.ResponseWriter, r **http.Request) (User, error) {
if user := (*r).Context().Value(ctxKeyUser); user != nil {
return user.(Storer), nil
return user.(User), nil
}
pid, err := a.LoadCurrentUserID(w, r)
@ -158,7 +148,9 @@ func (a *Authboss) LoadCurrentUser(w http.ResponseWriter, r **http.Request) (Sto
return user, nil
}
func (a *Authboss) LoadCurrentUserP(w http.ResponseWriter, r **http.Request) Storer {
// LoadCurrentUserP does the same as LoadCurrentUser but panics if
// the current user is not found.
func (a *Authboss) LoadCurrentUserP(w http.ResponseWriter, r **http.Request) User {
user, err := a.LoadCurrentUser(w, r)
if err != nil {
panic(err)

8
defaults/doc.go Normal file
View File

@ -0,0 +1,8 @@
// Package defaults houses default implementations for the very many
// interfaces that authboss has. It's a goal of the defaults package
// to provide the core where authboss implements the shell.
//
// It's simultaneously supposed to be possible to take as many or
// as few of these implementations as you desire, allowing you to
// reimplement where necessary, but reuse where possible.
package defaults

140
defaults/responder.go Normal file
View File

@ -0,0 +1,140 @@
package defaults
import (
"net/http"
"github.com/volatiletech/authboss"
)
// Responder helps respond to http requests
type Responder struct {
// CRSFHandler creates csrf tokens for inclusion on rendered forms
CSRFMaker CSRFMaker
// CRSFName is the name of the field that will include the token
CSRFName string
Renderer authboss.Renderer
}
// CSRFMaker returns an opaque string when handed a request and response
// to be included in the data as a
type CSRFMaker func(w http.ResponseWriter, r *http.Request) string
// Respond to an HTTP request. Renders templates, flash messages, does CSRF
// and writes the headers out.
func (r *Responder) Respond(w http.ResponseWriter, req *http.Request, code int, templateName string, data authboss.HTMLData) error {
data.MergeKV(
r.CSRFName, r.CSRFMaker(w, req),
)
/*
TODO(aarondl): Add middlewares for accumulating eventual view data using contexts
if a.LayoutDataMaker != nil {
data.Merge(a.LayoutDataMaker(w, req))
}
flashSuccess := authboss.FlashSuccess(w, req)
flashError := authboss.FlashError(w, req)
if len(flashSuccess) != 0 {
data.MergeKV(authboss.FlashSuccessKey, flashSuccess)
}
if len(flashError) != 0 {
data.MergeKV(authboss.FlashErrorKey, flashError)
}
*/
rendered, mime, err := r.Renderer.Render(req.Context(), templateName, data)
if err != nil {
return err
}
w.Header().Set("Content-Type", mime)
w.WriteHeader(code)
_, err = w.Write(rendered)
return err
}
func isAPIRequest(r *http.Request) bool {
return r.Header.Get("Content-Type") == "application/json"
}
// Redirector for http requests
type Redirector struct {
// FormValueName for the redirection
FormValueName string
Renderer authboss.Renderer
}
// Redirect the client elsewhere. If it's an API request it will simply render
// a JSON response with information that should help a client to decide what
// to do.
func (r *Redirector) Redirect(w http.ResponseWriter, req *http.Request, ro authboss.RedirectOptions) error {
var redirectFunction = r.redirectNonAPI
if isAPIRequest(req) {
redirectFunction = r.redirectAPI
}
return redirectFunction(w, req, ro)
}
func (r Redirector) redirectAPI(w http.ResponseWriter, req *http.Request, ro authboss.RedirectOptions) error {
path := ro.RedirectPath
redir := req.FormValue(r.FormValueName)
if len(redir) != 0 && ro.FollowRedirParam {
path = redir
}
var status, message string
if len(ro.Success) != 0 {
status = "success"
message = ro.Success
}
if len(ro.Failure) != 0 {
status = "failure"
message = ro.Failure
}
data := authboss.HTMLData{
"location": path,
}
if len(status) != 0 {
data["status"] = status
data["message"] = message
}
body, mime, err := r.Renderer.Render(req.Context(), "redirect", data)
if err != nil {
return err
}
if len(body) != 0 {
w.Header().Set("Content-Type", mime)
}
if ro.Code != 0 {
w.WriteHeader(ro.Code)
}
_, err = w.Write(body)
return err
}
func (r Redirector) redirectNonAPI(w http.ResponseWriter, req *http.Request, ro authboss.RedirectOptions) error {
path := ro.RedirectPath
redir := req.FormValue(r.FormValueName)
if len(redir) != 0 && ro.FollowRedirParam {
path = redir
}
if len(ro.Success) != 0 {
authboss.PutSession(w, authboss.FlashSuccessKey, ro.Success)
}
if len(ro.Failure) != 0 {
authboss.PutSession(w, authboss.FlashErrorKey, ro.Failure)
}
http.Redirect(w, req, path, http.StatusFound)
return nil
}

242
defaults/responder_test.go Normal file
View File

@ -0,0 +1,242 @@
package defaults
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/volatiletech/authboss"
"github.com/volatiletech/authboss/internal/mocks"
)
type testRenderer struct {
Callback func(context.Context, string, authboss.HTMLData) ([]byte, string, error)
}
func (t testRenderer) Render(ctx context.Context, name string, data authboss.HTMLData) ([]byte, string, error) {
return t.Callback(ctx, name, data)
}
func TestResponder(t *testing.T) {
t.Parallel()
renderer := testRenderer{
Callback: func(ctx context.Context, name string, data authboss.HTMLData) ([]byte, string, error) {
return nil, "", nil
},
}
responder := Responder{
Renderer: renderer,
CSRFName: "csrf",
CSRFMaker: func(w http.ResponseWriter, r *http.Request) string { return "csrftoken" },
}
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
err := responder.Respond(w, r, http.StatusCreated, "some_template.tpl", authboss.HTMLData{"auth_happy": true})
if err != nil {
t.Error(err)
}
if w.Code != http.StatusCreated {
t.Error("code was wrong:", w.Code)
}
if got := w.HeaderMap.Get("Content-Type"); got != "application/json" {
t.Error("content type was wrong:", got)
}
expectData := authboss.HTMLData{
"csrfName": "xsrf",
"csrfToken": "xsrftoken",
"hello": "world",
"auth_happy": true,
}
var gotData authboss.HTMLData
if err := json.Unmarshal(w.Body.Bytes(), &gotData); err != nil {
t.Error(err)
}
if !reflect.DeepEqual(gotData, expectData) {
t.Errorf("data mismatched:\nwant: %#v\ngot: %#v", expectData, gotData)
}
}
func TestRedirector(t *testing.T) {
t.Parallel()
renderer := testRenderer{
Callback: func(ctx context.Context, name string, data authboss.HTMLData) ([]byte, string, error) {
return nil, "", nil
},
}
redir := Redirector{
FormValueName: "redir",
Renderer: renderer,
}
r := httptest.NewRequest("POST", "/?redir=/pow", nil)
w := httptest.NewRecorder()
r.Header.Set("Content-Type", "application/json")
ro := authboss.RedirectOptions{
Success: "ok!",
Code: http.StatusTeapot,
RedirectPath: "/redirect", FollowRedirParam: false,
}
if err := redir.Redirect(w, r, ro); err != nil {
t.Error(err)
}
if w.Code != http.StatusTeapot {
t.Error("code is wrong:", w.Code)
}
var gotData map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &gotData); err != nil {
t.Error(err)
}
if got := gotData["status"]; got != "success" {
t.Error("status was wrong:", got)
}
if got := gotData["message"]; got != "ok!" {
t.Error("message was wrong:", got)
}
if got := gotData["location"]; got != "/redirect" {
t.Error("location was wrong:", got)
}
}
func TestResponseRedirectAPIFollowRedir(t *testing.T) {
t.Parallel()
renderer := testRenderer{
Callback: func(ctx context.Context, name string, data authboss.HTMLData) ([]byte, string, error) {
return nil, "", nil
},
}
redir := Redirector{
FormValueName: "redir",
Renderer: renderer,
}
r := httptest.NewRequest("POST", "/?redir=/pow", nil)
w := httptest.NewRecorder()
r.Header.Set("Content-Type", "application/json")
ro := authboss.RedirectOptions{
Failure: ":(",
Code: http.StatusTeapot,
RedirectPath: "/redirect", FollowRedirParam: true,
}
if err := redir.Redirect(w, r, ro); err != nil {
t.Error(err)
}
if w.Code != http.StatusTeapot {
t.Error("code is wrong:", w.Code)
}
var gotData map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &gotData); err != nil {
t.Error(err)
}
if got := gotData["status"]; got != "failure" {
t.Error("status was wrong:", got)
}
if got := gotData["message"]; got != ":(" {
t.Error("message was wrong:", got)
}
if got := gotData["location"]; got != "/pow" {
t.Error("location was wrong:", got)
}
}
func TestResponseRedirectNonAPI(t *testing.T) {
t.Parallel()
renderer := testRenderer{
Callback: func(ctx context.Context, name string, data authboss.HTMLData) ([]byte, string, error) {
return nil, "", nil
},
}
redir := Redirector{
FormValueName: "redir",
Renderer: renderer,
}
r := httptest.NewRequest("POST", "/?redir=/pow", nil)
w := httptest.NewRecorder()
ab := authboss.New()
ab.Config.SessionStateStorer = mocks.NewClientRW()
ab.Config.CookieStateStorer = mocks.NewClientRW()
aw := ab.NewResponse(w, r)
ro := authboss.RedirectOptions{
Success: "success", Failure: "failure",
RedirectPath: "/redirect", FollowRedirParam: false,
}
if err := redir.Redirect(aw, r, ro); err != nil {
t.Error(err)
}
if w.Code != http.StatusFound {
t.Error("code is wrong:", w.Code)
}
if got := w.Header().Get("Location"); got != "/redirect" {
t.Error("redirect location was wrong:", got)
}
}
func TestResponseRedirectNonAPIFollowRedir(t *testing.T) {
t.Parallel()
renderer := testRenderer{
Callback: func(ctx context.Context, name string, data authboss.HTMLData) ([]byte, string, error) {
return nil, "", nil
},
}
redir := Redirector{
FormValueName: "redir",
Renderer: renderer,
}
r := httptest.NewRequest("POST", "/?redir=/pow", nil)
w := httptest.NewRecorder()
ab := authboss.New()
ab.Config.SessionStateStorer = mocks.NewClientRW()
ab.Config.CookieStateStorer = mocks.NewClientRW()
aw := ab.NewResponse(w, r)
ro := authboss.RedirectOptions{
RedirectPath: "/redirect", FollowRedirParam: true,
}
if err := redir.Redirect(aw, r, ro); err != nil {
t.Error(err)
}
if w.Code != http.StatusFound {
t.Error("code is wrong:", w.Code)
}
if got := w.Header().Get("Location"); got != "/pow" {
t.Error("redirect location was wrong:", got)
}
}

65
defaults/router.go Normal file
View File

@ -0,0 +1,65 @@
package defaults
import (
"io"
"net/http"
)
// Router implementation
// Does not use a dynamic map to hope to be slightly more performant
type Router struct {
gets *http.ServeMux
posts *http.ServeMux
deletes *http.ServeMux
}
// NewRouter creates a new router
func NewRouter() *Router {
r := &Router{
gets: http.NewServeMux(),
posts: http.NewServeMux(),
deletes: http.NewServeMux(),
}
// Nothing gets handled at the root of the authboss router
r.gets.Handle("/", http.NotFoundHandler())
r.posts.Handle("/", http.NotFoundHandler())
r.deletes.Handle("/", http.NotFoundHandler())
return r
}
// Get method route
func (r *Router) Get(path string, handler http.Handler) {
r.gets.Handle(path, handler)
}
// Post method route
func (r *Router) Post(path string, handler http.Handler) {
r.posts.Handle(path, handler)
}
// Delete method route
func (r *Router) Delete(path string, handler http.Handler) {
r.deletes.Handle(path, handler)
}
// ServeHTTP for http.Handler
// Only does get/posts, all other request types are a bad request
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var router http.Handler
switch req.Method {
case "GET":
router = r.gets
case "POST":
router = r.posts
case "DELETE":
router = r.deletes
default:
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, "bad request, this method not allowed")
return
}
router.ServeHTTP(w, req)
}

65
defaults/router_test.go Normal file
View File

@ -0,0 +1,65 @@
package defaults
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
func TestRouter(t *testing.T) {
t.Parallel()
r := NewRouter()
var get, post, delete string
wantGet, wantPost, wantDelete := "testget", "testpost", "testdelete"
r.Get("/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
get = string(b)
}))
r.Post("/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
post = string(b)
}))
r.Delete("/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
delete = string(b)
}))
if get != wantGet {
t.Error("want:", wantGet, "got:", get)
}
if post != wantPost {
t.Error("want:", wantPost, "got:", post)
}
if delete != wantDelete {
t.Error("want:", wantDelete, "got:", delete)
}
}
func TestRouterBadMethod(t *testing.T) {
t.Parallel()
r := NewRouter()
wr := httptest.NewRecorder()
req := httptest.NewRequest("OPTIONS", "/", nil)
r.ServeHTTP(wr, req)
if wr.Code != http.StatusBadRequest {
t.Error("want bad request code, got:", wr.Code)
}
}

View File

@ -103,15 +103,15 @@ func (m *User) SetOAuthExpiry(ctx context.Context, oAuthExpiry time.Time) error
return nil
}
// StoreLoader should be valid for any module storer defined in authboss.
type StoreLoader struct {
// ServerStorer should be valid for any module storer defined in authboss.
type ServerStorer struct {
Users map[string]*User
RMTokens map[string][]string
}
// NewStoreLoader constructor
func NewStoreLoader() *StoreLoader {
return &StoreLoader{
// NewServerStorer constructor
func NewServerStorer() *ServerStorer {
return &ServerStorer{
Users: make(map[string]*User),
RMTokens: make(map[string][]string),
}
@ -220,15 +220,15 @@ func (FailStorer) Load(context.Context) error {
return errors.New("fail storer: get")
}
// ClientStorer is used for testing the client stores on context
type ClientStorer struct {
// ClientRW is used for testing the client stores on context
type ClientState struct {
Values map[string]string
GetShouldFail bool
}
// NewClientStorer constructs a ClientStorer
func NewClientStorer(data ...string) *ClientStorer {
if len(data)%2 != 0 {
// NewClientState constructs a ClientStorer
func NewClientState(data ...string) *ClientState {
if len(data) != 0 && len(data)%2 != 0 {
panic("It should be a key value list of arguments.")
}
@ -238,11 +238,11 @@ func NewClientStorer(data ...string) *ClientStorer {
values[data[i]] = data[i+1]
}
return &ClientStorer{Values: values}
return &ClientState{Values: values}
}
// Get a key's value
func (m *ClientStorer) Get(key string) (string, bool) {
func (m *ClientState) Get(key string) (string, bool) {
if m.GetShouldFail {
return "", false
}
@ -251,24 +251,35 @@ func (m *ClientStorer) Get(key string) (string, bool) {
return v, ok
}
// GetErr gets a key's value or err if not exist
func (m *ClientStorer) GetErr(key string) (string, error) {
if m.GetShouldFail {
return "", authboss.ClientDataErr{Name: key}
}
v, ok := m.Values[key]
if !ok {
return v, authboss.ClientDataErr{Name: key}
}
return v, nil
}
// Put a value
func (m *ClientStorer) Put(key, val string) { m.Values[key] = val }
func (m *ClientState) Put(key, val string) { m.Values[key] = val }
// Del a key/value pair
func (m *ClientStorer) Del(key string) { delete(m.Values, key) }
func (m *ClientState) Del(key string) { delete(m.Values, key) }
// ClientStateRW stores things that would originally
// go in a session, or a map, in memory!
type ClientStateRW struct {
ClientValues map[string]string
}
// NewClientRW takes the data from a client state
// and returns.
func NewClientRW() *ClientStateRW {
return &ClientStateRW{
ClientValues: make(map[string]string),
}
}
// ReadState from memory
func (c *ClientStateRW) ReadState(http.ResponseWriter, *http.Request) (authboss.ClientState, error) {
return &ClientState{Values: c.ClientValues}, nil
}
// WriteState to memory
func (c *ClientStateRW) WriteState(w http.ResponseWriter, cstate authboss.ClientState, cse []authboss.ClientStateEvent) error {
return nil
}
// Request returns a new request with optional key-value body (form-post)
func Request(method string, postKeyValues ...string) *http.Request {

View File

@ -1,79 +0,0 @@
package authboss
import "reflect"
var registeredModules = make(map[string]Modularizer)
// Modularizer should be implemented by all the authboss modules.
type Modularizer interface {
Initialize(*Authboss) error
Routes() RouteTable
Templates() []string
}
// RegisterModule with the core providing all the necessary information to
// integrate into authboss.
func RegisterModule(name string, m Modularizer) {
registeredModules[name] = m
}
// RegisteredModules returns a list of modules that are currently registered.
func RegisteredModules() []string {
mods := make([]string, len(registeredModules))
i := 0
for k := range registeredModules {
mods[i] = k
i++
}
return mods
}
// loadModule loads a particular module. It uses reflection to create a new
// instance of the module type. The original value is copied, but not deep copied
// so care should be taken to make sure most initialization happens inside the Initialize()
// method of the module.
func (a *Authboss) loadModule(name string) error {
module, ok := registeredModules[name]
if !ok {
panic("Could not find module: " + name)
}
var wasPtr bool
modVal := reflect.ValueOf(module)
if modVal.Kind() == reflect.Ptr {
wasPtr = true
modVal = modVal.Elem()
}
modType := modVal.Type()
value := reflect.New(modType)
if !wasPtr {
value = value.Elem()
value.Set(modVal)
} else {
value.Elem().Set(modVal)
}
mod, ok := value.Interface().(Modularizer)
a.loadedModules[name] = mod
a.templateNames = append(a.templateNames, mod.Templates()...)
return mod.Initialize(a)
}
// LoadedModules returns a list of modules that are currently loaded.
func (a *Authboss) LoadedModules() []string {
mods := make([]string, len(a.loadedModules))
i := 0
for k := range a.loadedModules {
mods[i] = k
i++
}
return mods
}
// IsLoaded checks if a specific module is loaded.
func (a *Authboss) IsLoaded(mod string) bool {
_, ok := a.loadedModules[mod]
return ok
}

View File

@ -1,71 +0,0 @@
package authboss
import (
"io/ioutil"
"net/http"
"testing"
)
const testModName = "testmodule"
func init() {
RegisterModule(testModName, testMod)
}
type testModule struct {
r RouteTable
}
var testMod = &testModule{
r: RouteTable{
"/testroute": testHandler,
},
}
func testHandler(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("testhandler", "test")
return nil
}
func (t *testModule) Initialize(a *Authboss) error { return nil }
func (t *testModule) Routes() RouteTable { return t.r }
func (t *testModule) Templates() []string { return []string{"template1.tpl"} }
func TestRegister(t *testing.T) {
// RegisterModule called by init()
if _, ok := registeredModules[testModName]; !ok {
t.Error("Expected module to be saved.")
}
}
func TestLoadedModules(t *testing.T) {
// RegisterModule called by init()
registered := RegisteredModules()
if len(registered) != 2 { // There is another test module loaded from router
t.Error("Expected only a single module to be loaded.")
} else {
found := false
for _, name := range registered {
if name == testModName {
found = true
break
}
}
if !found {
t.Error("It should have found the module:", registered)
}
}
}
func TestIsLoaded(t *testing.T) {
ab := New()
ab.LogWriter = ioutil.Discard
ab.ViewLoader = mockRenderLoader{}
if err := ab.Init(testModName); err != nil {
t.Error(err)
}
if loaded := ab.LoadedModules(); len(loaded) == 0 || loaded[0] != testModName {
t.Error("Loaded modules wrong:", loaded)
}
}

View File

@ -6,69 +6,6 @@ import (
"github.com/pkg/errors"
)
// Respond to an HTTP request. Renders templates, flash messages, does XSRF
// and writes the headers out.
func (a *Authboss) Respond(w http.ResponseWriter, r *http.Request, code int, templateName string, data HTMLData) error {
data.MergeKV(
"xsrfName", a.XSRFName,
"xsrfToken", a.XSRFMaker(w, r),
)
if a.LayoutDataMaker != nil {
data.Merge(a.LayoutDataMaker(w, r))
}
flashSuccess := FlashSuccess(w, r)
flashError := FlashError(w, r)
if len(flashSuccess) != 0 {
data.MergeKV(FlashSuccessKey, flashSuccess)
}
if len(flashError) != 0 {
data.MergeKV(FlashErrorKey, flashError)
}
rendered, mime, err := a.renderer.Render(r.Context(), templateName, data)
if err != nil {
return err
}
w.Header().Set("Content-Type", mime)
w.WriteHeader(code)
_, err = w.Write(rendered)
return err
}
// EmailResponseOptions controls how e-mails are rendered and sent
type EmailResponseOptions struct {
Data HTMLData
HTMLTemplate string
TextTemplate string
}
// Email renders the e-mail templates and sends it using the mailer.
func (a *Authboss) Email(w http.ResponseWriter, r *http.Request, email Email, ro EmailResponseOptions) error {
ctx := r.Context()
if len(ro.HTMLTemplate) != 0 {
htmlBody, _, err := a.renderer.Render(ctx, ro.HTMLTemplate, ro.Data)
if err != nil {
return errors.Wrap(err, "failed to render e-mail html body")
}
email.HTMLBody = string(htmlBody)
}
if len(ro.TextTemplate) != 0 {
textBody, _, err := a.renderer.Render(ctx, ro.TextTemplate, ro.Data)
if err != nil {
return errors.Wrap(err, "failed to render e-mail text body")
}
email.TextBody = string(textBody)
}
return a.Mailer.Send(ctx, email)
}
// RedirectOptions packages up all the pieces a module needs to write out a
// response.
type RedirectOptions struct {
@ -92,74 +29,51 @@ type RedirectOptions struct {
FollowRedirParam bool
}
// Redirect the client elsewhere. If it's an API request it will simply render
// a JSON response with information that should help a client to decide what
// to do.
func (a *Authboss) Redirect(w http.ResponseWriter, r *http.Request, ro RedirectOptions) error {
var redirectFunction = a.redirectNonAPI
if isAPIRequest(r) {
redirectFunction = a.redirectAPI
}
return redirectFunction(w, r, ro)
// EmailResponseOptions controls how e-mails are rendered and sent
type EmailResponseOptions struct {
Data HTMLData
HTMLTemplate string
TextTemplate string
}
func (a *Authboss) redirectAPI(w http.ResponseWriter, r *http.Request, ro RedirectOptions) error {
path := ro.RedirectPath
redir := r.FormValue(FormValueRedirect)
if len(redir) != 0 && ro.FollowRedirParam {
path = redir
}
var status, message string
if len(ro.Success) != 0 {
status = "success"
message = ro.Success
}
if len(ro.Failure) != 0 {
status = "failure"
message = ro.Failure
}
data := HTMLData{
"location": path,
}
if len(status) != 0 {
data["status"] = status
data["message"] = message
}
body, mime, err := a.renderer.Render(r.Context(), "redirect", data)
if err != nil {
return err
}
if len(body) != 0 {
w.Header().Set("Content-Type", mime)
}
if ro.Code != 0 {
w.WriteHeader(ro.Code)
}
_, err = w.Write(body)
return err
// HTTPResponder knows how to respond to an HTTP request
// Must consider:
// - Flash messages
// - XSRF handling (template data)
// - Assembling template data from various sources
//
// Authboss controller methods (like the one called in response to POST /auth/login)
// will call this method to write a response to the user.
type HTTPResponder interface {
Respond(w http.ResponseWriter, r *http.Request, code int, templateName string, data HTMLData) error
}
func (a *Authboss) redirectNonAPI(w http.ResponseWriter, r *http.Request, ro RedirectOptions) error {
path := ro.RedirectPath
redir := r.FormValue(FormValueRedirect)
if len(redir) != 0 && ro.FollowRedirParam {
path = redir
}
if len(ro.Success) != 0 {
PutSession(w, FlashSuccessKey, ro.Success)
}
if len(ro.Failure) != 0 {
PutSession(w, FlashErrorKey, ro.Failure)
}
http.Redirect(w, r, path, http.StatusFound)
return nil
// Redirector redirects http requests to a different url (must handle both json and html)
// When an authboss controller wants to redirect a user to a different path, it will use
// this interface.
type Redirector interface {
Redirect(w http.ResponseWriter, r *http.Request, ro RedirectOptions) error
}
// Email renders the e-mail templates and sends it using the mailer.
func (a *Authboss) Email(w http.ResponseWriter, r *http.Request, email Email, ro EmailResponseOptions) error {
ctx := r.Context()
if len(ro.HTMLTemplate) != 0 {
htmlBody, _, err := a.mailRenderer.Render(ctx, ro.HTMLTemplate, ro.Data)
if err != nil {
return errors.Wrap(err, "failed to render e-mail html body")
}
email.HTMLBody = string(htmlBody)
}
if len(ro.TextTemplate) != 0 {
textBody, _, err := a.mailRenderer.Render(ctx, ro.TextTemplate, ro.Data)
if err != nil {
return errors.Wrap(err, "failed to render e-mail text body")
}
email.TextBody = string(textBody)
}
return a.Mailer.Send(ctx, email)
}

View File

@ -2,65 +2,22 @@ package authboss
import (
"bytes"
"encoding/json"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
)
func TestResponseRespond(t *testing.T) {
t.Parallel()
type testMailer struct {
io.Writer
}
ab := New()
ab.renderer = mockRenderer{expectName: "some_template.tpl"}
ab.SessionStateStorer = newMockClientStateRW(
FlashSuccessKey, "flash_success",
FlashErrorKey, "flash_error",
)
ab.XSRFName = "xsrf"
ab.XSRFMaker = func(w http.ResponseWriter, r *http.Request) string {
return "xsrftoken"
}
ab.LayoutDataMaker = func(w http.ResponseWriter, r *http.Request) HTMLData {
return HTMLData{"hello": "world"}
}
r := httptest.NewRequest("GET", "/", nil)
wr := httptest.NewRecorder()
w := ab.NewResponse(wr, r)
r = loadClientStateP(ab, w, r)
err := ab.Respond(w, r, http.StatusCreated, "some_template.tpl", HTMLData{"auth_happy": true})
if err != nil {
t.Error(err)
}
if wr.Code != http.StatusCreated {
t.Error("code was wrong:", wr.Code)
}
if got := wr.HeaderMap.Get("Content-Type"); got != "application/json" {
t.Error("content type was wrong:", got)
}
expectData := HTMLData{
"xsrfName": "xsrf",
"xsrfToken": "xsrftoken",
"hello": "world",
FlashSuccessKey: "flash_success",
FlashErrorKey: "flash_error",
"auth_happy": true,
}
var gotData HTMLData
if err := json.Unmarshal(wr.Body.Bytes(), &gotData); err != nil {
t.Error(err)
}
if !reflect.DeepEqual(gotData, expectData) {
t.Errorf("data mismatched:\nwant: %#v\ngot: %#v", expectData, gotData)
}
func (t testMailer) Send(_ context.Context, email Email) error {
fmt.Fprintf(t.Writer, "%v", email)
return nil
}
func TestResponseEmail(t *testing.T) {
@ -81,7 +38,7 @@ func TestResponseEmail(t *testing.T) {
}
output := &bytes.Buffer{}
ab.Mailer = LogMailer(output)
ab.Mailer = testMailer{output}
r := httptest.NewRequest("GET", "/", nil)
wr := httptest.NewRecorder()
@ -113,144 +70,3 @@ func TestResponseEmail(t *testing.T) {
}
}
}
func TestResponseRedirectAPI(t *testing.T) {
t.Parallel()
ab := New()
ab.renderer = mockRenderer{}
r := httptest.NewRequest("POST", "/?redir=/pow", nil)
w := httptest.NewRecorder()
r.Header.Set("Content-Type", "application/json")
ro := RedirectOptions{
Success: "ok!",
Code: http.StatusTeapot,
RedirectPath: "/redirect", FollowRedirParam: false,
}
if err := ab.Redirect(w, r, ro); err != nil {
t.Error(err)
}
if w.Code != http.StatusTeapot {
t.Error("code is wrong:", w.Code)
}
var gotData map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &gotData); err != nil {
t.Error(err)
}
if got := gotData["status"]; got != "success" {
t.Error("status was wrong:", got)
}
if got := gotData["message"]; got != "ok!" {
t.Error("message was wrong:", got)
}
if got := gotData["location"]; got != "/redirect" {
t.Error("location was wrong:", got)
}
}
func TestResponseRedirectAPIFollowRedir(t *testing.T) {
t.Parallel()
ab := New()
ab.renderer = mockRenderer{}
r := httptest.NewRequest("POST", "/?redir=/pow", nil)
w := httptest.NewRecorder()
r.Header.Set("Content-Type", "application/json")
ro := RedirectOptions{
Failure: ":(",
Code: http.StatusTeapot,
RedirectPath: "/redirect", FollowRedirParam: true,
}
if err := ab.Redirect(w, r, ro); err != nil {
t.Error(err)
}
if w.Code != http.StatusTeapot {
t.Error("code is wrong:", w.Code)
}
var gotData map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &gotData); err != nil {
t.Error(err)
}
if got := gotData["status"]; got != "failure" {
t.Error("status was wrong:", got)
}
if got := gotData["message"]; got != ":(" {
t.Error("message was wrong:", got)
}
if got := gotData["location"]; got != "/pow" {
t.Error("location was wrong:", got)
}
}
func TestResponseRedirectNonAPI(t *testing.T) {
t.Parallel()
ab := New()
r := httptest.NewRequest("POST", "/?redir=/pow", nil)
wr := httptest.NewRecorder()
w := ab.NewResponse(wr, r)
ro := RedirectOptions{
Success: "success", Failure: "failure",
RedirectPath: "/redirect", FollowRedirParam: false,
}
if err := ab.Redirect(w, r, ro); err != nil {
t.Error(err)
}
csrw := w.(*ClientStateResponseWriter)
want := ClientStateEvent{Kind: ClientStateEventPut, Key: FlashSuccessKey, Value: "success"}
if csrw.sessionStateEvents[0] != want {
t.Error("event was wrong:", csrw.sessionStateEvents[0])
}
want = ClientStateEvent{Kind: ClientStateEventPut, Key: FlashErrorKey, Value: "failure"}
if csrw.sessionStateEvents[1] != want {
t.Error("event was wrong:", csrw.sessionStateEvents[1])
}
if wr.Code != http.StatusFound {
t.Error("code is wrong:", wr.Code)
}
if got := wr.Header().Get("Location"); got != "/redirect" {
t.Error("redirect location was wrong:", got)
}
}
func TestResponseRedirectNonAPIFollowRedir(t *testing.T) {
t.Parallel()
ab := New()
r := httptest.NewRequest("POST", "/?redir=/pow", nil)
wr := httptest.NewRecorder()
w := ab.NewResponse(wr, r)
ro := RedirectOptions{
RedirectPath: "/redirect", FollowRedirParam: true,
}
if err := ab.Redirect(w, r, ro); err != nil {
t.Error(err)
}
csrw := w.(*ClientStateResponseWriter)
if len(csrw.sessionStateEvents) != 0 {
t.Error("session state events should be empty:", csrw.sessionStateEvents)
}
if wr.Code != http.StatusFound {
t.Error("code is wrong:", wr.Code)
}
if got := wr.Header().Get("Location"); got != "/pow" {
t.Error("redirect location was wrong:", got)
}
}

147
router.go
View File

@ -1,148 +1,13 @@
package authboss
import (
"fmt"
"io"
"net/http"
"path"
)
// FormValue constants
const (
FormValueRedirect = "redir"
)
// HandlerFunc augments http.HandlerFunc with a context and error handling.
type HandlerFunc func(http.ResponseWriter, *http.Request) error
// RouteTable is a routing table from a path to a handlerfunc.
type RouteTable map[string]HandlerFunc
// NewRouter returns a router to be mounted at some mountpoint.
func (a *Authboss) NewRouter() http.Handler {
if a.mux != nil {
return a.mux
}
a.mux = http.NewServeMux()
for name, mod := range a.loadedModules {
for route, handler := range mod.Routes() {
fmt.Fprintf(a.LogWriter, "%-10s Route: %s\n", "["+name+"]", path.Join(a.MountPath, route))
a.mux.Handle(path.Join(a.MountPath, route), abHandler{a, handler})
}
}
a.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if a.NotFoundHandler != nil {
a.NotFoundHandler.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusNotFound)
io.WriteString(w, "404 Page not found")
}
})
return a.mux
// Router can register routes to later be used by the web application
type Router interface {
http.Handler
Get(path string, handler http.Handler)
Post(path string, handler http.Handler)
Delete(path string, handler http.Handler)
}
type abHandler struct {
*Authboss
fn HandlerFunc
}
// TODO(aarondl): Move this somewhere reasonable
func isAPIRequest(r *http.Request) bool {
return r.Header.Get("Content-Type") == "application/json"
}
func (a abHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Put uid in the context
_, err := a.LoadCurrentUserID(w, &r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
io.WriteString(w, "500 An error has occurred")
fmt.Fprintf(a.LogWriter, "failed to load current user id: %v", err)
return
}
// Call the handler
err = a.fn(w, r)
if err == nil {
return
}
// Log the error
fmt.Fprintf(a.LogWriter, "Error Occurred at %s: %v", r.URL.Path, err)
// Do specific error handling for special kinds of errors.
if _, ok := err.(ClientDataErr); ok {
if a.BadRequestHandler != nil {
a.BadRequestHandler.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, "400 Bad request")
}
return
}
if a.ErrorHandler != nil {
a.ErrorHandler.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusInternalServerError)
io.WriteString(w, "500 An error has occurred")
}
}
/*
// TODO(aarondl): Throw away this function
// redirectIfLoggedIn checks a user's existence by using currentUser. This is done instead of
// a simple Session cookie check so that the remember module has a chance to log the user in
// before they are determined to "not be logged in".
//
// The exceptional routes are sort of hardcoded in a terrible way in here, later on this could move to some
// configuration or something more interesting.
func redirectIfLoggedIn(w http.ResponseWriter, r *http.Request) (handled bool) {
// If it's a log out url, always let it pass through.
if strings.HasSuffix(r.URL.Path, "/logout") {
return false
}
// If it's an auth url, allow them through if they're half-authed.
if strings.HasSuffix(r.URL.Path, "/auth") || strings.Contains(r.URL.Path, "/oauth2/") {
if halfAuthed, ok := ctx.SessionStorer.Get(SessionHalfAuthKey); ok && halfAuthed == "true" {
return false
}
}
// Otherwise, check if they're logged in, this uses hooks to allow remember
// to set the session cookie
cu, err := ctx.currentUser(ctx, w, r)
// if the user was not found, that means the user was deleted from the underlying
// storer and we should just remove this session cookie and allow them through.
// if it's a generic error, 500
// if the user is found, redirect them away from this page, because they don't need
// to see it.
if err == ErrUserNotFound {
uname, _ := ctx.SessionStorer.Get(SessionKey)
fmt.Fprintf(ctx.LogWriter, "user (%s) has session cookie but user not found, removing cookie", uname)
ctx.SessionStorer.Del(SessionKey)
return false
} else if err != nil {
fmt.Fprintf(ctx.LogWriter, "error occurred reading current user at %s: %v", r.URL.Path, err)
w.WriteHeader(http.StatusInternalServerError)
io.WriteString(w, "500 An error has occurred")
return true
}
if cu != nil {
if redir := r.FormValue(FormValueRedirect); len(redir) > 0 {
http.Redirect(w, r, redir, http.StatusFound)
} else {
http.Redirect(w, r, ctx.AuthLoginOKPath, http.StatusFound)
}
return true
}
return false
}
*/

View File

@ -1,7 +1,6 @@
package authboss
import (
"bytes"
"context"
"time"
@ -29,22 +28,24 @@ var (
ErrUserNotFound = errors.New("user not found")
// ErrTokenNotFound should be returned from UseToken when the record is not found.
ErrTokenNotFound = errors.New("token not found")
// ErrUserFound should be returned from Create when the primaryID of the record is found.
// ErrUserFound should be returned from Create (see ConfirmUser) when the primaryID
// of the record is found.
ErrUserFound = errors.New("user found")
)
// StoreLoader represents the data store that's capable of loading users
// ServerStorer represents the data store that's capable of loading users
// and giving them a context with which to store themselves.
type StoreLoader interface {
// Load will be passed the PrimaryID and return pre-loaded storer (meaning
// Storer.Load will not be called)
Load(ctx context.Context, key string) (Storer, error)
type ServerStorer interface {
// Load will look up the user based on the passed the PrimaryID
Load(ctx context.Context, key string) (User, error)
// Save persists the user in the database
Save(ctx context.Context, user User) error
}
// Storer represents a user that also knows how to put himself into the db.
// It has functions for each piece of data it requires.
// Note that you should only persist data once Save() has been called.
type Storer interface {
// User has functions for each piece of data it requires.
// Data should not be persisted on each function call.
type User interface {
PutEmail(ctx context.Context, email string) error
PutUsername(ctx context.Context, username string) error
PutPassword(ctx context.Context, password string) error
@ -52,21 +53,13 @@ type Storer interface {
GetEmail(ctx context.Context) (email string, err error)
GetUsername(ctx context.Context) (username string, err error)
GetPassword(ctx context.Context) (password string, err error)
// Save the state.
Save(ctx context.Context) error
// Load the state based on the properties that have been given (typically
// an e-mail/username).
Load(ctx context.Context) error
}
// TODO(aarondl): Document & move to Register module
// ArbitraryStorer allows arbitrary data from the web form through. You should
// ArbitraryUser allows arbitrary data from the web form through. You should
// definitely only pull the keys you want from the map, since this is unfiltered
// input from a web request and is an attack vector.
type ArbitraryStorer interface {
Storer
type ArbitraryUser interface {
User
// PutArbitrary allows arbitrary fields defined by the authboss library
// consumer to add fields to the user registration piece.
@ -76,10 +69,12 @@ type ArbitraryStorer interface {
GetArbitrary(ctx context.Context) (arbitrary map[string]string, err error)
}
// OAuth2Storer allows reading and writing values relating to OAuth2
type OAuth2Storer interface {
Storer
// OAuth2User allows reading and writing values relating to OAuth2
type OAuth2User interface {
User
// IsOAuth2User checks to see if a user was registered in the site as an
// oauth2 user.
IsOAuth2User(ctx context.Context) (bool, error)
PutUID(ctx context.Context, uid string) error
@ -94,36 +89,3 @@ type OAuth2Storer interface {
GetRefreshToken(ctx context.Context) (refreshToken string, err error)
GetExpiry(ctx context.Context) (expiry time.Duration, err error)
}
func camelToUnder(in string) string {
out := bytes.Buffer{}
for i := 0; i < len(in); i++ {
chr := in[i]
if chr >= 'A' && chr <= 'Z' {
if i > 0 {
out.WriteByte('_')
}
out.WriteByte(chr + 'a' - 'A')
} else {
out.WriteByte(chr)
}
}
return out.String()
}
func underToCamel(in string) string {
out := bytes.Buffer{}
for i := 0; i < len(in); i++ {
chr := in[i]
if first := i == 0; first || chr == '_' {
if !first {
i++
}
out.WriteByte(in[i] - 'a' + 'A')
} else {
out.WriteByte(chr)
}
}
return out.String()
}

View File

@ -1,26 +0,0 @@
package authboss
import "testing"
func TestCasingStyleConversions(t *testing.T) {
t.Parallel()
tests := []struct {
In string
Out string
}{
{"SomethingInCamel", "something_in_camel"},
{"Oauth2Anything", "oauth2_anything"},
}
for i, test := range tests {
out := camelToUnder(test.In)
if out != test.Out {
t.Errorf("%d) Expected %q got %q", i, test.Out, out)
}
out = underToCamel(out)
if out != test.In {
t.Errorf("%d), Expected %q got %q", i, test.In, out)
}
}
}

View File

@ -1,10 +1,5 @@
package authboss
import "net/http"
// ViewDataMaker asks for an HTMLData object to assist with rendering.
type ViewDataMaker func(http.ResponseWriter, *http.Request) HTMLData
// HTMLData is used to render templates with.
type HTMLData map[string]interface{}