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:
@@ -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
|
||||
|
||||
20
_examples/mvc/login/database/database.go
Normal file
20
_examples/mvc/login/database/database.go
Normal 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)
|
||||
}
|
||||
41
_examples/mvc/login/main.go
Normal file
41
_examples/mvc/login/main.go
Normal 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"),
|
||||
)
|
||||
}
|
||||
61
_examples/mvc/login/public/css/site.css
Normal file
61
_examples/mvc/login/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%;
|
||||
}
|
||||
}
|
||||
125
_examples/mvc/login/user/auth.go
Normal file
125
_examples/mvc/login/user/auth.go
Normal 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)
|
||||
}
|
||||
125
_examples/mvc/login/user/controller.go
Normal file
125
_examples/mvc/login/user/controller.go
Normal 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)
|
||||
}
|
||||
}
|
||||
114
_examples/mvc/login/user/datasource.go
Normal file
114
_examples/mvc/login/user/datasource.go
Normal 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
|
||||
}
|
||||
36
_examples/mvc/login/user/model.go
Normal file
36
_examples/mvc/login/user/model.go
Normal 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
|
||||
}
|
||||
4
_examples/mvc/login/views/shared/error.html
Normal file
4
_examples/mvc/login/views/shared/error.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<h1>Error.</h1>
|
||||
<h2>An error occurred while processing your request.</h2>
|
||||
|
||||
<h3>{{.Message}}</h3>
|
||||
12
_examples/mvc/login/views/shared/layout.html
Normal file
12
_examples/mvc/login/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/views/user/login.html
Normal file
11
_examples/mvc/login/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/views/user/me.html
Normal file
3
_examples/mvc/login/views/user/me.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<p>
|
||||
Welcome back <strong>{{.User.Firstname}}</strong>!
|
||||
</p>
|
||||
3
_examples/mvc/login/views/user/notfound.html
Normal file
3
_examples/mvc/login/views/user/notfound.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<p>
|
||||
User with ID <strong>{{.ID}}</strong> does not exist.
|
||||
</p>
|
||||
14
_examples/mvc/login/views/user/register.html
Normal file
14
_examples/mvc/login/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>
|
||||
Reference in New Issue
Block a user