Compare commits

...

4 commits

Author SHA1 Message Date
86abb0b618 Some renaming 2022-11-20 13:42:10 -05:00
d34814f050 Some renaming 2022-11-20 12:51:26 -05:00
f3e17e149f Flow split begin 2022-11-20 08:49:24 -05:00
ae00c1534d get user 2022-11-13 19:06:53 -05:00
10 changed files with 343 additions and 159 deletions

View file

@ -15,6 +15,13 @@ type cmdOptions interface {
Execute() error Execute() error
} }
func AppNamePtr() *string {
s := AppName
return &s
}
func IntPtr(i int) *int { return &i }
// RunE is a convenience function for use with cobra. // RunE is a convenience function for use with cobra.
func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error { func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error {

View file

@ -11,6 +11,7 @@ import (
type ( type (
// PyTimeStamp is a timestamp that marshals to python-style timestamp strings (long nano). // PyTimeStamp is a timestamp that marshals to python-style timestamp strings (long nano).
PyTimestamp time.Time PyTimestamp time.Time
ClientID string
) )
const PytTimeFormat = "2006-01-02T15:04:05.999999-07:00" const PytTimeFormat = "2006-01-02T15:04:05.999999-07:00"

View file

@ -24,7 +24,7 @@ var (
type Authenticator struct { type Authenticator struct {
store AuthStore store AuthStore
flows FlowStore flows *AuthFlowManager
sessions AccessSessionStore sessions AccessSessionStore
providers map[string]provider.AuthProvider providers map[string]provider.AuthProvider
} }
@ -58,7 +58,7 @@ func (a *Authenticator) InitAuth(s storage.Store) error {
a.providers[nProv.ProviderType()] = nProv a.providers[nProv.ProviderType()] = nProv
} }
a.flows = make(FlowStore) a.flows = NewAuthFlowManager()
a.sessions.init() a.sessions.init()
@ -91,19 +91,14 @@ func (a *Authenticator) ProvidersHandler(c echo.Context) error {
return c.JSON(http.StatusOK, providers) return c.JSON(http.StatusOK, providers)
} }
func (a *Authenticator) Check(f *Flow, req *http.Request, rm map[string]interface{}) (provider.ProviderUser, error) { func (a *Authenticator) Check(f *LoginFlow, req *http.Request, rm map[string]interface{}) (provider.ProviderUser, error) {
cID, hasCID := rm["client_id"] cID, hasCID := rm["client_id"]
cIDStr, cidIsStr := cID.(string) cIDStr, cidIsStr := cID.(string)
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.request.ClientID) { if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.ClientID) {
return nil, ErrInvalidAuth return nil, ErrInvalidAuth
} }
for _, h := range f.Handler { p := a.Provider(f.Handler.String())
if h == nil {
return nil, ErrInvalidHandler
}
p := a.Provider(*h)
if p == nil { if p == nil {
return nil, ErrInvalidAuth return nil, ErrInvalidAuth
} }
@ -114,7 +109,6 @@ func (a *Authenticator) Check(f *Flow, req *http.Request, rm map[string]interfac
log.Info().Interface("user", user.UserData()).Msg("Login success") log.Info().Interface("user", user.UserData()).Msg("Login success")
return user, nil return user, nil
} }
}
return nil, ErrInvalidAuth return nil, ErrInvalidAuth
} }

View file

@ -3,132 +3,95 @@ package auth
import ( import (
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"dynatron.me/x/blasphem/internal/common" "dynatron.me/x/blasphem/internal/common"
"dynatron.me/x/blasphem/internal/generate"
"dynatron.me/x/blasphem/pkg/auth/provider" "dynatron.me/x/blasphem/pkg/auth/provider"
"dynatron.me/x/blasphem/pkg/flow"
) )
type FlowStore map[FlowID]*Flow type AuthFlowManager struct {
*flow.FlowManager
}
type FlowRequest struct { type LoginFlow struct {
ClientID ClientID `json:"client_id"` flow.FlowHandler
ClientID common.ClientID
FlowContext
}
type FlowContext struct {
IPAddr string
CredentialOnly bool
RedirectURI string
}
type LoginFlowRequest struct {
ClientID common.ClientID `json:"client_id"`
Handler []*string `json:"handler"` Handler []*string `json:"handler"`
RedirectURI string `json:"redirect_uri"` RedirectURI string `json:"redirect_uri"`
Type *string `json:"type"`
ip string `json:"-"`
} }
type FlowType string func (r *LoginFlowRequest) FlowContext() FlowContext {
return FlowContext{
const ( IPAddr: r.ip,
TypeForm FlowType = "form" RedirectURI: r.RedirectURI,
TypeCreateEntry FlowType = "create_entry" CredentialOnly: r.Type != nil && *r.Type == "link_user",
)
type FlowID string
type Step string
const (
StepInit Step = "init"
)
type Flow struct {
Type FlowType `json:"type"`
ID FlowID `json:"flow_id"`
Handler []*string `json:"handler"`
StepID *Step `json:"step_id,omitempty"`
Schema []provider.FlowSchemaItem `json:"data_schema"`
Errors interface{} `json:"errors"`
DescPlace *string `json:"description_placeholders"`
LastStep *string `json:"last_step"`
request *FlowRequest
ctime time.Time
}
func (f *Flow) touch() {
f.ctime = time.Now()
}
func (fs FlowStore) register(f *Flow) {
fs.cull()
fs[f.ID] = f
}
func (fs FlowStore) Remove(f *Flow) {
delete(fs, f.ID)
}
const cullAge = time.Minute * 30
func (fs FlowStore) cull() {
for k, v := range fs {
if time.Now().Sub(v.ctime) > cullAge {
delete(fs, k)
} }
} }
func NewAuthFlowManager() *AuthFlowManager {
return &AuthFlowManager{FlowManager: flow.NewFlowManager()}
} }
func (fs FlowStore) Get(id FlowID) *Flow { func (afm *AuthFlowManager) NewLoginFlow(req *LoginFlowRequest, prov provider.AuthProvider) *LoginFlow {
f, ok := fs[id] lf := &LoginFlow{
if ok { FlowHandler: flow.NewFlowHandlerBase(prov, prov.ProviderType()),
return f ClientID: req.ClientID,
FlowContext: req.FlowContext(),
} }
return nil afm.Register(lf)
return lf
} }
func (a *Authenticator) NewFlow(r *FlowRequest) *Flow { func (a *Authenticator) NewFlow(r *LoginFlowRequest) *flow.Result {
var sch []provider.FlowSchemaItem var prov provider.AuthProvider
for _, h := range r.Handler { for _, h := range r.Handler {
if h == nil { if h == nil {
break break
} }
if hand := a.Provider(*h); hand != nil { prov = a.Provider(*h)
sch = hand.FlowSchema() if prov != nil {
break break
} }
} }
if sch == nil { if prov == nil {
return nil return nil
} }
flow := &Flow{ flow := a.flows.NewLoginFlow(r, prov)
Type: TypeForm,
ID: FlowID(generate.UUID()),
StepID: stepPtr(StepInit),
Schema: sch,
Handler: r.Handler,
Errors: []string{},
request: r,
}
flow.touch()
a.flows.register(flow) return flow.ShowForm(nil)
return flow
} }
func stepPtr(s Step) *Step { return &s } func (f *LoginFlow) redirect(c echo.Context) {
c.Request().Header.Set("Location", f.RedirectURI)
func (f *Flow) redirect(c echo.Context) {
c.Request().Header.Set("Location", f.request.RedirectURI)
} }
func (f *Flow) progress(a *Authenticator, c echo.Context) error { func (f *LoginFlow) progress(a *Authenticator, c echo.Context) error {
if f.StepID == nil { switch f.Step() {
c.Logger().Error("stepID is nil") case flow.StepInit:
return c.String(http.StatusInternalServerError, "No Step ID")
}
switch *f.StepID {
case StepInit:
rm := make(map[string]interface{}) rm := make(map[string]interface{})
err := c.Bind(&rm) err := c.Bind(&rm)
@ -136,31 +99,21 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
return c.String(http.StatusBadRequest, err.Error()) return c.String(http.StatusBadRequest, err.Error())
} }
for _, si := range f.Schema { err = f.Schema.CheckRequired(rm)
if si.Required { if err != nil {
if _, ok := rm[si.Name]; !ok { return c.JSON(http.StatusBadRequest, f.ShowForm([]string{err.Error()}))
return c.String(http.StatusBadRequest, "missing required param "+si.Name)
}
}
} }
user, err := a.Check(f, c.Request(), rm) user, err := a.Check(f, c.Request(), rm)
switch err { switch err {
case nil: case nil:
var finishedFlow struct { finishedFlow := flow.Result{}
ID FlowID `json:"flow_id"`
Handler []*string `json:"handler"`
Result AccessTokenID `json:"result"`
Title string `json:"title"`
Type FlowType `json:"type"`
Version int `json:"version"`
}
a.flows.Remove(f) a.flows.Remove(f)
copier.Copy(&finishedFlow, f) copier.Copy(&finishedFlow, f)
finishedFlow.Type = TypeCreateEntry finishedFlow.Type = flow.TypeCreateEntry
finishedFlow.Title = common.AppName finishedFlow.Title = common.AppNamePtr()
finishedFlow.Version = 1 finishedFlow.Version = common.IntPtr(1)
finishedFlow.Result = a.NewAccessToken(c.Request(), user, f) finishedFlow.Result = a.NewAccessToken(c.Request(), user)
f.redirect(c) f.redirect(c)
@ -170,24 +123,26 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
case ErrInvalidAuth: case ErrInvalidAuth:
fallthrough fallthrough
default: default:
f.Errors = map[string]interface{}{ return c.JSON(http.StatusOK, f.ShowForm(map[string]interface{}{
"base": "invalid_auth", "base": "invalid_auth",
} }))
return c.JSON(http.StatusOK, f)
} }
default: default:
return c.String(http.StatusBadRequest, "unknown flow step") return c.JSON(http.StatusOK, f.ShowForm(map[string]interface{}{
"base": "unknown_flow_step",
}))
} }
} }
func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error { func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error {
flowID := c.Param("flow_id") flowID := flow.FlowID(c.Param("flow_id"))
if flowID == "" { if flowID == "" {
return c.String(http.StatusBadRequest, "empty flow ID") return c.String(http.StatusBadRequest, "empty flow ID")
} }
delete(a.flows, FlowID(flowID)) a.flows.Delete(flowID)
return c.String(http.StatusOK, "deleted") return c.String(http.StatusOK, "deleted")
} }
@ -202,12 +157,14 @@ func setJSON(c echo.Context) {
func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error { func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error {
setJSON(c) setJSON(c)
var flowReq FlowRequest var flowReq LoginFlowRequest
err := c.Bind(&flowReq) err := c.Bind(&flowReq)
if err != nil { if err != nil {
return c.String(http.StatusBadRequest, err.Error()) return c.String(http.StatusBadRequest, err.Error())
} }
flowReq.ip = c.Request().RemoteAddr
resp := a.NewFlow(&flowReq) resp := a.NewFlow(&flowReq)
if resp == nil { if resp == nil {
@ -222,16 +179,10 @@ func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
flowID := c.Param("flow_id") flowID := c.Param("flow_id")
flow := a.flows.Get(FlowID(flowID)) flow := a.flows.Get(flow.FlowID(flowID))
if flow == nil { if flow == nil {
return c.String(http.StatusNotFound, "no such flow") return c.String(http.StatusNotFound, "no such flow")
} }
if time.Now().Sub(flow.ctime) > cullAge { return flow.(*LoginFlow).progress(a, c)
a.flows.Remove(flow)
return c.String(http.StatusGone, "flow timed out")
}
return flow.progress(a, c)
} }

View file

@ -9,6 +9,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"dynatron.me/x/blasphem/pkg/auth/provider" "dynatron.me/x/blasphem/pkg/auth/provider"
"dynatron.me/x/blasphem/pkg/flow"
"dynatron.me/x/blasphem/pkg/storage" "dynatron.me/x/blasphem/pkg/storage"
) )
@ -24,7 +25,7 @@ type HAUser struct {
} }
func (hau *HAUser) UserData() provider.ProviderUser { func (hau *HAUser) UserData() provider.ProviderUser {
return &UserData{ return &UserData{ // strip secret
Username: hau.Username, Username: hau.Username,
} }
} }
@ -44,6 +45,7 @@ func (h *HAUser) ProviderUserData() interface{} { return h.UserData() }
type HomeAssistantProvider struct { type HomeAssistantProvider struct {
provider.AuthProviderBase `json:"-"` provider.AuthProviderBase `json:"-"`
Users []HAUser `json:"users"` Users []HAUser `json:"users"`
userMap map[string]*HAUser
} }
func NewHAProvider(s storage.Store) (provider.AuthProvider, error) { func NewHAProvider(s storage.Store) (provider.AuthProvider, error) {
@ -59,13 +61,25 @@ func NewHAProvider(s storage.Store) (provider.AuthProvider, error) {
return hap, err return hap, err
} }
for i := range hap.Users { hap.userMap = make(map[string]*HAUser)
for i, u := range hap.Users {
hap.Users[i].AuthProvider = hap hap.Users[i].AuthProvider = hap
hap.userMap[u.Username] = &hap.Users[i]
} }
return hap, nil return hap, nil
} }
func (hap *HomeAssistantProvider) Lookup(pu provider.ProviderUser) provider.ProviderUser {
u, has := hap.userMap[pu.(*HAUser).Username]
if !has {
return nil
}
return u
}
func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) { func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost) return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
} }
@ -110,11 +124,11 @@ func (hap *HomeAssistantProvider) ValidateCreds(r *http.Request, rm map[string]i
} }
func (hap *HomeAssistantProvider) NewCredData() interface{} { func (hap *HomeAssistantProvider) NewCredData() interface{} {
return &UserData{} return &HAUser{}
} }
func (hap *HomeAssistantProvider) FlowSchema() []provider.FlowSchemaItem { func (hap *HomeAssistantProvider) FlowSchema() flow.Schema {
return []provider.FlowSchemaItem{ return []flow.SchemaItem{
{ {
Type: "string", Type: "string",
Name: "username", Name: "username",

View file

@ -3,6 +3,7 @@ package provider
import ( import (
"net/http" "net/http"
"dynatron.me/x/blasphem/pkg/flow"
"dynatron.me/x/blasphem/pkg/storage" "dynatron.me/x/blasphem/pkg/storage"
) )
@ -13,9 +14,10 @@ var Providers = make(map[string]Constructor)
type AuthProvider interface { // TODO: this should include stepping type AuthProvider interface { // TODO: this should include stepping
AuthProviderMetadata AuthProviderMetadata
ProviderBase() AuthProviderBase ProviderBase() AuthProviderBase
FlowSchema() []FlowSchemaItem FlowSchema() flow.Schema
NewCredData() interface{} NewCredData() interface{}
ValidateCreds(r *http.Request, reqMap map[string]interface{}) (user ProviderUser, success bool) ValidateCreds(r *http.Request, reqMap map[string]interface{}) (user ProviderUser, success bool)
Lookup(ProviderUser) ProviderUser
} }
func Register(providerName string, f func(storage.Store) (AuthProvider, error)) { func Register(providerName string, f func(storage.Store) (AuthProvider, error)) {
@ -23,6 +25,7 @@ func Register(providerName string, f func(storage.Store) (AuthProvider, error))
} }
type ProviderUser interface { type ProviderUser interface {
// TODO: make sure this is sane with all the ProviderUser and UserData type stuff
UserData() ProviderUser UserData() ProviderUser
} }
@ -42,9 +45,3 @@ func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID } func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
func (bp *AuthProviderBase) ProviderType() string { return bp.Type } func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp } func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
type FlowSchemaItem struct {
Type string `json:"type"`
Name string `json:"name"`
Required bool `json:"required"`
}

View file

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"dynatron.me/x/blasphem/pkg/auth/provider" "dynatron.me/x/blasphem/pkg/auth/provider"
"dynatron.me/x/blasphem/pkg/flow"
"dynatron.me/x/blasphem/pkg/storage" "dynatron.me/x/blasphem/pkg/storage"
) )
@ -46,6 +47,10 @@ func New(s storage.Store) (provider.AuthProvider, error) {
return hap, nil return hap, nil
} }
func (tnp *TrustedNetworksProvider) Lookup(pu provider.ProviderUser) provider.ProviderUser {
return pu
}
func (hap *TrustedNetworksProvider) ValidateCreds(r *http.Request, rm map[string]interface{}) (provider.ProviderUser, bool) { func (hap *TrustedNetworksProvider) ValidateCreds(r *http.Request, rm map[string]interface{}) (provider.ProviderUser, bool) {
/* /*
if req.RemoteAddr in allowed then do the thing if req.RemoteAddr in allowed then do the thing
@ -57,8 +62,8 @@ func (hap *TrustedNetworksProvider) NewCredData() interface{} {
return &UserData{} return &UserData{}
} }
func (hap *TrustedNetworksProvider) FlowSchema() []provider.FlowSchemaItem { func (hap *TrustedNetworksProvider) FlowSchema() flow.Schema {
return []provider.FlowSchemaItem{ return []flow.SchemaItem{
{ {
Type: "string", Type: "string",
Name: "username", Name: "username",

View file

@ -124,16 +124,14 @@ func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request
return nil return nil
} }
cred := &Credential{ cred := a.store.Credential(user)
user: user,
}
return cred return cred
} }
const defaultExpiration = 15 * time.Minute const defaultExpiration = 15 * time.Minute
func (a *Authenticator) NewAccessToken(r *http.Request, user provider.ProviderUser, f *Flow) AccessTokenID { func (a *Authenticator) NewAccessToken(r *http.Request, user provider.ProviderUser) AccessTokenID {
id := AccessTokenID(generate.UUID()) id := AccessTokenID(generate.UUID())
now := time.Now() now := time.Now()
@ -158,7 +156,7 @@ const (
GTRefreshToken GrantType = "refresh_token" GTRefreshToken GrantType = "refresh_token"
) )
type ClientID string type ClientID common.ClientID
func (c *ClientID) IsValid() bool { func (c *ClientID) IsValid() bool {
// TODO: || !indieauth.VerifyClientID(rq.ClientID)? // TODO: || !indieauth.VerifyClientID(rq.ClientID)?

View file

@ -14,6 +14,7 @@ const (
type AuthStore interface { type AuthStore interface {
User(UserID) *User User(UserID) *User
Credential(provider.ProviderUser) *Credential
} }
type authStore struct { type authStore struct {
@ -23,6 +24,16 @@ type authStore struct {
Refresh []RefreshToken `json:"refresh_tokens"` Refresh []RefreshToken `json:"refresh_tokens"`
userMap map[UserID]*User userMap map[UserID]*User
providerUsers map[provider.ProviderUser]*Credential
}
func (as *authStore) Credential(p provider.ProviderUser) *Credential {
c, have := as.providerUsers[p]
if !have {
return nil
}
return c
} }
func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) { func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) {
@ -30,6 +41,7 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
err = s.Get(AuthStoreKey, as) err = s.Get(AuthStoreKey, as)
as.userMap = make(map[UserID]*User) as.userMap = make(map[UserID]*User)
as.providerUsers = make(map[provider.ProviderUser]*Credential)
for _, u := range as.Users { for _, u := range as.Users {
as.userMap[u.ID] = &u as.userMap[u.ID] = &u
@ -49,7 +61,11 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
return nil, err return nil, err
} }
c.user = pd.(provider.ProviderUser) c.user = prov.Lookup(pd.(provider.ProviderUser))
if c.user == nil {
return nil, fmt.Errorf("cannot find user in provider %s", prov.ProviderName())
}
as.providerUsers[c.user] = &c
} }
} }

201
pkg/flow/flow.go Normal file
View file

@ -0,0 +1,201 @@
// flow is the data entry flow.
package flow
import (
"fmt"
"time"
"dynatron.me/x/blasphem/internal/generate"
)
type (
ResultType string
FlowID string
Step string
HandlerKey string
Errors interface{}
Context interface{}
FlowStore map[FlowID]Handler
FlowManager struct {
flows FlowStore
}
Result struct {
Type ResultType `json:"type"`
ID FlowID `json:"flow_id"`
Handler []*HandlerKey `json:"handler"`
Title *string `json:"title,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
StepID *Step `json:"step_id,omitempty"`
Schema []SchemaItem `json:"data_schema"`
Extra *string `json:"extra,omitempty"`
Required *bool `json:"required,omitempty"`
Errors interface{} `json:"errors"`
Description *string `json:"description,omitempty"`
DescPlace *string `json:"description_placeholders"`
URL *string `json:"url,omitempty"`
Reason *string `json:"reason,omitempty"`
Context *string `json:"context,omitempty"`
Result interface{} `json:"result,omitempty"`
LastStep *string `json:"last_step"`
Options map[string]interface{} `json:"options,omitempty"`
Version *int `json:"version,omitempty"`
}
SchemaItem struct {
Type string `json:"type"`
Name string `json:"name"`
Required bool `json:"required"`
}
Schema []SchemaItem
)
type (
Schemer interface {
FlowSchema() Schema
}
Handler interface {
Base() FlowHandler
FlowID() FlowID
flowCtime() time.Time
}
)
const (
StepInit Step = "init"
)
func (fs *Schema) CheckRequired(rm map[string]interface{}) error {
for _, si := range *fs {
if si.Required {
if _, ok := rm[si.Name]; !ok {
return fmt.Errorf("missing required param %s", si.Name)
}
}
}
return nil
}
func NewFlowManager() *FlowManager {
return &FlowManager{
flows: make(FlowStore),
}
}
func stepPtr(s Step) *Step { return &s }
type FlowHandler struct {
ID FlowID // ID is the FlowID
Handler HandlerKey // Handler key
Context Context // flow Context
Schema Schema
// curStep is the current step set by the flow manager
curStep Step
ctime time.Time
}
func (f *FlowHandler) Step() Step { return f.curStep }
func (f *FlowHandler) Base() FlowHandler { return *f }
func (f *FlowHandler) FlowID() FlowID {
return f.ID
}
func (f *FlowHandler) flowCtime() time.Time { return f.ctime }
func NewFlowHandlerBase(sch Schemer, hand string) FlowHandler {
return FlowHandler{
ID: FlowID(generate.UUID()),
Handler: HandlerKey(hand),
Schema: sch.FlowSchema(),
curStep: StepInit,
ctime: time.Now(),
}
}
func (hk *HandlerKey) String() string {
return string(*hk)
}
func (fm *FlowHandler) Handlers() []*HandlerKey {
return []*HandlerKey{&fm.Handler, nil}
}
func resultErrs(e Errors) Errors {
if e == nil {
return []string{}
}
return e
}
func (fm *FlowHandler) ShowForm(errs Errors) *Result {
res := &Result{
Type: TypeForm,
ID: fm.ID,
StepID: stepPtr(fm.curStep),
Schema: fm.Schema,
Handler: fm.Handlers(),
Errors: resultErrs(errs),
}
return res
}
func (fm *FlowManager) Delete(id FlowID) {
delete(fm.flows, id)
}
const (
TypeForm ResultType = "form"
TypeCreateEntry ResultType = "create_entry"
TypeAbort ResultType = "abort"
TypeExternalStep ResultType = "external"
TypeExternalStepDone ResultType = "external_done"
TypeShowProgress ResultType = "progress"
TypeShowProgressDone ResultType = "progress_done"
TypeMenu ResultType = "menu"
)
func (f *FlowHandler) touch() {
f.ctime = time.Now()
}
func (fm *FlowManager) Register(f Handler) {
fm.flows.cull()
fm.flows[f.FlowID()] = f
}
func (fs *FlowManager) Remove(f Handler) {
delete(fs.flows, f.FlowID())
}
const cullAge = time.Minute * 30
func (fs FlowStore) cull() {
for k, v := range fs {
if time.Now().Sub(v.flowCtime()) > cullAge {
delete(fs, k)
}
}
}
func (fs *FlowManager) Get(id FlowID) Handler {
f, ok := fs.flows[id]
if ok {
return f
}
return nil
}