mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
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:
parent
b0f8329768
commit
32d14db46d
|
@ -205,12 +205,12 @@ If you're new to back-end web development read about the MVC architectural patte
|
||||||
Follow the examples below,
|
Follow the examples below,
|
||||||
|
|
||||||
- [Overview - Plus Repository and Service layers](mvc/overview) **NEW**
|
- [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)
|
- [Hello world](mvc/hello-world/main.go)
|
||||||
- [Session Controller](mvc/session-controller/main.go)
|
- [Session Controller](mvc/session-controller/main.go)
|
||||||
- [A simple but featured Controller with model and views](mvc/controller-with-model-and-view)
|
- [A simple but featured Controller with model and views](mvc/controller-with-model-and-view)
|
||||||
- [Login showcase](mvc/login/main.go)
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Subdomains
|
### Subdomains
|
||||||
|
|
43
_examples/mvc/login/_ugly/main.go
Normal file
43
_examples/mvc/login/_ugly/main.go
Normal 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"),
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
41
_examples/mvc/login/datamodels/user.go
Normal file
41
_examples/mvc/login/datamodels/user.go
Normal 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
|
||||||
|
}
|
31
_examples/mvc/login/datasource/users.go
Normal file
31
_examples/mvc/login/datasource/users.go
Normal 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
|
||||||
|
}
|
|
@ -1,43 +1,71 @@
|
||||||
|
// file: main.go
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris/_examples/mvc/login/user"
|
|
||||||
|
|
||||||
"github.com/kataras/iris"
|
"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"
|
"github.com/kataras/iris/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
// You got full debug messages, useful when using MVC and you want to make
|
// 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.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",
|
Cookie: "sessioncookiename",
|
||||||
Expires: 24 * time.Hour,
|
Expires: 24 * time.Hour,
|
||||||
})
|
})
|
||||||
users := user.NewDataSource()
|
app.Controller("/user", new(controllers.UserController), userService, sessManager)
|
||||||
|
|
||||||
app.Controller("/user", new(user.Controller), manager, users)
|
// Start the web server at localhost:8080
|
||||||
|
// http://localhost:8080/hello
|
||||||
// http://localhost:8080/user/register
|
// http://localhost:8080/hello/iris
|
||||||
// http://localhost:8080/user/login
|
// http://localhost:8080/users/1
|
||||||
// http://localhost:8080/user/me
|
app.Run(
|
||||||
// http://localhost:8080/user/logout
|
iris.Addr("localhost:8080"),
|
||||||
// http://localhost:8080/user/1
|
iris.WithoutVersionChecker,
|
||||||
app.Run(iris.Addr(":8080"), configure)
|
|
||||||
}
|
|
||||||
|
|
||||||
func configure(app *iris.Application) {
|
|
||||||
app.Configure(
|
|
||||||
iris.WithoutServerError(iris.ErrServerClosed),
|
iris.WithoutServerError(iris.ErrServerClosed),
|
||||||
iris.WithCharset("UTF-8"),
|
iris.WithOptimizations, // enables faster json serialization and more
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
173
_examples/mvc/login/repositories/user_repository.go
Normal file
173
_examples/mvc/login/repositories/user_repository.go
Normal 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)
|
||||||
|
}
|
125
_examples/mvc/login/services/user_service.go
Normal file
125
_examples/mvc/login/services/user_service.go
Normal 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)
|
||||||
|
}
|
189
_examples/mvc/login/web/controllers/user_controller.go
Normal file
189
_examples/mvc/login/web/controllers/user_controller.go
Normal 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")
|
||||||
|
}
|
101
_examples/mvc/login/web/controllers/users_controller.go
Normal file
101
_examples/mvc/login/web/controllers/users_controller.go
Normal 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`.
|
||||||
|
}
|
12
_examples/mvc/login/web/middleware/basicauth.go
Normal file
12
_examples/mvc/login/web/middleware/basicauth.go
Normal 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",
|
||||||
|
},
|
||||||
|
})
|
61
_examples/mvc/login/web/public/css/site.css
Normal file
61
_examples/mvc/login/web/public/css/site.css
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
55
_examples/mvc/login/web/viewmodels/README.md
Normal file
55
_examples/mvc/login/web/viewmodels/README.md
Normal 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.
|
15
_examples/mvc/login/web/views/shared/error.html
Normal file
15
_examples/mvc/login/web/views/shared/error.html
Normal 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>
|
12
_examples/mvc/login/web/views/shared/layout.html
Normal file
12
_examples/mvc/login/web/views/shared/layout.html
Normal 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>
|
11
_examples/mvc/login/web/views/user/login.html
Normal file
11
_examples/mvc/login/web/views/user/login.html
Normal 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>
|
3
_examples/mvc/login/web/views/user/me.html
Normal file
3
_examples/mvc/login/web/views/user/me.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<p>
|
||||||
|
Welcome back <strong>{{.User.Firstname}}</strong>!
|
||||||
|
</p>
|
14
_examples/mvc/login/web/views/user/register.html
Normal file
14
_examples/mvc/login/web/views/user/register.html
Normal 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>
|
|
@ -242,6 +242,44 @@ type Context interface {
|
||||||
//
|
//
|
||||||
// Look Handlers(), Next() and StopExecution() too.
|
// Look Handlers(), Next() and StopExecution() too.
|
||||||
HandlerIndex(n int) (currentIndex int)
|
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 returns the current handler's name, helpful for debugging.
|
||||||
HandlerName() string
|
HandlerName() string
|
||||||
// Next calls all the next handler from the handlers chain,
|
// 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,
|
// Skip skips/ignores the next handler from the handlers chain,
|
||||||
// it should be used inside a middleware.
|
// it should be used inside a middleware.
|
||||||
Skip()
|
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()
|
StopExecution()
|
||||||
// IsStopped checks and returns true if the current position of the Context is 255,
|
// IsStopped checks and returns true if the current position of the Context is 255,
|
||||||
// means that the StopExecution() was called.
|
// means that the StopExecution() was called.
|
||||||
|
@ -353,11 +392,15 @@ type Context interface {
|
||||||
// Look StatusCode too.
|
// Look StatusCode too.
|
||||||
GetStatusCode() int
|
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
|
// accepts 2 parameters string and an optional int
|
||||||
// first parameter is the url to redirect
|
// first parameter is the url to redirect
|
||||||
// second parameter is the http status should send, default is 302 (StatusFound),
|
// second parameter is the http status should send,
|
||||||
// you can set it to 301 (Permant redirect), if that's nessecery
|
// 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)
|
Redirect(urlToRedirect string, statusHeader ...int)
|
||||||
|
|
||||||
// +------------------------------------------------------------+
|
// +------------------------------------------------------------+
|
||||||
|
@ -963,6 +1006,52 @@ func (ctx *context) HandlerIndex(n int) (currentIndex int) {
|
||||||
return n
|
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.
|
// HandlerName returns the current handler's name, helpful for debugging.
|
||||||
func (ctx *context) HandlerName() string {
|
func (ctx *context) HandlerName() string {
|
||||||
return runtime.FuncForPC(reflect.ValueOf(ctx.handlers[ctx.currentHandlerIndex]).Pointer()).Name()
|
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
|
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() {
|
func (ctx *context) StopExecution() {
|
||||||
ctx.currentHandlerIndex = stopExecutionIndex
|
ctx.currentHandlerIndex = stopExecutionIndex
|
||||||
}
|
}
|
||||||
|
@ -1531,14 +1621,17 @@ func (ctx *context) FormFile(key string) (multipart.File, *multipart.FileHeader,
|
||||||
return ctx.request.FormFile(key)
|
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
|
// accepts 2 parameters string and an optional int
|
||||||
// first parameter is the url to redirect
|
// first parameter is the url to redirect
|
||||||
// second parameter is the http status should send, default is 302 (StatusFound),
|
// second parameter is the http status should send,
|
||||||
// you can set it to 301 (Permant redirect), if that's nessecery
|
// 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) {
|
func (ctx *context) Redirect(urlToRedirect string, statusHeader ...int) {
|
||||||
ctx.StopExecution()
|
ctx.StopExecution()
|
||||||
|
|
||||||
// get the previous status code given by the end-developer.
|
// get the previous status code given by the end-developer.
|
||||||
status := ctx.GetStatusCode()
|
status := ctx.GetStatusCode()
|
||||||
if status < 300 { // the previous is not a RCF-valid redirect status.
|
if status < 300 { // the previous is not a RCF-valid redirect status.
|
||||||
|
|
|
@ -57,6 +57,11 @@ var (
|
||||||
// which the main request `Controller` will implement automatically.
|
// 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
|
// End-User doesn't need to have any knowledge of this if she/he doesn't want to implement
|
||||||
// a new Controller type.
|
// 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 {
|
type BaseController interface {
|
||||||
SetName(name string)
|
SetName(name string)
|
||||||
BeginRequest(ctx context.Context)
|
BeginRequest(ctx context.Context)
|
||||||
|
|
|
@ -21,6 +21,13 @@ type Response struct {
|
||||||
// "ContentType" if not empty.
|
// "ContentType" if not empty.
|
||||||
Object interface{}
|
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
|
// if not empty then fire a 400 bad request error
|
||||||
// unless the Status is > 200, then fire that error code
|
// unless the Status is > 200, then fire that error code
|
||||||
// with the Err.Error() string as its content.
|
// 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.
|
// if any otherwise the framework sends the default http error text based on the status.
|
||||||
Err error
|
Err error
|
||||||
Try func() int
|
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{}
|
var _ methodfunc.Result = Response{}
|
||||||
|
|
||||||
// Dispatch writes the response result to the context's response writer.
|
// Dispatch writes the response result to the context's response writer.
|
||||||
func (r Response) Dispatch(ctx context.Context) {
|
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 != "" {
|
if s := r.Text; s != "" {
|
||||||
r.Content = []byte(s)
|
r.Content = []byte(s)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,9 +36,14 @@ const dotB = byte('.')
|
||||||
var DefaultViewExt = ".html"
|
var DefaultViewExt = ".html"
|
||||||
|
|
||||||
func ensureExt(s string) string {
|
func ensureExt(s string) string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return "index.html"
|
||||||
|
}
|
||||||
|
|
||||||
if strings.IndexByte(s, dotB) < 1 {
|
if strings.IndexByte(s, dotB) < 1 {
|
||||||
s += DefaultViewExt
|
s += DefaultViewExt
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user