1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 18:37:05 +00:00

Structuring examples - Pushed to iris-contrib/examples as well.

Former-commit-id: 24ee6ce233d83f0b394afc6c69b5a88243406045
This commit is contained in:
Gerasimos (Makis) Maropoulos
2017-10-22 16:04:11 +03:00
parent f95986d0c0
commit 11277f12a0
42 changed files with 32 additions and 270 deletions

View File

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

View File

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

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