2015-08-18 05:57:27 +05:30
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2016-02-10 01:52:40 +05:30
|
|
|
"encoding/json"
|
2015-08-18 05:57:27 +05:30
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
2016-04-05 06:35:00 +05:30
|
|
|
"io"
|
2015-08-18 05:57:27 +05:30
|
|
|
"net/url"
|
|
|
|
"os"
|
2016-07-23 02:29:34 +05:30
|
|
|
"path"
|
2015-08-18 05:57:27 +05:30
|
|
|
"path/filepath"
|
|
|
|
texttemplate "text/template"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/coreos/go-oidc/key"
|
|
|
|
"github.com/coreos/pkg/health"
|
2016-02-10 06:01:32 +05:30
|
|
|
"github.com/go-gorp/gorp"
|
2015-08-18 05:57:27 +05:30
|
|
|
|
2016-04-15 04:27:53 +05:30
|
|
|
"github.com/coreos/dex/client"
|
2016-05-12 22:23:01 +05:30
|
|
|
clientmanager "github.com/coreos/dex/client/manager"
|
2015-08-18 05:57:27 +05:30
|
|
|
"github.com/coreos/dex/connector"
|
|
|
|
"github.com/coreos/dex/db"
|
|
|
|
"github.com/coreos/dex/email"
|
2016-02-10 00:40:28 +05:30
|
|
|
sessionmanager "github.com/coreos/dex/session/manager"
|
2015-08-18 05:57:27 +05:30
|
|
|
"github.com/coreos/dex/user"
|
|
|
|
useremail "github.com/coreos/dex/user/email"
|
2016-02-10 00:40:28 +05:30
|
|
|
usermanager "github.com/coreos/dex/user/manager"
|
2015-08-18 05:57:27 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
type ServerConfig struct {
|
2016-08-01 13:47:06 +05:30
|
|
|
IssuerURL string
|
|
|
|
IssuerName string
|
|
|
|
IssuerLogoURL string
|
|
|
|
TemplateDir string
|
|
|
|
EmailTemplateDirs []string
|
|
|
|
EmailFromAddress string
|
|
|
|
EmailerConfigFile string
|
|
|
|
StateConfig StateConfigurer
|
|
|
|
EnableRegistration bool
|
|
|
|
EnableClientRegistration bool
|
|
|
|
EnableClientCredentialAccess bool
|
|
|
|
RegisterOnFirstLogin bool
|
2015-08-18 05:57:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
type StateConfigurer interface {
|
|
|
|
Configure(*Server) error
|
|
|
|
}
|
|
|
|
|
|
|
|
type SingleServerConfig struct {
|
|
|
|
ClientsFile string
|
|
|
|
ConnectorsFile string
|
|
|
|
UsersFile string
|
|
|
|
}
|
|
|
|
|
|
|
|
type MultiServerConfig struct {
|
2015-08-26 05:14:38 +05:30
|
|
|
KeySecrets [][]byte
|
2015-08-18 05:57:27 +05:30
|
|
|
DatabaseConfig db.Config
|
2015-10-29 05:19:25 +05:30
|
|
|
UseOldFormat bool
|
2015-08-18 05:57:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
func (cfg *ServerConfig) Server() (*Server, error) {
|
|
|
|
iu, err := url.Parse(cfg.IssuerURL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2016-07-23 02:29:34 +05:30
|
|
|
tpl, err := getTemplates(cfg.IssuerName, cfg.IssuerURL, cfg.IssuerLogoURL, cfg.EnableRegistration, cfg.TemplateDir)
|
2015-08-18 05:57:27 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
km := key.NewPrivateKeyManager()
|
|
|
|
srv := Server{
|
|
|
|
IssuerURL: *iu,
|
|
|
|
KeyManager: km,
|
|
|
|
Templates: tpl,
|
|
|
|
|
|
|
|
HealthChecks: []health.Checkable{km},
|
|
|
|
Connectors: []connector.Connector{},
|
2015-10-01 05:05:58 +05:30
|
|
|
|
2016-08-01 13:47:06 +05:30
|
|
|
EnableRegistration: cfg.EnableRegistration,
|
|
|
|
EnableClientRegistration: cfg.EnableClientRegistration,
|
|
|
|
EnableClientCredentialAccess: cfg.EnableClientCredentialAccess,
|
|
|
|
RegisterOnFirstLogin: cfg.RegisterOnFirstLogin,
|
2015-08-18 05:57:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
err = cfg.StateConfig.Configure(&srv)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = setTemplates(&srv, tpl)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2015-10-31 03:30:40 +05:30
|
|
|
err = setEmailer(&srv, cfg.IssuerName, cfg.EmailFromAddress, cfg.EmailerConfigFile, cfg.EmailTemplateDirs)
|
2015-08-18 05:57:27 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &srv, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cfg *SingleServerConfig) Configure(srv *Server) error {
|
|
|
|
k, err := key.GeneratePrivateKey()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-02-10 01:52:40 +05:30
|
|
|
dbMap := db.NewMemDB()
|
|
|
|
|
2015-08-18 05:57:27 +05:30
|
|
|
ks := key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(24*time.Hour))
|
|
|
|
kRepo := key.NewPrivateKeySetRepo()
|
|
|
|
if err = kRepo.Set(ks); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-02-10 05:48:05 +05:30
|
|
|
clients, err := loadClients(cfg.ClientsFile)
|
2015-08-18 05:57:27 +05:30
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to read clients from file %s: %v", cfg.ClientsFile, err)
|
|
|
|
}
|
2016-05-12 22:23:01 +05:30
|
|
|
|
2016-06-07 06:28:16 +05:30
|
|
|
clientRepo, err := db.NewClientRepoFromClients(dbMap, clients)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2016-02-10 04:36:07 +05:30
|
|
|
}
|
2015-08-18 05:57:27 +05:30
|
|
|
|
2016-01-19 22:29:34 +05:30
|
|
|
f, err := os.Open(cfg.ConnectorsFile)
|
2015-08-18 05:57:27 +05:30
|
|
|
if err != nil {
|
2016-01-19 22:29:34 +05:30
|
|
|
return fmt.Errorf("opening connectors file: %v", err)
|
2015-08-18 05:57:27 +05:30
|
|
|
}
|
2016-01-19 22:29:34 +05:30
|
|
|
defer f.Close()
|
|
|
|
cfgs, err := connector.ReadConfigs(f)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("decoding connector configs: %v", err)
|
|
|
|
}
|
2016-02-10 05:14:38 +05:30
|
|
|
cfgRepo := db.NewConnectorConfigRepo(dbMap)
|
|
|
|
if err := cfgRepo.Set(cfgs); err != nil {
|
|
|
|
return fmt.Errorf("failed to set connectors: %v", err)
|
|
|
|
}
|
2015-08-18 05:57:27 +05:30
|
|
|
|
2016-02-10 01:52:40 +05:30
|
|
|
sRepo := db.NewSessionRepo(dbMap)
|
|
|
|
skRepo := db.NewSessionKeyRepo(dbMap)
|
2016-02-10 00:40:28 +05:30
|
|
|
sm := sessionmanager.NewSessionManager(sRepo, skRepo)
|
2015-08-18 05:57:27 +05:30
|
|
|
|
2016-04-05 06:35:00 +05:30
|
|
|
users, pwis, err := loadUsers(cfg.UsersFile)
|
2015-08-18 05:57:27 +05:30
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to read users from file: %v", err)
|
|
|
|
}
|
2016-02-10 01:52:40 +05:30
|
|
|
userRepo, err := db.NewUserRepoFromUsers(dbMap, users)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-08-18 05:57:27 +05:30
|
|
|
|
2016-04-05 06:35:00 +05:30
|
|
|
pwiRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, pwis)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-08-18 05:57:27 +05:30
|
|
|
|
2016-02-10 01:52:40 +05:30
|
|
|
refTokRepo := db.NewRefreshTokenRepo(dbMap)
|
2015-08-18 05:57:27 +05:30
|
|
|
|
2016-02-10 01:52:40 +05:30
|
|
|
txnFactory := db.TransactionFactory(dbMap)
|
2016-02-10 00:40:28 +05:30
|
|
|
userManager := usermanager.NewUserManager(userRepo, pwiRepo, cfgRepo, txnFactory, usermanager.ManagerOptions{})
|
2016-06-07 06:28:16 +05:30
|
|
|
clientManager := clientmanager.NewClientManager(clientRepo, db.TransactionFactory(dbMap), clientmanager.ManagerOptions{})
|
2016-05-12 22:23:01 +05:30
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to create client identity manager: %v", err)
|
|
|
|
}
|
|
|
|
srv.ClientRepo = clientRepo
|
|
|
|
srv.ClientManager = clientManager
|
2015-08-18 05:57:27 +05:30
|
|
|
srv.KeySetRepo = kRepo
|
|
|
|
srv.ConnectorConfigRepo = cfgRepo
|
|
|
|
srv.UserRepo = userRepo
|
|
|
|
srv.UserManager = userManager
|
|
|
|
srv.PasswordInfoRepo = pwiRepo
|
|
|
|
srv.SessionManager = sm
|
|
|
|
srv.RefreshTokenRepo = refTokRepo
|
2016-03-01 00:57:17 +05:30
|
|
|
srv.HealthChecks = append(srv.HealthChecks, db.NewHealthChecker(dbMap))
|
2016-04-06 23:59:33 +05:30
|
|
|
srv.dbMap = dbMap
|
2015-08-18 05:57:27 +05:30
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-04-05 06:35:00 +05:30
|
|
|
// loadUsers parses the user.json file and returns the users to be created.
|
|
|
|
func loadUsers(filepath string) ([]user.UserWithRemoteIdentities, []user.PasswordInfo, error) {
|
2016-02-10 01:52:40 +05:30
|
|
|
f, err := os.Open(filepath)
|
|
|
|
if err != nil {
|
2016-04-05 06:35:00 +05:30
|
|
|
return nil, nil, err
|
2016-02-10 01:52:40 +05:30
|
|
|
}
|
|
|
|
defer f.Close()
|
2016-04-05 06:35:00 +05:30
|
|
|
return loadUsersFromReader(f)
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadUsersFromReader(r io.Reader) (users []user.UserWithRemoteIdentities, pwis []user.PasswordInfo, err error) {
|
|
|
|
// Encoding used by the user config file.
|
|
|
|
var configUsers []struct {
|
|
|
|
user.User
|
|
|
|
Password string `json:"password"`
|
|
|
|
RemoteIdentities []user.RemoteIdentity `json:"remoteIdentities"`
|
2016-04-12 04:45:44 +05:30
|
|
|
|
|
|
|
// The old format stored all user data under the "user" key.
|
|
|
|
// Attempt to detect that, and print an better error.
|
|
|
|
OldUserFields map[string]string `json:"user"`
|
2016-04-05 06:35:00 +05:30
|
|
|
}
|
|
|
|
if err := json.NewDecoder(r).Decode(&configUsers); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
users = make([]user.UserWithRemoteIdentities, len(configUsers))
|
|
|
|
pwis = make([]user.PasswordInfo, len(configUsers))
|
|
|
|
|
|
|
|
for i, u := range configUsers {
|
2016-04-12 04:45:44 +05:30
|
|
|
if u.OldUserFields != nil {
|
|
|
|
return nil, nil, fmt.Errorf("Static user file is using an outdated format. Please refer to example in static/fixtures.")
|
|
|
|
}
|
|
|
|
|
2016-04-05 06:35:00 +05:30
|
|
|
users[i] = user.UserWithRemoteIdentities{
|
|
|
|
User: u.User,
|
|
|
|
RemoteIdentities: u.RemoteIdentities,
|
|
|
|
}
|
|
|
|
hashedPassword, err := user.NewPasswordFromPlaintext(u.Password)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
pwis[i] = user.PasswordInfo{UserID: u.ID, Password: hashedPassword}
|
|
|
|
}
|
2016-02-10 01:52:40 +05:30
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-04-15 04:27:53 +05:30
|
|
|
// loadClients parses the clients.json file and returns a list of clients.
|
2016-06-09 00:01:50 +05:30
|
|
|
func loadClients(filepath string) ([]client.LoadableClient, error) {
|
2016-02-10 05:48:05 +05:30
|
|
|
f, err := os.Open(filepath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
2016-04-15 04:27:53 +05:30
|
|
|
return client.ClientsFromReader(f)
|
2016-02-10 05:48:05 +05:30
|
|
|
}
|
|
|
|
|
2015-08-18 05:57:27 +05:30
|
|
|
func (cfg *MultiServerConfig) Configure(srv *Server) error {
|
2015-08-26 05:14:38 +05:30
|
|
|
if len(cfg.KeySecrets) == 0 {
|
2015-08-18 05:57:27 +05:30
|
|
|
return errors.New("missing key secret")
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.DatabaseConfig.DSN == "" {
|
|
|
|
return errors.New("missing database connection string")
|
|
|
|
}
|
|
|
|
|
|
|
|
dbc, err := db.NewConnection(cfg.DatabaseConfig)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to initialize database connection: %v", err)
|
|
|
|
}
|
2016-02-10 06:01:32 +05:30
|
|
|
if _, ok := dbc.Dialect.(gorp.PostgresDialect); !ok {
|
|
|
|
return errors.New("only postgres backend supported for multi server configurations")
|
|
|
|
}
|
2015-08-18 05:57:27 +05:30
|
|
|
|
2015-10-29 05:19:25 +05:30
|
|
|
kRepo, err := db.NewPrivateKeySetRepo(dbc, cfg.UseOldFormat, cfg.KeySecrets...)
|
2015-08-18 05:57:27 +05:30
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to create PrivateKeySetRepo: %v", err)
|
|
|
|
}
|
|
|
|
|
2016-04-15 04:57:57 +05:30
|
|
|
ciRepo := db.NewClientRepo(dbc)
|
2015-08-18 05:57:27 +05:30
|
|
|
sRepo := db.NewSessionRepo(dbc)
|
|
|
|
skRepo := db.NewSessionKeyRepo(dbc)
|
|
|
|
cfgRepo := db.NewConnectorConfigRepo(dbc)
|
|
|
|
userRepo := db.NewUserRepo(dbc)
|
|
|
|
pwiRepo := db.NewPasswordInfoRepo(dbc)
|
2016-02-10 00:40:28 +05:30
|
|
|
userManager := usermanager.NewUserManager(userRepo, pwiRepo, cfgRepo, db.TransactionFactory(dbc), usermanager.ManagerOptions{})
|
2016-05-12 22:23:01 +05:30
|
|
|
clientManager := clientmanager.NewClientManager(ciRepo, db.TransactionFactory(dbc), clientmanager.ManagerOptions{})
|
2015-08-18 05:57:27 +05:30
|
|
|
refreshTokenRepo := db.NewRefreshTokenRepo(dbc)
|
|
|
|
|
2016-02-10 00:40:28 +05:30
|
|
|
sm := sessionmanager.NewSessionManager(sRepo, skRepo)
|
2015-08-18 05:57:27 +05:30
|
|
|
|
2016-04-15 04:57:57 +05:30
|
|
|
srv.ClientRepo = ciRepo
|
2016-05-12 22:23:01 +05:30
|
|
|
srv.ClientManager = clientManager
|
2015-08-18 05:57:27 +05:30
|
|
|
srv.KeySetRepo = kRepo
|
|
|
|
srv.ConnectorConfigRepo = cfgRepo
|
|
|
|
srv.UserRepo = userRepo
|
|
|
|
srv.UserManager = userManager
|
|
|
|
srv.PasswordInfoRepo = pwiRepo
|
|
|
|
srv.SessionManager = sm
|
|
|
|
srv.RefreshTokenRepo = refreshTokenRepo
|
2016-03-01 00:57:17 +05:30
|
|
|
srv.HealthChecks = append(srv.HealthChecks, db.NewHealthChecker(dbc))
|
2016-04-06 23:59:33 +05:30
|
|
|
srv.dbMap = dbc
|
2015-08-18 05:57:27 +05:30
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-07-23 02:29:34 +05:30
|
|
|
func getTemplates(issuerName, issuerURL, issuerLogoURL string,
|
2015-10-01 05:37:00 +05:30
|
|
|
enableRegister bool, dir string) (*template.Template, error) {
|
2016-07-23 02:29:34 +05:30
|
|
|
u, err := url.Parse(issuerURL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
issuerPath := u.Path
|
|
|
|
|
2015-09-01 04:32:47 +05:30
|
|
|
tpl := template.New("").Funcs(map[string]interface{}{
|
|
|
|
"issuerName": func() string {
|
|
|
|
return issuerName
|
|
|
|
},
|
|
|
|
"issuerLogoURL": func() string {
|
|
|
|
return issuerLogoURL
|
|
|
|
},
|
2015-10-01 05:05:58 +05:30
|
|
|
"enableRegister": func() bool {
|
|
|
|
return enableRegister
|
|
|
|
},
|
2016-07-23 02:29:34 +05:30
|
|
|
"absPath": func(p string) string {
|
|
|
|
return path.Join(issuerPath, p)
|
|
|
|
},
|
2015-09-01 04:32:47 +05:30
|
|
|
})
|
|
|
|
|
|
|
|
return tpl.ParseGlob(dir + "/*.html")
|
2015-08-18 05:57:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
func setTemplates(srv *Server, tpls *template.Template) error {
|
2016-06-20 23:07:03 +05:30
|
|
|
for _, t := range []struct {
|
|
|
|
templateName string
|
|
|
|
templatePtr **template.Template
|
|
|
|
}{
|
|
|
|
{LoginPageTemplateName, &srv.LoginTemplate},
|
|
|
|
{RegisterTemplateName, &srv.RegisterTemplate},
|
|
|
|
{VerifyEmailTemplateName, &srv.VerifyEmailTemplate},
|
|
|
|
{SendResetPasswordEmailTemplateName, &srv.SendResetPasswordEmailTemplate},
|
|
|
|
{ResetPasswordTemplateName, &srv.ResetPasswordTemplate},
|
|
|
|
{OOBTemplateName, &srv.OOBTemplate},
|
|
|
|
} {
|
|
|
|
tpl, err := findTemplate(t.templateName, tpls)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
*t.templatePtr = tpl
|
2015-08-18 05:57:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-10-31 03:30:40 +05:30
|
|
|
func setEmailer(srv *Server, issuerName, fromAddress, emailerConfigFile string, emailTemplateDirs []string) error {
|
2015-08-18 05:57:27 +05:30
|
|
|
|
|
|
|
cfg, err := email.NewEmailerConfigFromFile(emailerConfigFile)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-06-28 12:04:55 +05:30
|
|
|
emailer, err := cfg.Emailer(fromAddress)
|
2015-08-18 05:57:27 +05:30
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
getFileNames := func(dir, ext string) ([]string, error) {
|
|
|
|
fns, err := filepath.Glob(dir + "/*." + ext)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return fns, nil
|
|
|
|
}
|
|
|
|
getTextFiles := func(dir string) ([]string, error) {
|
|
|
|
return getFileNames(dir, "txt")
|
|
|
|
}
|
|
|
|
getHTMLFiles := func(dir string) ([]string, error) {
|
|
|
|
return getFileNames(dir, "html")
|
|
|
|
}
|
|
|
|
|
|
|
|
textTemplates := texttemplate.New("textTemplates")
|
|
|
|
htmlTemplates := template.New("htmlTemplates")
|
|
|
|
for _, dir := range emailTemplateDirs {
|
|
|
|
textFileNames, err := getTextFiles(dir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(textFileNames) != 0 {
|
|
|
|
textTemplates, err = textTemplates.ParseFiles(textFileNames...)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
htmlFileNames, err := getHTMLFiles(dir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(htmlFileNames) != 0 {
|
|
|
|
htmlTemplates, err = htmlTemplates.ParseFiles(htmlFileNames...)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2016-06-28 12:04:55 +05:30
|
|
|
tMailer := email.NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer, fromAddress)
|
2015-10-31 03:30:40 +05:30
|
|
|
tMailer.SetGlobalContext(map[string]interface{}{
|
|
|
|
"issuer_name": issuerName,
|
|
|
|
})
|
2015-08-18 05:57:27 +05:30
|
|
|
|
|
|
|
ue := useremail.NewUserEmailer(srv.UserRepo,
|
|
|
|
srv.PasswordInfoRepo,
|
|
|
|
srv.KeyManager.Signer,
|
|
|
|
srv.SessionManager.ValidityWindow,
|
|
|
|
srv.IssuerURL,
|
|
|
|
tMailer,
|
|
|
|
srv.absURL(httpPathResetPassword),
|
2015-11-10 04:05:11 +05:30
|
|
|
srv.absURL(httpPathEmailVerify),
|
|
|
|
srv.absURL(httpPathAcceptInvitation),
|
|
|
|
)
|
2015-08-18 05:57:27 +05:30
|
|
|
|
|
|
|
srv.UserEmailer = ue
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func findTemplate(name string, tpls *template.Template) (*template.Template, error) {
|
|
|
|
tpl := tpls.Lookup(name)
|
|
|
|
if tpl == nil {
|
|
|
|
return nil, fmt.Errorf("unable to find template: %q", name)
|
|
|
|
}
|
|
|
|
return tpl, nil
|
|
|
|
}
|