Compare commits
4 commits
c618197c54
...
86abb0b618
Author | SHA1 | Date | |
---|---|---|---|
86abb0b618 | |||
d34814f050 | |||
f3e17e149f | |||
ae00c1534d |
10 changed files with 343 additions and 159 deletions
|
@ -15,6 +15,13 @@ type cmdOptions interface {
|
|||
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.
|
||||
func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
type (
|
||||
// PyTimeStamp is a timestamp that marshals to python-style timestamp strings (long nano).
|
||||
PyTimestamp time.Time
|
||||
ClientID string
|
||||
)
|
||||
|
||||
const PytTimeFormat = "2006-01-02T15:04:05.999999-07:00"
|
||||
|
|
|
@ -24,7 +24,7 @@ var (
|
|||
|
||||
type Authenticator struct {
|
||||
store AuthStore
|
||||
flows FlowStore
|
||||
flows *AuthFlowManager
|
||||
sessions AccessSessionStore
|
||||
providers map[string]provider.AuthProvider
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ func (a *Authenticator) InitAuth(s storage.Store) error {
|
|||
a.providers[nProv.ProviderType()] = nProv
|
||||
}
|
||||
|
||||
a.flows = make(FlowStore)
|
||||
a.flows = NewAuthFlowManager()
|
||||
|
||||
a.sessions.init()
|
||||
|
||||
|
@ -91,29 +91,23 @@ func (a *Authenticator) ProvidersHandler(c echo.Context) error {
|
|||
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"]
|
||||
cIDStr, cidIsStr := cID.(string)
|
||||
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.request.ClientID) {
|
||||
if !hasCID || !cidIsStr || cIDStr == "" || cIDStr != string(f.ClientID) {
|
||||
return nil, ErrInvalidAuth
|
||||
}
|
||||
|
||||
for _, h := range f.Handler {
|
||||
if h == nil {
|
||||
return nil, ErrInvalidHandler
|
||||
}
|
||||
p := a.Provider(f.Handler.String())
|
||||
if p == nil {
|
||||
return nil, ErrInvalidAuth
|
||||
}
|
||||
|
||||
p := a.Provider(*h)
|
||||
if p == nil {
|
||||
return nil, ErrInvalidAuth
|
||||
}
|
||||
user, success := p.ValidateCreds(req, rm)
|
||||
|
||||
user, success := p.ValidateCreds(req, rm)
|
||||
|
||||
if success {
|
||||
log.Info().Interface("user", user.UserData()).Msg("Login success")
|
||||
return user, nil
|
||||
}
|
||||
if success {
|
||||
log.Info().Interface("user", user.UserData()).Msg("Login success")
|
||||
return user, nil
|
||||
}
|
||||
|
||||
return nil, ErrInvalidAuth
|
||||
|
|
191
pkg/auth/flow.go
191
pkg/auth/flow.go
|
@ -3,132 +3,95 @@ package auth
|
|||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"dynatron.me/x/blasphem/internal/common"
|
||||
"dynatron.me/x/blasphem/internal/generate"
|
||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||
"dynatron.me/x/blasphem/pkg/flow"
|
||||
)
|
||||
|
||||
type FlowStore map[FlowID]*Flow
|
||||
|
||||
type FlowRequest struct {
|
||||
ClientID ClientID `json:"client_id"`
|
||||
Handler []*string `json:"handler"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
type AuthFlowManager struct {
|
||||
*flow.FlowManager
|
||||
}
|
||||
|
||||
type FlowType string
|
||||
type LoginFlow struct {
|
||||
flow.FlowHandler
|
||||
|
||||
const (
|
||||
TypeForm FlowType = "form"
|
||||
TypeCreateEntry FlowType = "create_entry"
|
||||
)
|
||||
|
||||
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
|
||||
ClientID common.ClientID
|
||||
FlowContext
|
||||
}
|
||||
|
||||
func (f *Flow) touch() {
|
||||
f.ctime = time.Now()
|
||||
type FlowContext struct {
|
||||
IPAddr string
|
||||
CredentialOnly bool
|
||||
RedirectURI string
|
||||
}
|
||||
|
||||
func (fs FlowStore) register(f *Flow) {
|
||||
fs.cull()
|
||||
fs[f.ID] = f
|
||||
type LoginFlowRequest struct {
|
||||
ClientID common.ClientID `json:"client_id"`
|
||||
Handler []*string `json:"handler"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
Type *string `json:"type"`
|
||||
|
||||
ip string `json:"-"`
|
||||
}
|
||||
|
||||
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 (r *LoginFlowRequest) FlowContext() FlowContext {
|
||||
return FlowContext{
|
||||
IPAddr: r.ip,
|
||||
RedirectURI: r.RedirectURI,
|
||||
CredentialOnly: r.Type != nil && *r.Type == "link_user",
|
||||
}
|
||||
}
|
||||
|
||||
func (fs FlowStore) Get(id FlowID) *Flow {
|
||||
f, ok := fs[id]
|
||||
if ok {
|
||||
return f
|
||||
}
|
||||
|
||||
return nil
|
||||
func NewAuthFlowManager() *AuthFlowManager {
|
||||
return &AuthFlowManager{FlowManager: flow.NewFlowManager()}
|
||||
}
|
||||
|
||||
func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
|
||||
var sch []provider.FlowSchemaItem
|
||||
func (afm *AuthFlowManager) NewLoginFlow(req *LoginFlowRequest, prov provider.AuthProvider) *LoginFlow {
|
||||
lf := &LoginFlow{
|
||||
FlowHandler: flow.NewFlowHandlerBase(prov, prov.ProviderType()),
|
||||
ClientID: req.ClientID,
|
||||
FlowContext: req.FlowContext(),
|
||||
}
|
||||
|
||||
afm.Register(lf)
|
||||
|
||||
return lf
|
||||
}
|
||||
|
||||
func (a *Authenticator) NewFlow(r *LoginFlowRequest) *flow.Result {
|
||||
var prov provider.AuthProvider
|
||||
|
||||
for _, h := range r.Handler {
|
||||
if h == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if hand := a.Provider(*h); hand != nil {
|
||||
sch = hand.FlowSchema()
|
||||
prov = a.Provider(*h)
|
||||
if prov != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if sch == nil {
|
||||
if prov == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
flow := &Flow{
|
||||
Type: TypeForm,
|
||||
ID: FlowID(generate.UUID()),
|
||||
StepID: stepPtr(StepInit),
|
||||
Schema: sch,
|
||||
Handler: r.Handler,
|
||||
Errors: []string{},
|
||||
request: r,
|
||||
}
|
||||
flow.touch()
|
||||
flow := a.flows.NewLoginFlow(r, prov)
|
||||
|
||||
a.flows.register(flow)
|
||||
|
||||
return flow
|
||||
return flow.ShowForm(nil)
|
||||
}
|
||||
|
||||
func stepPtr(s Step) *Step { return &s }
|
||||
|
||||
func (f *Flow) redirect(c echo.Context) {
|
||||
c.Request().Header.Set("Location", f.request.RedirectURI)
|
||||
func (f *LoginFlow) redirect(c echo.Context) {
|
||||
c.Request().Header.Set("Location", f.RedirectURI)
|
||||
}
|
||||
|
||||
func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
||||
if f.StepID == nil {
|
||||
c.Logger().Error("stepID is nil")
|
||||
return c.String(http.StatusInternalServerError, "No Step ID")
|
||||
}
|
||||
|
||||
switch *f.StepID {
|
||||
case StepInit:
|
||||
func (f *LoginFlow) progress(a *Authenticator, c echo.Context) error {
|
||||
switch f.Step() {
|
||||
case flow.StepInit:
|
||||
rm := make(map[string]interface{})
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
for _, si := range f.Schema {
|
||||
if si.Required {
|
||||
if _, ok := rm[si.Name]; !ok {
|
||||
return c.String(http.StatusBadRequest, "missing required param "+si.Name)
|
||||
}
|
||||
}
|
||||
err = f.Schema.CheckRequired(rm)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, f.ShowForm([]string{err.Error()}))
|
||||
}
|
||||
|
||||
user, err := a.Check(f, c.Request(), rm)
|
||||
switch err {
|
||||
case nil:
|
||||
var finishedFlow struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
finishedFlow := flow.Result{}
|
||||
a.flows.Remove(f)
|
||||
copier.Copy(&finishedFlow, f)
|
||||
finishedFlow.Type = TypeCreateEntry
|
||||
finishedFlow.Title = common.AppName
|
||||
finishedFlow.Version = 1
|
||||
finishedFlow.Result = a.NewAccessToken(c.Request(), user, f)
|
||||
finishedFlow.Type = flow.TypeCreateEntry
|
||||
finishedFlow.Title = common.AppNamePtr()
|
||||
finishedFlow.Version = common.IntPtr(1)
|
||||
finishedFlow.Result = a.NewAccessToken(c.Request(), user)
|
||||
|
||||
f.redirect(c)
|
||||
|
||||
|
@ -170,24 +123,26 @@ func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
|||
case ErrInvalidAuth:
|
||||
fallthrough
|
||||
default:
|
||||
f.Errors = map[string]interface{}{
|
||||
return c.JSON(http.StatusOK, f.ShowForm(map[string]interface{}{
|
||||
"base": "invalid_auth",
|
||||
}
|
||||
return c.JSON(http.StatusOK, f)
|
||||
}))
|
||||
}
|
||||
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 {
|
||||
flowID := c.Param("flow_id")
|
||||
flowID := flow.FlowID(c.Param("flow_id"))
|
||||
|
||||
if flowID == "" {
|
||||
return c.String(http.StatusBadRequest, "empty flow ID")
|
||||
}
|
||||
|
||||
delete(a.flows, FlowID(flowID))
|
||||
a.flows.Delete(flowID)
|
||||
|
||||
return c.String(http.StatusOK, "deleted")
|
||||
}
|
||||
|
@ -202,12 +157,14 @@ func setJSON(c echo.Context) {
|
|||
func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error {
|
||||
setJSON(c)
|
||||
|
||||
var flowReq FlowRequest
|
||||
var flowReq LoginFlowRequest
|
||||
err := c.Bind(&flowReq)
|
||||
if err != nil {
|
||||
return c.String(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
flowReq.ip = c.Request().RemoteAddr
|
||||
|
||||
resp := a.NewFlow(&flowReq)
|
||||
|
||||
if resp == nil {
|
||||
|
@ -222,16 +179,10 @@ func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
|||
|
||||
flowID := c.Param("flow_id")
|
||||
|
||||
flow := a.flows.Get(FlowID(flowID))
|
||||
flow := a.flows.Get(flow.FlowID(flowID))
|
||||
if flow == nil {
|
||||
return c.String(http.StatusNotFound, "no such flow")
|
||||
}
|
||||
|
||||
if time.Now().Sub(flow.ctime) > cullAge {
|
||||
a.flows.Remove(flow)
|
||||
|
||||
return c.String(http.StatusGone, "flow timed out")
|
||||
}
|
||||
|
||||
return flow.progress(a, c)
|
||||
return flow.(*LoginFlow).progress(a, c)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||
"dynatron.me/x/blasphem/pkg/flow"
|
||||
"dynatron.me/x/blasphem/pkg/storage"
|
||||
)
|
||||
|
||||
|
@ -24,7 +25,7 @@ type HAUser struct {
|
|||
}
|
||||
|
||||
func (hau *HAUser) UserData() provider.ProviderUser {
|
||||
return &UserData{
|
||||
return &UserData{ // strip secret
|
||||
Username: hau.Username,
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +45,7 @@ func (h *HAUser) ProviderUserData() interface{} { return h.UserData() }
|
|||
type HomeAssistantProvider struct {
|
||||
provider.AuthProviderBase `json:"-"`
|
||||
Users []HAUser `json:"users"`
|
||||
userMap map[string]*HAUser
|
||||
}
|
||||
|
||||
func NewHAProvider(s storage.Store) (provider.AuthProvider, error) {
|
||||
|
@ -59,13 +61,25 @@ func NewHAProvider(s storage.Store) (provider.AuthProvider, error) {
|
|||
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.userMap[u.Username] = &hap.Users[i]
|
||||
}
|
||||
|
||||
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) {
|
||||
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{} {
|
||||
return &UserData{}
|
||||
return &HAUser{}
|
||||
}
|
||||
|
||||
func (hap *HomeAssistantProvider) FlowSchema() []provider.FlowSchemaItem {
|
||||
return []provider.FlowSchemaItem{
|
||||
func (hap *HomeAssistantProvider) FlowSchema() flow.Schema {
|
||||
return []flow.SchemaItem{
|
||||
{
|
||||
Type: "string",
|
||||
Name: "username",
|
||||
|
|
|
@ -3,6 +3,7 @@ package provider
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"dynatron.me/x/blasphem/pkg/flow"
|
||||
"dynatron.me/x/blasphem/pkg/storage"
|
||||
)
|
||||
|
||||
|
@ -13,9 +14,10 @@ var Providers = make(map[string]Constructor)
|
|||
type AuthProvider interface { // TODO: this should include stepping
|
||||
AuthProviderMetadata
|
||||
ProviderBase() AuthProviderBase
|
||||
FlowSchema() []FlowSchemaItem
|
||||
FlowSchema() flow.Schema
|
||||
NewCredData() interface{}
|
||||
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)) {
|
||||
|
@ -23,6 +25,7 @@ func Register(providerName string, f func(storage.Store) (AuthProvider, error))
|
|||
}
|
||||
|
||||
type ProviderUser interface {
|
||||
// TODO: make sure this is sane with all the ProviderUser and UserData type stuff
|
||||
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) ProviderType() string { return bp.Type }
|
||||
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
|
||||
|
||||
type FlowSchemaItem struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||
"dynatron.me/x/blasphem/pkg/flow"
|
||||
"dynatron.me/x/blasphem/pkg/storage"
|
||||
)
|
||||
|
||||
|
@ -46,6 +47,10 @@ func New(s storage.Store) (provider.AuthProvider, error) {
|
|||
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) {
|
||||
/*
|
||||
if req.RemoteAddr in allowed then do the thing
|
||||
|
@ -57,8 +62,8 @@ func (hap *TrustedNetworksProvider) NewCredData() interface{} {
|
|||
return &UserData{}
|
||||
}
|
||||
|
||||
func (hap *TrustedNetworksProvider) FlowSchema() []provider.FlowSchemaItem {
|
||||
return []provider.FlowSchemaItem{
|
||||
func (hap *TrustedNetworksProvider) FlowSchema() flow.Schema {
|
||||
return []flow.SchemaItem{
|
||||
{
|
||||
Type: "string",
|
||||
Name: "username",
|
||||
|
|
|
@ -124,16 +124,14 @@ func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request
|
|||
return nil
|
||||
}
|
||||
|
||||
cred := &Credential{
|
||||
user: user,
|
||||
}
|
||||
cred := a.store.Credential(user)
|
||||
|
||||
return cred
|
||||
}
|
||||
|
||||
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())
|
||||
now := time.Now()
|
||||
|
||||
|
@ -158,7 +156,7 @@ const (
|
|||
GTRefreshToken GrantType = "refresh_token"
|
||||
)
|
||||
|
||||
type ClientID string
|
||||
type ClientID common.ClientID
|
||||
|
||||
func (c *ClientID) IsValid() bool {
|
||||
// TODO: || !indieauth.VerifyClientID(rq.ClientID)?
|
||||
|
|
|
@ -14,6 +14,7 @@ const (
|
|||
|
||||
type AuthStore interface {
|
||||
User(UserID) *User
|
||||
Credential(provider.ProviderUser) *Credential
|
||||
}
|
||||
|
||||
type authStore struct {
|
||||
|
@ -22,7 +23,17 @@ type authStore struct {
|
|||
Credentials []Credential `json:"credentials"`
|
||||
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) {
|
||||
|
@ -30,6 +41,7 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
|
|||
err = s.Get(AuthStoreKey, as)
|
||||
|
||||
as.userMap = make(map[UserID]*User)
|
||||
as.providerUsers = make(map[provider.ProviderUser]*Credential)
|
||||
|
||||
for _, u := range as.Users {
|
||||
as.userMap[u.ID] = &u
|
||||
|
@ -49,7 +61,11 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
|
|||
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
201
pkg/flow/flow.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue