From 32d14db46dd000ac976c0be14b7d50b5a7388112 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 12 Oct 2017 16:28:41 +0300 Subject: [PATCH] Update the _examples/mvc/login example - add a Path property at `mvc.Response` and add a `context.Proceed` helper function Former-commit-id: b898901fe4a324e888a6e09c93530cf7a551cf2a --- _examples/README.md | 2 +- _examples/mvc/login/_ugly/main.go | 43 ++++ .../mvc/login/{ => _ugly}/public/css/site.css | 0 _examples/mvc/login/{ => _ugly}/user/auth.go | 0 .../mvc/login/{ => _ugly}/user/controller.go | 0 .../mvc/login/{ => _ugly}/user/datasource.go | 0 _examples/mvc/login/{ => _ugly}/user/model.go | 0 .../login/{ => _ugly}/views/shared/error.html | 0 .../{ => _ugly}/views/shared/layout.html | 0 .../login/{ => _ugly}/views/user/login.html | 0 .../mvc/login/{ => _ugly}/views/user/me.html | 0 .../{ => _ugly}/views/user/notfound.html | 0 .../{ => _ugly}/views/user/register.html | 0 _examples/mvc/login/database/database.go | 20 -- _examples/mvc/login/datamodels/user.go | 41 ++++ _examples/mvc/login/datasource/users.go | 31 +++ _examples/mvc/login/main.go | 68 +++++-- .../mvc/login/repositories/user_repository.go | 173 ++++++++++++++++ _examples/mvc/login/services/user_service.go | 125 ++++++++++++ .../login/web/controllers/user_controller.go | 189 ++++++++++++++++++ .../login/web/controllers/users_controller.go | 101 ++++++++++ .../mvc/login/web/middleware/basicauth.go | 12 ++ _examples/mvc/login/web/public/css/site.css | 61 ++++++ _examples/mvc/login/web/viewmodels/README.md | 55 +++++ .../mvc/login/web/views/shared/error.html | 15 ++ .../mvc/login/web/views/shared/layout.html | 12 ++ _examples/mvc/login/web/views/user/login.html | 11 + _examples/mvc/login/web/views/user/me.html | 3 + .../mvc/login/web/views/user/register.html | 14 ++ context/context.go | 111 +++++++++- mvc/activator/activator.go | 5 + mvc/method_result_response.go | 26 +++ mvc/method_result_view.go | 5 + 33 files changed, 1073 insertions(+), 50 deletions(-) create mode 100644 _examples/mvc/login/_ugly/main.go rename _examples/mvc/login/{ => _ugly}/public/css/site.css (100%) rename _examples/mvc/login/{ => _ugly}/user/auth.go (100%) rename _examples/mvc/login/{ => _ugly}/user/controller.go (100%) rename _examples/mvc/login/{ => _ugly}/user/datasource.go (100%) rename _examples/mvc/login/{ => _ugly}/user/model.go (100%) rename _examples/mvc/login/{ => _ugly}/views/shared/error.html (100%) rename _examples/mvc/login/{ => _ugly}/views/shared/layout.html (100%) rename _examples/mvc/login/{ => _ugly}/views/user/login.html (100%) rename _examples/mvc/login/{ => _ugly}/views/user/me.html (100%) rename _examples/mvc/login/{ => _ugly}/views/user/notfound.html (100%) rename _examples/mvc/login/{ => _ugly}/views/user/register.html (100%) delete mode 100644 _examples/mvc/login/database/database.go create mode 100644 _examples/mvc/login/datamodels/user.go create mode 100644 _examples/mvc/login/datasource/users.go create mode 100644 _examples/mvc/login/repositories/user_repository.go create mode 100644 _examples/mvc/login/services/user_service.go create mode 100644 _examples/mvc/login/web/controllers/user_controller.go create mode 100644 _examples/mvc/login/web/controllers/users_controller.go create mode 100644 _examples/mvc/login/web/middleware/basicauth.go create mode 100644 _examples/mvc/login/web/public/css/site.css create mode 100644 _examples/mvc/login/web/viewmodels/README.md create mode 100644 _examples/mvc/login/web/views/shared/error.html create mode 100644 _examples/mvc/login/web/views/shared/layout.html create mode 100644 _examples/mvc/login/web/views/user/login.html create mode 100644 _examples/mvc/login/web/views/user/me.html create mode 100644 _examples/mvc/login/web/views/user/register.html diff --git a/_examples/README.md b/_examples/README.md index 5045ba06..b44c2f3e 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -205,12 +205,12 @@ If you're new to back-end web development read about the MVC architectural patte Follow the examples below, - [Overview - Plus Repository and Service layers](mvc/overview) **NEW** +- [Login showcase - Plus Repository and Service layers](mvc/login) **NEW** ### Subdomains diff --git a/_examples/mvc/login/_ugly/main.go b/_examples/mvc/login/_ugly/main.go new file mode 100644 index 00000000..ea0f2851 --- /dev/null +++ b/_examples/mvc/login/_ugly/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "time" + + "github.com/kataras/iris/_examples/mvc/login/_ugly/user" + + "github.com/kataras/iris" + "github.com/kataras/iris/sessions" +) + +func main() { + app := iris.New() + // You got full debug messages, useful when using MVC and you want to make + // sure that your code is aligned with the Iris' MVC Architecture. + app.Logger().SetLevel("debug") + + app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html")) + + app.StaticWeb("/public", "./public") + + manager := sessions.New(sessions.Config{ + Cookie: "sessioncookiename", + Expires: 24 * time.Hour, + }) + users := user.NewDataSource() + + app.Controller("/user", new(user.Controller), manager, users) + + // http://localhost:8080/user/register + // http://localhost:8080/user/login + // http://localhost:8080/user/me + // http://localhost:8080/user/logout + // http://localhost:8080/user/1 + app.Run(iris.Addr(":8080"), configure) +} + +func configure(app *iris.Application) { + app.Configure( + iris.WithoutServerError(iris.ErrServerClosed), + iris.WithCharset("UTF-8"), + ) +} diff --git a/_examples/mvc/login/public/css/site.css b/_examples/mvc/login/_ugly/public/css/site.css similarity index 100% rename from _examples/mvc/login/public/css/site.css rename to _examples/mvc/login/_ugly/public/css/site.css diff --git a/_examples/mvc/login/user/auth.go b/_examples/mvc/login/_ugly/user/auth.go similarity index 100% rename from _examples/mvc/login/user/auth.go rename to _examples/mvc/login/_ugly/user/auth.go diff --git a/_examples/mvc/login/user/controller.go b/_examples/mvc/login/_ugly/user/controller.go similarity index 100% rename from _examples/mvc/login/user/controller.go rename to _examples/mvc/login/_ugly/user/controller.go diff --git a/_examples/mvc/login/user/datasource.go b/_examples/mvc/login/_ugly/user/datasource.go similarity index 100% rename from _examples/mvc/login/user/datasource.go rename to _examples/mvc/login/_ugly/user/datasource.go diff --git a/_examples/mvc/login/user/model.go b/_examples/mvc/login/_ugly/user/model.go similarity index 100% rename from _examples/mvc/login/user/model.go rename to _examples/mvc/login/_ugly/user/model.go diff --git a/_examples/mvc/login/views/shared/error.html b/_examples/mvc/login/_ugly/views/shared/error.html similarity index 100% rename from _examples/mvc/login/views/shared/error.html rename to _examples/mvc/login/_ugly/views/shared/error.html diff --git a/_examples/mvc/login/views/shared/layout.html b/_examples/mvc/login/_ugly/views/shared/layout.html similarity index 100% rename from _examples/mvc/login/views/shared/layout.html rename to _examples/mvc/login/_ugly/views/shared/layout.html diff --git a/_examples/mvc/login/views/user/login.html b/_examples/mvc/login/_ugly/views/user/login.html similarity index 100% rename from _examples/mvc/login/views/user/login.html rename to _examples/mvc/login/_ugly/views/user/login.html diff --git a/_examples/mvc/login/views/user/me.html b/_examples/mvc/login/_ugly/views/user/me.html similarity index 100% rename from _examples/mvc/login/views/user/me.html rename to _examples/mvc/login/_ugly/views/user/me.html diff --git a/_examples/mvc/login/views/user/notfound.html b/_examples/mvc/login/_ugly/views/user/notfound.html similarity index 100% rename from _examples/mvc/login/views/user/notfound.html rename to _examples/mvc/login/_ugly/views/user/notfound.html diff --git a/_examples/mvc/login/views/user/register.html b/_examples/mvc/login/_ugly/views/user/register.html similarity index 100% rename from _examples/mvc/login/views/user/register.html rename to _examples/mvc/login/_ugly/views/user/register.html diff --git a/_examples/mvc/login/database/database.go b/_examples/mvc/login/database/database.go deleted file mode 100644 index 5409c6ae..00000000 --- a/_examples/mvc/login/database/database.go +++ /dev/null @@ -1,20 +0,0 @@ -package database - -// Result is our imaginary result, it will never be used, it's -// here to show you a method of doing these things. -type Result struct { - cur int -} - -// Next moves the cursor to the next result. -func (r *Result) Next() interface{} { - return nil -} - -// Database is our imaginary database interface, it will never be used here. -type Database interface { - Open(connstring string) error - Close() error - Query(q string) (result Result, err error) - Exec(q string) (lastInsertedID int64, err error) -} diff --git a/_examples/mvc/login/datamodels/user.go b/_examples/mvc/login/datamodels/user.go new file mode 100644 index 00000000..a648deb9 --- /dev/null +++ b/_examples/mvc/login/datamodels/user.go @@ -0,0 +1,41 @@ +package datamodels + +import ( + "time" + + "golang.org/x/crypto/bcrypt" +) + +// User is our User example model. +// Keep note that the tags for public-use (for our web app) +// should be kept in other file like "web/viewmodels/user.go" +// which could wrap by embedding the datamodels.User or +// define completely new fields instead but for the shake +// of the example, we will use this datamodel +// as the only one User model in our application. +type User struct { + ID int64 `json:"id" form:"id"` + Firstname string `json:"firstname" form:"firstname"` + Username string `json:"username" form:"username"` + HashedPassword []byte `json:"-" form:"-"` + CreatedAt time.Time `json:"created_at" form:"created_at"` +} + +// IsValid can do some very very simple "low-level" data validations. +func (u User) IsValid() bool { + return u.ID > 0 +} + +// GeneratePassword will generate a hashed password for us based on the +// user's input. +func GeneratePassword(userPassword string) ([]byte, error) { + return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost) +} + +// ValidatePassword will check if passwords are matched. +func ValidatePassword(userPassword string, hashed []byte) (bool, error) { + if err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword)); err != nil { + return false, err + } + return true, nil +} diff --git a/_examples/mvc/login/datasource/users.go b/_examples/mvc/login/datasource/users.go new file mode 100644 index 00000000..fb4ea62e --- /dev/null +++ b/_examples/mvc/login/datasource/users.go @@ -0,0 +1,31 @@ +// file: datasource/users.go + +package datasource + +import ( + "errors" + + "github.com/kataras/iris/_examples/mvc/login/datamodels" +) + +// Engine is from where to fetch the data, in this case the users. +type Engine uint32 + +const ( + // Memory stands for simple memory location; + // map[int64] datamodels.User ready to use, it's our source in this example. + Memory Engine = iota + // Bolt for boltdb source location. + Bolt + // MySQL for mysql-compatible source location. + MySQL +) + +// LoadUsers returns all users(empty map) from the memory, for the shake of simplicty. +func LoadUsers(engine Engine) (map[int64]datamodels.User, error) { + if engine != Memory { + return nil, errors.New("for the shake of simplicity we're using a simple map as the data source") + } + + return make(map[int64]datamodels.User), nil +} diff --git a/_examples/mvc/login/main.go b/_examples/mvc/login/main.go index 38b2f63f..fffe66c6 100644 --- a/_examples/mvc/login/main.go +++ b/_examples/mvc/login/main.go @@ -1,43 +1,71 @@ +// file: main.go + package main import ( "time" - "github.com/kataras/iris/_examples/mvc/login/user" - "github.com/kataras/iris" + "github.com/kataras/iris/_examples/mvc/login/datasource" + "github.com/kataras/iris/_examples/mvc/login/repositories" + "github.com/kataras/iris/_examples/mvc/login/services" + "github.com/kataras/iris/_examples/mvc/login/web/controllers" + "github.com/kataras/iris/_examples/mvc/login/web/middleware" "github.com/kataras/iris/sessions" ) func main() { app := iris.New() // You got full debug messages, useful when using MVC and you want to make - // sure that your code is compatible with the Iris' MVC Architecture. + // sure that your code is aligned with the Iris' MVC Architecture. app.Logger().SetLevel("debug") - app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html")) + // Load the template files. + tmpl := iris.HTML("./web/views", ".html"). + Layout("shared/layout.html"). + Reload(true) + app.RegisterView(tmpl) - app.StaticWeb("/public", "./public") + app.StaticWeb("/public", "./web/public") - manager := sessions.New(sessions.Config{ + app.OnAnyErrorCode(func(ctx iris.Context) { + ctx.ViewData("Message", ctx.Values(). + GetStringDefault("message", "The page you're looking for doesn't exist")) + ctx.View("shared/error.html") + }) + + // Create our repositories and services. + db, err := datasource.LoadUsers(datasource.Memory) + if err != nil { + app.Logger().Fatalf("error while loading the users: %v", err) + return + } + repo := repositories.NewUserRepository(db) + userService := services.NewUserService(repo) + + // Register our controllers. + app.Controller("/users", new(controllers.UsersController), + // Add the basic authentication(admin:password) middleware + // for the /users based requests. + middleware.BasicAuth, + // Bind the "userService" to the UserController's Service (interface) field. + userService, + ) + + sessManager := sessions.New(sessions.Config{ Cookie: "sessioncookiename", Expires: 24 * time.Hour, }) - users := user.NewDataSource() + app.Controller("/user", new(controllers.UserController), userService, sessManager) - app.Controller("/user", new(user.Controller), manager, users) - - // http://localhost:8080/user/register - // http://localhost:8080/user/login - // http://localhost:8080/user/me - // http://localhost:8080/user/logout - // http://localhost:8080/user/1 - app.Run(iris.Addr(":8080"), configure) -} - -func configure(app *iris.Application) { - app.Configure( + // Start the web server at localhost:8080 + // http://localhost:8080/hello + // http://localhost:8080/hello/iris + // http://localhost:8080/users/1 + app.Run( + iris.Addr("localhost:8080"), + iris.WithoutVersionChecker, iris.WithoutServerError(iris.ErrServerClosed), - iris.WithCharset("UTF-8"), + iris.WithOptimizations, // enables faster json serialization and more ) } diff --git a/_examples/mvc/login/repositories/user_repository.go b/_examples/mvc/login/repositories/user_repository.go new file mode 100644 index 00000000..60c3dd91 --- /dev/null +++ b/_examples/mvc/login/repositories/user_repository.go @@ -0,0 +1,173 @@ +package repositories + +import ( + "errors" + "sync" + + "github.com/kataras/iris/_examples/mvc/login/datamodels" +) + +// Query represents the visitor and action queries. +type Query func(datamodels.User) bool + +// UserRepository handles the basic operations of a user entity/model. +// It's an interface in order to be testable, i.e a memory user repository or +// a connected to an sql database. +type UserRepository interface { + Exec(query Query, action Query, limit int, mode int) (ok bool) + + Select(query Query) (user datamodels.User, found bool) + SelectMany(query Query, limit int) (results []datamodels.User) + + InsertOrUpdate(user datamodels.User) (updatedUser datamodels.User, err error) + Delete(query Query, limit int) (deleted bool) +} + +// NewUserRepository returns a new user memory-based repository, +// the one and only repository type in our example. +func NewUserRepository(source map[int64]datamodels.User) UserRepository { + return &userMemoryRepository{source: source} +} + +// userMemoryRepository is a "UserRepository" +// which manages the users using the memory data source (map). +type userMemoryRepository struct { + source map[int64]datamodels.User + mu sync.RWMutex +} + +const ( + // ReadOnlyMode will RLock(read) the data . + ReadOnlyMode = iota + // ReadWriteMode will Lock(read/write) the data. + ReadWriteMode +) + +func (r *userMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { + loops := 0 + + if mode == ReadOnlyMode { + r.mu.RLock() + defer r.mu.RUnlock() + } else { + r.mu.Lock() + defer r.mu.Unlock() + } + + for _, user := range r.source { + ok = query(user) + if ok { + if action(user) { + if actionLimit >= loops { + break // break + } + } + } + } + + return +} + +// Select receives a query function +// which is fired for every single user model inside +// our imaginary data source. +// When that function returns true then it stops the iteration. +// +// It returns the query's return last known boolean value +// and the last known user model +// to help callers to reduce the LOC. +// +// It's actually a simple but very clever prototype function +// I'm using everywhere since I firstly think of it, +// hope you'll find it very useful as well. +func (r *userMemoryRepository) Select(query Query) (user datamodels.User, found bool) { + found = r.Exec(query, func(m datamodels.User) bool { + user = m + return true + }, 1, ReadOnlyMode) + + // set an empty datamodels.User if not found at all. + if !found { + user = datamodels.User{} + } + + return +} + +// SelectMany same as Select but returns one or more datamodels.User as a slice. +// If limit <=0 then it returns everything. +func (r *userMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.User) { + r.Exec(query, func(m datamodels.User) bool { + results = append(results, m) + return true + }, limit, ReadOnlyMode) + + return +} + +// InsertOrUpdate adds or updates a user to the (memory) storage. +// +// Returns the new user and an error if any. +func (r *userMemoryRepository) InsertOrUpdate(user datamodels.User) (datamodels.User, error) { + id := user.ID + + if id == 0 { // Create new action + var lastID int64 + // find the biggest ID in order to not have duplications + // in productions apps you can use a third-party + // library to generate a UUID as string. + r.mu.RLock() + for _, item := range r.source { + if item.ID > lastID { + lastID = item.ID + } + } + r.mu.RUnlock() + + id = lastID + 1 + user.ID = id + + // map-specific thing + r.mu.Lock() + r.source[id] = user + r.mu.Unlock() + + return user, nil + } + + // Update action based on the user.ID, + // here we will allow updating the poster and genre if not empty. + // Alternatively we could do pure replace instead: + // r.source[id] = user + // and comment the code below; + current, exists := r.Select(func(m datamodels.User) bool { + return m.ID == id + }) + + if !exists { // ID is not a real one, return an error. + return datamodels.User{}, errors.New("failed to update a nonexistent user") + } + + // or comment these and r.source[id] = user for pure replace + if user.Username != "" { + current.Username = user.Username + } + + if user.Firstname != "" { + current.Firstname = user.Firstname + } + + // map-specific thing + r.mu.Lock() + r.source[id] = current + r.mu.Unlock() + + return user, nil +} + +func (r *userMemoryRepository) Delete(query Query, limit int) bool { + return r.Exec(query, func(m datamodels.User) bool { + delete(r.source, m.ID) + return true + }, limit, ReadWriteMode) +} diff --git a/_examples/mvc/login/services/user_service.go b/_examples/mvc/login/services/user_service.go new file mode 100644 index 00000000..9c110f99 --- /dev/null +++ b/_examples/mvc/login/services/user_service.go @@ -0,0 +1,125 @@ +package services + +import ( + "errors" + + "github.com/kataras/iris/_examples/mvc/login/datamodels" + "github.com/kataras/iris/_examples/mvc/login/repositories" +) + +// UserService handles CRUID operations of a user datamodel, +// it depends on a user repository for its actions. +// It's here to decouple the data source from the higher level compoments. +// As a result a different repository type can be used with the same logic without any aditional changes. +// It's an interface and it's used as interface everywhere +// because we may need to change or try an experimental different domain logic at the future. +type UserService interface { + GetAll() []datamodels.User + GetByID(id int64) (datamodels.User, bool) + GetByUsernameAndPassword(username, userPassword string) (datamodels.User, bool) + DeleteByID(id int64) bool + + Update(id int64, user datamodels.User) (datamodels.User, error) + UpdatePassword(id int64, newPassword string) (datamodels.User, error) + UpdateUsername(id int64, newUsername string) (datamodels.User, error) + + Create(userPassword string, user datamodels.User) (datamodels.User, error) +} + +// NewUserService returns the default user service. +func NewUserService(repo repositories.UserRepository) UserService { + return &userService{ + repo: repo, + } +} + +type userService struct { + repo repositories.UserRepository +} + +// GetAll returns all users. +func (s *userService) GetAll() []datamodels.User { + return s.repo.SelectMany(func(_ datamodels.User) bool { + return true + }, -1) +} + +// GetByID returns a user based on its id. +func (s *userService) GetByID(id int64) (datamodels.User, bool) { + return s.repo.Select(func(m datamodels.User) bool { + return m.ID == id + }) +} + +// GetByUsernameAndPassword returns a user based on its username and passowrd, +// used for authentication. +func (s *userService) GetByUsernameAndPassword(username, userPassword string) (datamodels.User, bool) { + if username == "" || userPassword == "" { + return datamodels.User{}, false + } + + return s.repo.Select(func(m datamodels.User) bool { + if m.Username == username { + hashed := m.HashedPassword + if ok, _ := datamodels.ValidatePassword(userPassword, hashed); ok { + return true + } + } + return false + }) +} + +// Update updates every field from an existing User, +// it's not safe to be used via public API, +// however we will use it on the web/controllers/user_controller.go#PutBy +// in order to show you how it works. +func (s *userService) Update(id int64, user datamodels.User) (datamodels.User, error) { + user.ID = id + return s.repo.InsertOrUpdate(user) +} + +// UpdatePassword updates a user's password. +func (s *userService) UpdatePassword(id int64, newPassword string) (datamodels.User, error) { + // update the user and return it. + hashed, err := datamodels.GeneratePassword(newPassword) + if err != nil { + return datamodels.User{}, err + } + + return s.Update(id, datamodels.User{ + HashedPassword: hashed, + }) +} + +// UpdateUsername updates a user's username. +func (s *userService) UpdateUsername(id int64, newUsername string) (datamodels.User, error) { + return s.Update(id, datamodels.User{ + Username: newUsername, + }) +} + +// Create inserts a new User, +// the userPassword is the client-typed password +// it will be hashed before the insertion to our repository. +func (s *userService) Create(userPassword string, user datamodels.User) (datamodels.User, error) { + if user.ID > 0 || userPassword == "" || user.Firstname == "" || user.Username == "" { + return datamodels.User{}, errors.New("unable to create this user") + } + + hashed, err := datamodels.GeneratePassword(userPassword) + if err != nil { + return datamodels.User{}, err + } + user.HashedPassword = hashed + + return s.repo.InsertOrUpdate(user) +} + +// DeleteByID deletes a user by its id. +// +// Returns true if deleted otherwise false. +func (s *userService) DeleteByID(id int64) bool { + return s.repo.Delete(func(m datamodels.User) bool { + return m.ID == id + }, 1) +} diff --git a/_examples/mvc/login/web/controllers/user_controller.go b/_examples/mvc/login/web/controllers/user_controller.go new file mode 100644 index 00000000..e4bda39d --- /dev/null +++ b/_examples/mvc/login/web/controllers/user_controller.go @@ -0,0 +1,189 @@ +// file: controllers/user_controller.go + +package controllers + +import ( + "github.com/kataras/iris/_examples/mvc/login/datamodels" + "github.com/kataras/iris/_examples/mvc/login/services" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/mvc" + "github.com/kataras/iris/sessions" +) + +// UserController is our /user controller. +// UserController is responsible to handle the following requests: +// GET /user/register +// POST /user/register +// GET /user/login +// POST /user/login +// GET /user/me +// All HTTP Methods /user/logout +type UserController struct { + // mvc.C is just a lightweight lightweight alternative + // to the "mvc.Controller" controller type, + // use it when you don't need mvc.Controller's fields + // (you don't need those fields when you return values from the method functions). + mvc.C + + // Our UserService, it's an interface which + // is binded from the main application. + Service services.UserService + + // Session-relative things. + Manager *sessions.Sessions + Session *sessions.Session +} + +// BeginRequest will set the current session to the controller. +// +// Remember: iris.Context and context.Context is exactly the same thing, +// iris.Context is just a type alias for go 1.9 users. +// We use context.Context here because we don't need all iris' root functions, +// when we see the import paths, we make it visible to ourselves that this file is using only the context. +func (c *UserController) BeginRequest(ctx context.Context) { + c.C.BeginRequest(ctx) + + if c.Manager == nil { + ctx.Application().Logger().Errorf(`UserController: sessions manager is nil, you should bind it`) + ctx.StopExecution() // dont run the main method handler and any "done" handlers. + return + } + + c.Session = c.Manager.Start(ctx) +} + +const userIDKey = "UserID" + +func (c *UserController) getCurrentUserID() int64 { + userID, _ := c.Session.GetInt64Default(userIDKey, 0) + return userID +} + +func (c *UserController) isLoggedIn() bool { + return c.getCurrentUserID() > 0 +} + +func (c *UserController) logout() { + c.Manager.DestroyByID(c.Session.ID()) +} + +var registerStaticView = mvc.View{ + Name: "user/register.html", + Data: context.Map{"Title": "User Registration"}, +} + +// GetRegister handles GET: http://localhost:8080/user/register. +func (c *UserController) GetRegister() mvc.Result { + if c.isLoggedIn() { + c.logout() + } + + return registerStaticView +} + +// PostRegister handles POST: http://localhost:8080/user/register. +func (c *UserController) PostRegister() mvc.Result { + // get firstname, username and password from the form. + var ( + firstname = c.Ctx.FormValue("firstname") + username = c.Ctx.FormValue("username") + password = c.Ctx.FormValue("password") + ) + + // create the new user, the password will be hashed by the service. + u, err := c.Service.Create(password, datamodels.User{ + Username: username, + Firstname: firstname, + }) + + // set the user's id to this session even if err != nil, + // the zero id doesn't matters because .getCurrentUserID() checks for that. + // If err != nil then it will be shown, see below on mvc.Response.Err: err. + c.Session.Set(userIDKey, u.ID) + + return mvc.Response{ + // if not nil then this error will be shown instead. + Err: err, + // redirect to /user/me. + Path: "/user/me", + // When redirecting from POST to GET request you -should- use this HTTP status code, + // however there're some (complicated) alternatives if you + // search online or even the HTTP RFC. + // Status "See Other" RFC 7231, however iris can automatically fix that + // but it's good to know you can set a custom code; + // Code: 303, + } + +} + +var loginStaticView = mvc.View{ + Name: "user/login.html", + Data: context.Map{"Title": "User Login"}, +} + +// GetLogin handles GET: http://localhost:8080/user/login. +func (c *UserController) GetLogin() mvc.Result { + if c.isLoggedIn() { + // if it's already logged in then destroy the previous session. + c.logout() + } + + return loginStaticView +} + +// PostLogin handles POST: http://localhost:8080/user/register. +func (c *UserController) PostLogin() mvc.Result { + var ( + username = c.Ctx.FormValue("username") + password = c.Ctx.FormValue("password") + ) + + u, found := c.Service.GetByUsernameAndPassword(username, password) + + if !found { + return mvc.Response{ + Path: "/user/register", + } + } + + c.Session.Set(userIDKey, u.ID) + + return mvc.Response{ + Path: "/user/me", + } +} + +// GetMe handles GET: http://localhost:8080/user/me. +func (c *UserController) GetMe() mvc.Result { + if !c.isLoggedIn() { + // if it's not logged in then redirect user to the login page. + return mvc.Response{Path: "/user/login"} + } + + u, found := c.Service.GetByID(c.getCurrentUserID()) + if !found { + // if the session exists but for some reason the user doesn't exist in the "database" + // then logout and re-execute the function, it will redirect the client to the + // /user/login page. + c.logout() + return c.GetMe() + } + + return mvc.View{ + Name: "user/me.html", + Data: context.Map{ + "Title": "Profile of " + u.Username, + "User": u, + }, + } +} + +// AnyLogout handles All/Any HTTP Methods for: http://localhost:8080/user/logout. +func (c *UserController) AnyLogout() { + if c.isLoggedIn() { + c.logout() + } + + c.Ctx.Redirect("/user/login") +} diff --git a/_examples/mvc/login/web/controllers/users_controller.go b/_examples/mvc/login/web/controllers/users_controller.go new file mode 100644 index 00000000..f3f7b80f --- /dev/null +++ b/_examples/mvc/login/web/controllers/users_controller.go @@ -0,0 +1,101 @@ +package controllers + +import ( + "github.com/kataras/iris/_examples/mvc/login/datamodels" + "github.com/kataras/iris/_examples/mvc/login/services" + + "github.com/kataras/iris/mvc" +) + +// UsersController is our /users API controller. +// GET /users | get all +// GET /users/{id:long} | get by id +// PUT /users/{id:long} | update by id +// DELETE /users/{id:long} | delete by id +// Requires basic authentication. +type UsersController struct { + mvc.C + + Service services.UserService +} + +// This could be possible but we should not call handlers inside the `BeginRequest`. +// Because `BeginRequest` was introduced to set common, shared variables between all method handlers +// before their execution. +// We will add this middleware from our `app.Controller` call. +// +// var authMiddleware = basicauth.New(basicauth.Config{ +// Users: map[string]string{ +// "admin": "password", +// }, +// }) +// +// func (c *UsersController) BeginRequest(ctx iris.Context) { +// c.C.BeginRequest(ctx) +// +// if !ctx.Proceed(authMiddleware) { +// ctx.StopExecution() +// } +// } + +// Get returns list of the users. +// Demo: +// curl -i -u admin:password http://localhost:8080/users +// +// The correct way if you have sensitive data: +// func (c *UsersController) Get() (results []viewmodels.User) { +// data := c.Service.GetAll() +// +// for _, user := range data { +// results = append(results, viewmodels.User{user}) +// } +// return +// } +// otherwise just return the datamodels. +func (c *UsersController) Get() (results []datamodels.User) { + return c.Service.GetAll() +} + +// GetBy returns a user. +// Demo: +// curl -i -u admin:password http://localhost:8080/users/1 +func (c *UsersController) GetBy(id int64) (user datamodels.User, found bool) { + u, found := c.Service.GetByID(id) + if !found { + // this message will be binded to the + // main.go -> app.OnAnyErrorCode -> NotFound -> shared/error.html -> .Message text. + c.Ctx.Values().Set("message", "User couldn't be found!") + } + return u, found // it will throw/emit 404 if found == false. +} + +// PutBy updates a user. +// Demo: +// curl -i -X PUT -u admin:password -F "username=kataras" +// -F "password=rawPasswordIsNotSafeIfOrNotHTTPs_You_Should_Use_A_client_side_lib_for_hash_as_well" +// http://localhost:8080/users/1 +func (c *UsersController) PutBy(id int64) (datamodels.User, error) { + // username := c.Ctx.FormValue("username") + // password := c.Ctx.FormValue("password") + u := datamodels.User{} + if err := c.Ctx.ReadForm(&u); err != nil { + return u, err + } + + return c.Service.Update(id, u) +} + +// DeleteBy deletes a user. +// Demo: +// curl -i -X DELETE -u admin:password http://localhost:8080/users/1 +func (c *UsersController) DeleteBy(id int64) interface{} { + wasDel := c.Service.DeleteByID(id) + if wasDel { + // return the deleted user's ID + return map[string]interface{}{"deleted": id} + } + // right here we can see that a method function + // can return any of those two types(map or int), + // we don't have to specify the return type to a specific type. + return 400 // same as `iris.StatusBadRequest`. +} diff --git a/_examples/mvc/login/web/middleware/basicauth.go b/_examples/mvc/login/web/middleware/basicauth.go new file mode 100644 index 00000000..b8a5355b --- /dev/null +++ b/_examples/mvc/login/web/middleware/basicauth.go @@ -0,0 +1,12 @@ +// file: middleware/basicauth.go + +package middleware + +import "github.com/kataras/iris/middleware/basicauth" + +// BasicAuth middleware sample. +var BasicAuth = basicauth.New(basicauth.Config{ + Users: map[string]string{ + "admin": "password", + }, +}) diff --git a/_examples/mvc/login/web/public/css/site.css b/_examples/mvc/login/web/public/css/site.css new file mode 100644 index 00000000..163af622 --- /dev/null +++ b/_examples/mvc/login/web/public/css/site.css @@ -0,0 +1,61 @@ +/* Bordered form */ +form { + border: 3px solid #f1f1f1; +} + +/* Full-width inputs */ +input[type=text], input[type=password] { + width: 100%; + padding: 12px 20px; + margin: 8px 0; + display: inline-block; + border: 1px solid #ccc; + box-sizing: border-box; +} + +/* Set a style for all buttons */ +button { + background-color: #4CAF50; + color: white; + padding: 14px 20px; + margin: 8px 0; + border: none; + cursor: pointer; + width: 100%; +} + +/* Add a hover effect for buttons */ +button:hover { + opacity: 0.8; +} + +/* Extra style for the cancel button (red) */ +.cancelbtn { + width: auto; + padding: 10px 18px; + background-color: #f44336; +} + +/* Center the container */ + +/* Add padding to containers */ +.container { + padding: 16px; +} + +/* The "Forgot password" text */ +span.psw { + float: right; + padding-top: 16px; +} + +/* Change styles for span and cancel button on extra small screens */ +@media screen and (max-width: 300px) { + span.psw { + display: block; + float: none; + } + .cancelbtn { + width: 100%; + } +} \ No newline at end of file diff --git a/_examples/mvc/login/web/viewmodels/README.md b/_examples/mvc/login/web/viewmodels/README.md new file mode 100644 index 00000000..f7814885 --- /dev/null +++ b/_examples/mvc/login/web/viewmodels/README.md @@ -0,0 +1,55 @@ +# View Models + +There should be the view models, the structure that the client will be able to see. + +Example: + +```go +import ( + "github.com/kataras/iris/_examples/mvc/login/datamodels" + + "github.com/kataras/iris/context" +) + +type User struct { + datamodels.User +} + +func (m User) IsValid() bool { + /* do some checks and return true if it's valid... */ + return m.ID > 0 +} +``` + +Iris is able to convert any custom data Structure into an HTTP Response Dispatcher, +so theoritically, something like the following is permitted if it's really necessary; + +```go +// Dispatch completes the `kataras/iris/mvc#Result` interface. +// Sends a `User` as a controlled http response. +// If its ID is zero or less then it returns a 404 not found error +// else it returns its json representation, +// (just like the controller's functions do for custom types by default). +// +// Don't overdo it, the application's logic should not be here. +// It's just one more step of validation before the response, +// simple checks can be added here. +// +// It's just a showcase, +// imagine the potentials this feature gives when designing a bigger application. +// +// This is called where the return value from a controller's method functions +// is type of `User`. +// For example the `controllers/user_controller.go#GetBy`. +func (m User) Dispatch(ctx context.Context) { + if !m.IsValid() { + ctx.NotFound() + return + } + ctx.JSON(m, context.JSON{Indent: " "}) +} +``` + +However, we will use the "datamodels" as the only one models package because +User structure doesn't contain any sensitive data, clients are able to see all of its fields +and we don't need any extra functionality or validation inside it. \ No newline at end of file diff --git a/_examples/mvc/login/web/views/shared/error.html b/_examples/mvc/login/web/views/shared/error.html new file mode 100644 index 00000000..264d780a --- /dev/null +++ b/_examples/mvc/login/web/views/shared/error.html @@ -0,0 +1,15 @@ +

Error.

+

An error occurred while processing your request.

+ +

{{.Message}}

+ + \ No newline at end of file diff --git a/_examples/mvc/login/web/views/shared/layout.html b/_examples/mvc/login/web/views/shared/layout.html new file mode 100644 index 00000000..1e02677b --- /dev/null +++ b/_examples/mvc/login/web/views/shared/layout.html @@ -0,0 +1,12 @@ + + + + {{.Title}} + + + + + {{ yield }} + + + \ No newline at end of file diff --git a/_examples/mvc/login/web/views/user/login.html b/_examples/mvc/login/web/views/user/login.html new file mode 100644 index 00000000..132c739e --- /dev/null +++ b/_examples/mvc/login/web/views/user/login.html @@ -0,0 +1,11 @@ +
+
+ + + + + + + +
+
\ No newline at end of file diff --git a/_examples/mvc/login/web/views/user/me.html b/_examples/mvc/login/web/views/user/me.html new file mode 100644 index 00000000..17e2f0b3 --- /dev/null +++ b/_examples/mvc/login/web/views/user/me.html @@ -0,0 +1,3 @@ +

+ Welcome back {{.User.Firstname}}! +

\ No newline at end of file diff --git a/_examples/mvc/login/web/views/user/register.html b/_examples/mvc/login/web/views/user/register.html new file mode 100644 index 00000000..79684d99 --- /dev/null +++ b/_examples/mvc/login/web/views/user/register.html @@ -0,0 +1,14 @@ +
+
+ + + + + + + + + + +
+
\ No newline at end of file diff --git a/context/context.go b/context/context.go index cd0c1d36..47a2e05e 100644 --- a/context/context.go +++ b/context/context.go @@ -242,6 +242,44 @@ type Context interface { // // Look Handlers(), Next() and StopExecution() too. HandlerIndex(n int) (currentIndex int) + // Proceed is an alternative way to check if a particular handler + // has been executed and called the `ctx.Next` function inside it. + // This is useful only when you run a handler inside + // another handler. It justs checks for before index and the after index. + // + // A usecase example is when you want to execute a middleware + // inside controller's `BeginRequest` that calls the `ctx.Next` inside it. + // The Controller looks the whole flow (BeginRequest, method handler, EndRequest) + // as one handler, so `ctx.Next` will not be reflected to the method handler + // if called from the `BeginRequest`. + // + // Although `BeginRequest` should NOT be used to call other handlers, + // the `BeginRequest` has been introduced to be able to set + // common data to all method handlers before their execution. + // Controllers can accept middleware(s) from the `app.Controller` + // function. + // + // That said let's see an example of `ctx.Proceed`: + // + // var authMiddleware = basicauth.New(basicauth.Config{ + // Users: map[string]string{ + // "admin": "password", + // }, + // }) + // + // func (c *UsersController) BeginRequest(ctx iris.Context) { + // c.C.BeginRequest(ctx) // call the parent's base controller BeginRequest first. + // if !ctx.Proceed(authMiddleware) { + // ctx.StopExecution() + // } + // } + // This Get() will be executed in the same handler as `BeginRequest`, + // internally controller checks for `ctx.StopExecution`. + // So it will not be fired if BeginRequest called the `StopExecution`. + // func(c *UsersController) Get() []models.User { + // return c.Service.GetAll() + //} + Proceed(Handler) bool // HandlerName returns the current handler's name, helpful for debugging. HandlerName() string // Next calls all the next handler from the handlers chain, @@ -256,7 +294,8 @@ type Context interface { // Skip skips/ignores the next handler from the handlers chain, // it should be used inside a middleware. Skip() - // StopExecution if called then the following .Next calls are ignored. + // StopExecution if called then the following .Next calls are ignored, + // as a result the next handlers in the chain will not be fire. StopExecution() // IsStopped checks and returns true if the current position of the Context is 255, // means that the StopExecution() was called. @@ -353,11 +392,15 @@ type Context interface { // Look StatusCode too. GetStatusCode() int - // Redirect redirect sends a redirect response the client + // Redirect sends a redirect response to the client + // to a specific url or relative path. // accepts 2 parameters string and an optional int // first parameter is the url to redirect - // second parameter is the http status should send, default is 302 (StatusFound), - // you can set it to 301 (Permant redirect), if that's nessecery + // second parameter is the http status should send, + // default is 302 (StatusFound), + // you can set it to 301 (Permant redirect) + // or 303 (StatusSeeOther) if POST method, + // or StatusTemporaryRedirect(307) if that's nessecery. Redirect(urlToRedirect string, statusHeader ...int) // +------------------------------------------------------------+ @@ -963,6 +1006,52 @@ func (ctx *context) HandlerIndex(n int) (currentIndex int) { return n } +// Proceed is an alternative way to check if a particular handler +// has been executed and called the `ctx.Next` function inside it. +// This is useful only when you run a handler inside +// another handler. It justs checks for before index and the after index. +// +// A usecase example is when you want to execute a middleware +// inside controller's `BeginRequest` that calls the `ctx.Next` inside it. +// The Controller looks the whole flow (BeginRequest, method handler, EndRequest) +// as one handler, so `ctx.Next` will not be reflected to the method handler +// if called from the `BeginRequest`. +// +// Although `BeginRequest` should NOT be used to call other handlers, +// the `BeginRequest` has been introduced to be able to set +// common data to all method handlers before their execution. +// Controllers can accept middleware(s) from the `app.Controller` +// function. +// +// That said let's see an example of `ctx.Proceed`: +// +// var authMiddleware = basicauth.New(basicauth.Config{ +// Users: map[string]string{ +// "admin": "password", +// }, +// }) +// +// func (c *UsersController) BeginRequest(ctx iris.Context) { +// c.C.BeginRequest(ctx) // call the parent's base controller BeginRequest first. +// if !ctx.Proceed(authMiddleware) { +// ctx.StopExecution() +// } +// } +// This Get() will be executed in the same handler as `BeginRequest`, +// internally controller checks for `ctx.StopExecution`. +// So it will not be fired if BeginRequest called the `StopExecution`. +// func(c *UsersController) Get() []models.User { +// return c.Service.GetAll() +//} +func (ctx *context) Proceed(h Handler) bool { + beforeIdx := ctx.currentHandlerIndex + h(ctx) + if ctx.currentHandlerIndex > beforeIdx && !ctx.IsStopped() { + return true + } + return false +} + // HandlerName returns the current handler's name, helpful for debugging. func (ctx *context) HandlerName() string { return runtime.FuncForPC(reflect.ValueOf(ctx.handlers[ctx.currentHandlerIndex]).Pointer()).Name() @@ -1006,7 +1095,8 @@ func (ctx *context) Skip() { const stopExecutionIndex = -1 // I don't set to a max value because we want to be able to reuse the handlers even if stopped with .Skip -// StopExecution if called then the following .Next calls are ignored. +// StopExecution if called then the following .Next calls are ignored, +// as a result the next handlers in the chain will not be fire. func (ctx *context) StopExecution() { ctx.currentHandlerIndex = stopExecutionIndex } @@ -1531,14 +1621,17 @@ func (ctx *context) FormFile(key string) (multipart.File, *multipart.FileHeader, return ctx.request.FormFile(key) } -// Redirect redirect sends a redirect response the client +// Redirect sends a redirect response to the client +// to a specific url or relative path. // accepts 2 parameters string and an optional int // first parameter is the url to redirect -// second parameter is the http status should send, default is 302 (StatusFound), -// you can set it to 301 (Permant redirect), if that's nessecery +// second parameter is the http status should send, +// default is 302 (StatusFound), +// you can set it to 301 (Permant redirect) +// or 303 (StatusSeeOther) if POST method, +// or StatusTemporaryRedirect(307) if that's nessecery. func (ctx *context) Redirect(urlToRedirect string, statusHeader ...int) { ctx.StopExecution() - // get the previous status code given by the end-developer. status := ctx.GetStatusCode() if status < 300 { // the previous is not a RCF-valid redirect status. diff --git a/mvc/activator/activator.go b/mvc/activator/activator.go index 76a0dd9e..451656c9 100644 --- a/mvc/activator/activator.go +++ b/mvc/activator/activator.go @@ -57,6 +57,11 @@ var ( // which the main request `Controller` will implement automatically. // End-User doesn't need to have any knowledge of this if she/he doesn't want to implement // a new Controller type. +// Controller looks the whole flow as one handler, so `ctx.Next` +// inside `BeginRequest` is not be respected. +// Alternative way to check if a middleware was procceed succesfully +// and called its `ctx.Next` is the `ctx.Proceed(handler) bool`. +// You have to navigate to the `context/context#Proceed` function's documentation. type BaseController interface { SetName(name string) BeginRequest(ctx context.Context) diff --git a/mvc/method_result_response.go b/mvc/method_result_response.go index 4669409f..4c11cdaa 100644 --- a/mvc/method_result_response.go +++ b/mvc/method_result_response.go @@ -21,6 +21,13 @@ type Response struct { // "ContentType" if not empty. Object interface{} + // If Path is not empty then it will redirect + // the client to this Path, if Code is >= 300 and < 400 + // then it will use that Code to do the redirection, otherwise + // StatusFound(302) or StatusSeeOther(303) for post methods will be used. + // Except when err != nil. + Path string + // if not empty then fire a 400 bad request error // unless the Status is > 200, then fire that error code // with the Err.Error() string as its content. @@ -29,12 +36,31 @@ type Response struct { // if any otherwise the framework sends the default http error text based on the status. Err error Try func() int + + // if true then it skips everything else and it throws a 404 not found error. + // Can be named as Failure but NotFound is more precise name in order + // to be visible that it's different than the `Err` + // because it throws a 404 not found instead of a 400 bad request. + // NotFound bool + // let's don't add this yet, it has its dangerous of missuse. } var _ methodfunc.Result = Response{} // Dispatch writes the response result to the context's response writer. func (r Response) Dispatch(ctx context.Context) { + if r.Path != "" && r.Err == nil { + // it's not a redirect valid status + if r.Code < 300 || r.Code >= 400 { + if ctx.Method() == "POST" { + r.Code = 303 // StatusSeeOther + } + r.Code = 302 // StatusFound + } + ctx.Redirect(r.Path, r.Code) + return + } + if s := r.Text; s != "" { r.Content = []byte(s) } diff --git a/mvc/method_result_view.go b/mvc/method_result_view.go index 36a898db..167fcc6a 100644 --- a/mvc/method_result_view.go +++ b/mvc/method_result_view.go @@ -36,9 +36,14 @@ const dotB = byte('.') var DefaultViewExt = ".html" func ensureExt(s string) string { + if len(s) == 0 { + return "index.html" + } + if strings.IndexByte(s, dotB) < 1 { s += DefaultViewExt } + return s }