Update the _examples/mvc/login example - add a Path property at mvc.Response and add a context.Proceed helper function

Former-commit-id: b898901fe4a324e888a6e09c93530cf7a551cf2a
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-10-12 16:28:41 +03:00
parent b0f8329768
commit 32d14db46d
33 changed files with 1073 additions and 50 deletions

View File

@ -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**
<!--
- [Hello world](mvc/hello-world/main.go)
- [Session Controller](mvc/session-controller/main.go)
- [A simple but featured Controller with model and views](mvc/controller-with-model-and-view)
- [Login showcase](mvc/login/main.go)
-->
### Subdomains

View File

@ -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"),
)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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`.
}

View File

@ -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",
},
})

View File

@ -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%;
}
}

View File

@ -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.

View File

@ -0,0 +1,15 @@
<h1>Error.</h1>
<h2>An error occurred while processing your request.</h2>
<h3>{{.Message}}</h3>
<footer>
<h2>Sitemap</h2>
<a href="http://localhost:8080/user/register">/user/register</a><br/>
<a href="http://localhost:8080/user/login">/user/login</a><br/>
<a href="http://localhost:8080/user/logout">/user/logout</a><br/>
<a href="http://localhost:8080/user/me">/user/me</a><br/>
<h3>requires authentication</h3><br/>
<a href="http://localhost:8080/users">/users</a><br/>
<a href="http://localhost:8080/users/1">/users/{id}</a><br/>
</footer>

View File

@ -0,0 +1,12 @@
<html>
<head>
<title>{{.Title}}</title>
<link rel="stylesheet" type="text/css" href="/public/css/site.css" />
</head>
<body>
{{ yield }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<form action="/user/login" method="POST">
<div class="container">
<label><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="username" required>
<label><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Login</button>
</div>
</form>

View File

@ -0,0 +1,3 @@
<p>
Welcome back <strong>{{.User.Firstname}}</strong>!
</p>

View File

@ -0,0 +1,14 @@
<form action="/user/register" method="POST">
<div class="container">
<label><b>Firstname</b></label>
<input type="text" placeholder="Enter Firstname" name="firstname" required>
<label><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="username" required>
<label><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Register</button>
</div>
</form>

View File

@ -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.

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}