1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-08 20:41:57 +00:00

Update to 8.4.0 | New macro type, new high-optimized MVC features. Read HISTORY.md

Former-commit-id: b72a23ba063be60a9750c8b1b0df024b0c8ed549
This commit is contained in:
kataras
2017-08-27 18:46:04 +03:00
parent 8602517371
commit 591806795e
37 changed files with 1242 additions and 453 deletions

View File

@@ -136,6 +136,27 @@ Optional `EndRequest(ctx)` function to perform any finalization after any method
Inheritance, see for example our `mvc.SessionController`, it has the `mvc.Controller` as an embedded field
and it adds its logic to its `BeginRequest`, [here](https://github.com/kataras/iris/blob/master/mvc/session_controller.go).
Register one or more relative paths and able to get path parameters, i.e
If `app.Controller("/user", new(user.Controller))`
- `func(*Controller) Get()` - `GET:/user` , as usual.
- `func(*Controller) Post()` - `POST:/user`, as usual.
- `func(*Controller) GetLogin()` - `GET:/user/login`
- `func(*Controller) PostLogin()` - `POST:/user/login`
- `func(*Controller) GetProfileFollowers()` - `GET:/user/profile/followers`
- `func(*Controller) PostProfileFollowers()` - `POST:/user/profile/followers`
- `func(*Controller) GetBy(id int64)` - `GET:/user/{param:long}`
- `func(*Controller) PostBy(id int64)` - `POST:/user/{param:long}`
If `app.Controller("/profile", new(profile.Controller))`
- `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}`
If `app.Controller("/assets", new(file.Controller))`
- `func(*Controller) GetByWildard(path string)` - `GET:/assets/{param:path}`
**Using Iris MVC for code reuse**
By creating components that are independent of one another, developers are able to reuse components quickly and easily in other applications. The same (or similar) view for one application can be refactored for another application with different data because the view is simply handling how the data is being displayed to the user.
@@ -148,7 +169,7 @@ Follow the examples below,
- [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) **NEW**
### Subdomains

View File

@@ -0,0 +1,20 @@
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 main
import (
"time"
"github.com/kataras/iris/_examples/mvc/login-example/user"
"github.com/kataras/iris"
"github.com/kataras/iris/sessions"
)
func main() {
app := iris.New()
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

@@ -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,125 @@
package user
import (
"errors"
"strconv"
"strings"
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc"
)
// paths
const (
PathLogin = "/user/login"
PathLogout = "/user/logout"
)
// the session key for the user id comes from the Session.
const (
sessionIDKey = "UserID"
)
// AuthController is the user authentication controller, a custom shared controller.
type AuthController struct {
mvc.SessionController
Source *DataSource
User Model `iris:"model"`
}
// BeginRequest saves login state to the context, the user id.
func (c *AuthController) BeginRequest(ctx context.Context) {
c.SessionController.BeginRequest(ctx)
if userID := c.Session.Get(sessionIDKey); userID != nil {
ctx.Values().Set(sessionIDKey, userID)
}
}
func (c *AuthController) fireError(err error) {
if err != nil {
c.Ctx.Application().Logger().Debug(err.Error())
c.Status = 400
c.Data["Title"] = "User Error"
c.Data["Message"] = strings.ToUpper(err.Error())
c.Tmpl = "shared/error.html"
}
}
func (c *AuthController) redirectTo(id int64) {
if id > 0 {
c.Path = "/user/" + strconv.Itoa(int(id))
}
}
func (c *AuthController) createOrUpdate(firstname, username, password string) (user Model, err error) {
username = strings.Trim(username, " ")
if username == "" || password == "" || firstname == "" {
return user, errors.New("empty firstname, username or/and password")
}
userToInsert := Model{
Firstname: firstname,
Username: username,
password: password,
} // password is hashed by the Source.
newUser, err := c.Source.InsertOrUpdate(userToInsert)
if err != nil {
return user, err
}
return newUser, nil
}
func (c *AuthController) isLoggedIn() bool {
// we don't search by session, we have the user id
// already by the `SaveState` middleware.
return c.Values.Get(sessionIDKey) != nil
}
func (c *AuthController) verify(username, password string) (user Model, err error) {
if username == "" || password == "" {
return user, errors.New("please fill both username and password fields")
}
u, found := c.Source.GetByUsername(username)
if !found {
// if user found with that username not found at all.
return user, errors.New("user with that username does not exist")
}
if ok, err := ValidatePassword(password, u.HashedPassword); err != nil || !ok {
// if user found but an error occurred or the password is not valid.
return user, errors.New("please try to login with valid credentials")
}
return u, nil
}
// if logged in then destroy the session
// and redirect to the login page
// otherwise redirect to the registration page.
func (c *AuthController) logout() {
if c.isLoggedIn() {
// c.Manager is the Sessions manager created
// by the embedded SessionController, automatically.
c.Manager.DestroyByID(c.Session.ID())
return
}
c.Path = PathLogin
}
// AllowUser will check if this client is a logged user,
// if not then it will redirect that guest to the login page
// otherwise it will allow the execution of the next handler.
func AllowUser(ctx context.Context) {
if ctx.Values().Get(sessionIDKey) != nil {
ctx.Next()
return
}
ctx.Redirect(PathLogin)
}

View File

@@ -0,0 +1,125 @@
package user
const (
pathMyProfile = "/user/me"
pathRegister = "/user/register"
)
// Controller is responsible to handle the following requests:
// GET /user/register
// POST /user/register
// GET /user/login
// POST /user/login
// GET /user/me
// GET /user/{id:long} | long is a new param type, it's the int64.
// All HTTP Methods /user/logout
type Controller struct {
AuthController
}
// GetRegister handles GET:/user/register.
func (c *Controller) GetRegister() {
if c.isLoggedIn() {
c.logout()
return
}
c.Data["Title"] = "User Registration"
c.Tmpl = pathRegister + ".html"
}
// PostRegister handles POST:/user/register.
func (c *Controller) PostRegister() {
// we can either use the `c.Ctx.ReadForm` or read values one by one.
var (
firstname = c.Ctx.FormValue("firstname")
username = c.Ctx.FormValue("username")
password = c.Ctx.FormValue("password")
)
user, err := c.createOrUpdate(firstname, username, password)
if err != nil {
c.fireError(err)
return
}
// setting a session value was never easier.
c.Session.Set(sessionIDKey, user.ID)
// succeed, nothing more to do here, just redirect to the /user/me.
c.Path = pathMyProfile
}
// GetLogin handles GET:/user/login.
func (c *Controller) GetLogin() {
if c.isLoggedIn() {
c.logout()
return
}
c.Data["Title"] = "User Login"
c.Tmpl = PathLogin + ".html"
}
// PostLogin handles POST:/user/login.
func (c *Controller) PostLogin() {
var (
username = c.Ctx.FormValue("username")
password = c.Ctx.FormValue("password")
)
user, err := c.verify(username, password)
if err != nil {
c.fireError(err)
return
}
c.Session.Set(sessionIDKey, user.ID)
c.Path = pathMyProfile
}
// AnyLogout handles any method on path /user/logout.
func (c *Controller) AnyLogout() {
c.logout()
}
// GetMe handles GET:/user/me.
func (c *Controller) GetMe() {
id, err := c.Session.GetInt64(sessionIDKey)
if err != nil || id <= 0 {
// when not already logged in.
c.Path = PathLogin
return
}
u, found := c.Source.GetByID(id)
if !found {
// if the session exists but for some reason the user doesn't exist in the "database"
// then logout him and redirect to the register page.
c.logout()
return
}
// set the model and render the view template.
c.User = u
c.Data["Title"] = "Profile of " + u.Username
c.Tmpl = pathMyProfile + ".html"
}
func (c *Controller) renderNotFound(id int64) {
c.Status = 404
c.Data["Title"] = "User Not Found"
c.Data["ID"] = id
c.Tmpl = "user/notfound.html"
}
// GetBy handles GET:/user/{id:long},
// i.e http://localhost:8080/user/1
func (c *Controller) GetBy(userID int64) {
// we have /user/{id}
// fetch and render user json.
if user, found := c.Source.GetByID(userID); !found {
// not user found with that ID.
c.renderNotFound(userID)
} else {
c.Ctx.JSON(user)
}
}

View File

@@ -0,0 +1,114 @@
package user
import (
"errors"
"sync"
"time"
)
// IDGenerator would be our user ID generator
// but here we keep the order of users by their IDs
// so we will use numbers that can be easly written
// to the browser to get results back from the REST API.
// var IDGenerator = func() string {
// return uuid.NewV4().String()
// }
// DataSource is our data store example.
type DataSource struct {
Users map[int64]Model
mu sync.RWMutex
}
// NewDataSource returns a new user data source.
func NewDataSource() *DataSource {
return &DataSource{
Users: make(map[int64]Model),
}
}
// GetBy returns receives a query function
// which is fired for every single user model inside
// our imaginary database.
// 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.
//
// But be carefully, the caller should always check for the "found"
// because it may be false but the user model has actually real data inside it.
//
// It's actually a simple but very clever prototype function
// I'm think of and using everywhere since then,
// hope you find it very useful too.
func (d *DataSource) GetBy(query func(Model) bool) (user Model, found bool) {
d.mu.RLock()
for _, user = range d.Users {
found = query(user)
if found {
break
}
}
d.mu.RUnlock()
return
}
// GetByID returns a user model based on its ID.
func (d *DataSource) GetByID(id int64) (Model, bool) {
return d.GetBy(func(u Model) bool {
return u.ID == id
})
}
// GetByUsername returns a user model based on the Username.
func (d *DataSource) GetByUsername(username string) (Model, bool) {
return d.GetBy(func(u Model) bool {
return u.Username == username
})
}
func (d *DataSource) getLastID() (lastID int64) {
d.mu.RLock()
for id := range d.Users {
if id > lastID {
lastID = id
}
}
d.mu.RUnlock()
return lastID
}
// InsertOrUpdate adds or updates a user to the (memory) storage.
func (d *DataSource) InsertOrUpdate(user Model) (Model, error) {
// no matter what we will update the password hash
// for both update and insert actions.
hashedPassword, err := GeneratePassword(user.password)
if err != nil {
return user, err
}
user.HashedPassword = hashedPassword
// update
if id := user.ID; id > 0 {
_, found := d.GetByID(id)
if !found {
return user, errors.New("ID should be zero or a valid one that maps to an existing User")
}
d.mu.Lock()
d.Users[id] = user
d.mu.Unlock()
return user, nil
}
// insert
id := d.getLastID() + 1
user.ID = id
d.mu.Lock()
user.CreatedAt = time.Now()
d.Users[id] = user
d.mu.Unlock()
return user, nil
}

View File

@@ -0,0 +1,36 @@
package user
import (
"time"
"golang.org/x/crypto/bcrypt"
)
// Model is our User example model.
type Model struct {
ID int64 `json:"id"`
Firstname string `json:"firstname"`
Username string `json:"username"`
// password is the client-given password
// which will not be stored anywhere in the server.
// It's here only for actions like registration and update password,
// because we caccept a Model instance
// inside the `DataSource#InsertOrUpdate` function.
password string
HashedPassword []byte `json:"-"`
CreatedAt time.Time `json:"created_at"`
}
// 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,4 @@
<h1>Error.</h1>
<h2>An error occurred while processing your request.</h2>
<h3>{{.Message}}</h3>

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,3 @@
<p>
User with ID <strong>{{.ID}}</strong> does not exist.
</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>