mirror of
https://github.com/kataras/iris.git
synced 2025-12-17 18:07:01 +00:00
Structuring examples - Pushed to iris-contrib/examples as well.
Former-commit-id: 24ee6ce233d83f0b394afc6c69b5a88243406045
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/_examples/structuring/login-single-responsibility-package/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"),
|
||||
)
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
iris.SessionController
|
||||
|
||||
Source *DataSource
|
||||
User Model `iris:"model"`
|
||||
}
|
||||
|
||||
// BeginRequest saves login state to the context, the user id.
|
||||
func (c *AuthController) BeginRequest(ctx iris.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 iris.Context) {
|
||||
if ctx.Values().Get(sessionIDKey) != nil {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
ctx.Redirect(PathLogin)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
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.
|
||||
|
||||
// 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.
|
||||
c.Status = 303 // "See Other" RFC 7231
|
||||
|
||||
// Redirect to GET: /user/me
|
||||
// by changing the Path (and the status code because we're in POST request at this case).
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<h1>Error.</h1>
|
||||
<h2>An error occurred while processing your request.</h2>
|
||||
|
||||
<h3>{{.Message}}</h3>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>
|
||||
Welcome back <strong>{{.User.Firstname}}</strong>!
|
||||
</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>
|
||||
User with ID <strong>{{.ID}}</strong> does not exist.
|
||||
</p>
|
||||
@@ -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