Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make user invitation function #4

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions css/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ body {
height: 100%;
}

div.login p {
margin: 15px auto;
}
div.login a.login.btn {
margin: 0;
}

.form-inline input[type="url"] {
width: 480px;
max-width: 100%;
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/web/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func (web *Web) authenticate(h http.HandlerFunc) http.HandlerFunc {
userID, ok := session.Values["user_id"].(uint)
if ok {
user, err := web.engine.FindUser(userID)
if err != nil || user.GoogleToken == "" {
if err != nil || user.Status != domain.UserStatusValid {
log.Println("Failed to find user: " + err.Error())
ok = false
} else {
Expand Down
10 changes: 5 additions & 5 deletions src/adapters/web/page_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ func (web *Web) pagesHandler(w http.ResponseWriter, r *http.Request) {
if bytes != nil {
json.Unmarshal(bytes, &resp)
}
user, ok := context.Get(r, "user").(*domain.User)
if ok {
resp["User"] = user
}
var req engine.FindPagesRequest
err := form.NewDecoder().Decode(&req, r.URL.Query())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
req.UserID = user.ID
user, ok := context.Get(r, "user").(*domain.User)
if ok {
resp["User"] = user
req.UserID = user.ID
}
pages, count, err := web.engine.FindPages(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Expand Down
8 changes: 4 additions & 4 deletions src/adapters/web/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,21 @@ func renderTemplate(w http.ResponseWriter, r *http.Request, path string, data ma
data["MainHost"] = scheme + os.Getenv("MAIN_DOMAIN")
data["FileHost"] = scheme + os.Getenv("FILE_DOMAIN")
data["ImageHost"] = scheme + os.Getenv("IMAGE_DOMAIN")
tpl := findTemplate("/layouts.tpl", path)
tpl := findTemplate("/layouts.tpl", "/login.tpl", path)
tpl.ExecuteTemplate(w, "base", data)
}

// cache templates so that it doesn't parse files every time in production
func findTemplate(basePath, path string) (tpl *template.Template) {
func findTemplate(basePath, loginPath, path string) (tpl *template.Template) {
if Digest == "" {
tpl = template.Must(template.ParseFiles(baseTplPath+basePath, baseTplPath+path))
tpl = template.Must(template.ParseFiles(baseTplPath+basePath, baseTplPath+loginPath, baseTplPath+path))
return
}
tpl, ok := templates[path]
if ok {
return
}
tpl = template.Must(template.ParseFiles(baseTplPath+basePath, baseTplPath+path))
tpl = template.Must(template.ParseFiles(baseTplPath+basePath, baseTplPath+loginPath, baseTplPath+path))
templates[path] = tpl
return
}
Expand Down
54 changes: 54 additions & 0 deletions src/adapters/web/user_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package web

import (
"net/http"

"github.com/gorilla/mux"
"github.com/scoville/scvl/src/engine"
)

func (web *Web) userRegistrationHandler(w http.ResponseWriter, r *http.Request) {
user, err := web.engine.UserRegister(&engine.RegistrationRequest{
Hash: r.FormValue("hash"),
Password: r.FormValue("password"),
})
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
session, _ := web.store.Get(r, "scvl")
session.Values["user_id"] = user.ID
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusSeeOther)
}

func (web *Web) userRegistrationPageHandler(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["hash"]
invitation, err := web.engine.FindInvitation(&engine.FindInvitationRequest{
Hash: hash,
})
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
resp := map[string]interface{}{
"Email": invitation.ToUser.Email,
"Hash": hash,
}
renderTemplate(w, r, "/register.tpl", resp)
}

func (web *Web) loginHandler(w http.ResponseWriter, r *http.Request) {
session, _ := web.store.Get(r, "scvl")
user, err := web.engine.LoginUser(&engine.LoginUserRequest{
Email: r.FormValue("email"),
Password: r.FormValue("password"),
})
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
session.Values["user_id"] = user.ID
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
40 changes: 40 additions & 0 deletions src/adapters/web/user_invitation_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package web

import (
"net/http"

"github.com/gorilla/context"
"github.com/scoville/scvl/src/domain"
"github.com/scoville/scvl/src/engine"
)

func (web *Web) invitationCreateHandler(w http.ResponseWriter, r *http.Request) {
user, ok := context.Get(r, "user").(*domain.User)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
invitation, err := web.engine.InviteUser(&engine.InviteRequest{
FromUserID: uint(user.ID),
Email: r.FormValue("email"),
})
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
registerPath := "/register/" + invitation.Hash
resp := map[string]interface{}{
"RegisterPath": registerPath,
}
renderTemplate(w, r, "/invitation.tpl", resp)
}

func (web *Web) invitationsHandler(w http.ResponseWriter, r *http.Request) {
_, ok := context.Get(r, "user").(*domain.User)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
resp := map[string]interface{}{}
renderTemplate(w, r, "/invitations.tpl", resp)
}
5 changes: 5 additions & 0 deletions src/adapters/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ func (web *Web) Start(port string) error {
r.HandleFunc("/files/{slug}", web.authenticate(web.updateFileHandler)).Methods(http.MethodPost, http.MethodPut, http.MethodPatch)
r.Handle("/images", web.authenticate(web.imagesHandler)).Methods(http.MethodGet)
r.Handle("/images", web.authenticate(web.imageUploadHandler)).Methods(http.MethodPost)
r.Handle("/invites", web.authenticate(web.invitationsHandler)).Methods(http.MethodGet)
r.Handle("/invites", web.authenticate(web.invitationCreateHandler)).Methods(http.MethodPost)
r.HandleFunc("/register/{hash}", web.userRegistrationPageHandler).Methods(http.MethodGet)
r.HandleFunc("/register", web.userRegistrationHandler).Methods(http.MethodPost)
r.HandleFunc("/login", web.loginHandler).Methods(http.MethodPost)

r.HandleFunc("/{slug}/qr.png", web.qrHandler).Methods(http.MethodGet)
r.Handle("/{slug}/edit", web.authenticate(web.editHandler)).Methods(http.MethodGet)
Expand Down
39 changes: 34 additions & 5 deletions src/domain/user.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package domain

import "time"
import (
"fmt"
"time"
)

// User is a user
type User struct {
Expand All @@ -9,9 +12,35 @@ type User struct {
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
Name string `json:"name"`
Email string `json:"email" gorm:"type:varchar(100);unique_index"`
Email string `json:"email" gorm:"type:varchar(100);unique_index; not null"`

Files []*File `json:"files"`
Images []*Image `json:"images"`
GoogleToken string `json:"google_token"`
Files []*File `json:"files"`
Images []*Image `json:"images"`
GoogleToken string `json:"google_token"`
EncryptedPassword string `json:"-"`
Status string `json:"status"`
}

// Status for User
const (
UserStatusTemp = "temp"
UserStatusValid = "valid"
UserStatusDeleted = "deleted"
)

// SetPassword sets the password
func (w *User) SetPassword(pass string) error {
if len(pass) < 6 {
return fmt.Errorf("password should be greater or equal than 6 characters")
}
w.EncryptedPassword = Encrypt(pass)
return nil
}

// BeforeSave is called before it is saved to the database
func (w *User) BeforeSave() error {
if w.GoogleToken == "" && w.EncryptedPassword == "" {
return fmt.Errorf("password is required")
}
return nil
}
46 changes: 46 additions & 0 deletions src/domain/user_invitation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package domain

import (
"fmt"
"time"
)

// UserInvitation is the struct for user_invitation.
type UserInvitation struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
Hash string `json:"hash" gorm:"unique_index; not null"`
Status string `json:"-" gorm:"not null" valid:"required,in(sent|used|deleted)"`
FromUserID uint `json:"-" gorm:"type:integer REFERENCES users(id) ON DELETE CASCADE; not null" valid:"required"`
ToUserID uint `json:"to_user_id" gorm:"type:integer REFERENCES users(id) ON DELETE CASCADE"`

ToUser *User `json:"to_user,omitempty" gorm:"association_autupdate:false;association_autcreate:false"`
}

// Invitation statuses
const (
InvitationStatusSent = "sent"
InvitationStatusUsed = "used"
InvitationStatusDeleted = "deleted"
)

// BeforeCreate generates a unique hash for the invitation.
func (i *UserInvitation) BeforeCreate() error {
i.Hash = GenerateSlug(64)
i.Status = InvitationStatusSent
i.ToUser.SetPassword(GenerateSlug(12))
return nil
}

// Valid returns Error if the invitation is not valid
func (i *UserInvitation) Valid() error {
if i.Status == InvitationStatusUsed {
return fmt.Errorf("the invitation is already used")
}
if time.Now().Sub(i.CreatedAt) > time.Hour*24 {
return fmt.Errorf("the invitation is expired")
}
return nil
}
2 changes: 1 addition & 1 deletion src/engine/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func (e *Engine) UpdatePage(req *UpdatePageRequest) (page *domain.Page, err erro
}

func (e *Engine) fetchTitle(userID int, url string) (title string, err error) {
user, err := e.sqlClient.FindUser(uint(userID))
user, err := e.sqlClient.FindUser(domain.User{ID: uint(userID)})
if err != nil {
return
}
Expand Down
6 changes: 5 additions & 1 deletion src/engine/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import "github.com/scoville/scvl/src/domain"
type SQLClient interface {
Close() error

FindUser(uint) (*domain.User, error)
FindUser(domain.User) (*domain.User, error)
FindOrCreateUser(domain.User) (*domain.User, error)
CreateInvitation(*domain.UserInvitation) (*domain.UserInvitation, error)
FindInvitation(string) (*domain.UserInvitation, error)
UpdateInvitation(*domain.UserInvitation, *domain.UserInvitation) (*domain.UserInvitation, error)
UserRegister(*domain.User, *domain.User) (*domain.User, error)

FindPages(params *FindPagesRequest) (pages []*domain.Page, count int, err error)
FindPageBySlug(string) (*domain.Page, error)
Expand Down
56 changes: 55 additions & 1 deletion src/engine/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import (
"strings"

"github.com/scoville/scvl/src/domain"
"golang.org/x/crypto/bcrypt"
)

// FindUser finds and returns the user
func (e *Engine) FindUser(userID uint) (*domain.User, error) {
return e.sqlClient.FindUser(userID)
return e.sqlClient.FindUser(domain.User{ID: userID})
}

// FindOrCreateUserByGoogleCode finds or creates the user
Expand All @@ -23,3 +24,56 @@ func (e *Engine) FindOrCreateUserByGoogleCode(code string) (*domain.User, error)
}
return e.sqlClient.FindOrCreateUser(u)
}

// RegistrationRequest is the request
type RegistrationRequest struct {
Hash string
Password string
}

// UserRegister creates the user who is invited to the system.
func (e *Engine) UserRegister(req *RegistrationRequest) (*domain.User, error) {
invitation, err := e.sqlClient.FindInvitation(req.Hash)
if err != nil {
return nil, err
}
if err := invitation.Valid(); err != nil {
return nil, err
}
user, err := e.sqlClient.FindUser(domain.User{
Email: invitation.ToUser.Email,
Status: domain.UserStatusTemp,
})
if err != nil {
return nil, err
}
if err := user.SetPassword(req.Password); err != nil {
return nil, err
}
user.Status = domain.UserStatusValid
usedInvitation, err := e.sqlClient.UpdateInvitation(invitation, &domain.UserInvitation{
Status: domain.InvitationStatusUsed,
ToUser: user,
})
return usedInvitation.ToUser, err
}

// LoginUserRequest is the Reqeust
type LoginUserRequest struct {
Email string
Password string
}

// LoginUser is login request
func (e *Engine) LoginUser(req *LoginUserRequest) (*domain.User, error) {
user, err := e.sqlClient.FindUser(domain.User{
Email: req.Email,
})
if err != nil {
return nil, err
}
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(req.Password)); err != nil {
return nil, err
}
return user, nil
}
Loading