Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions css/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ body {
height: 100%;
}

div.register {
flex-direction: column;
align-items: inherit;
}
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
55 changes: 55 additions & 0 deletions src/adapters/web/user_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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"),
Name: r.FormValue("name"),
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
7 changes: 6 additions & 1 deletion src/engine/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ 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) error
FindInvitation(domain.UserInvitation) (*domain.UserInvitation, error)
UpdateInvitation(*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)
CreatePage(*domain.Page) error
Expand Down
59 changes: 58 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 @@ -21,5 +22,61 @@ func (e *Engine) FindOrCreateUserByGoogleCode(code string) (*domain.User, error)
if e.allowedDomain != "" && !strings.HasSuffix(u.Email, "@"+e.allowedDomain) {
return nil, fmt.Errorf("only %s can allowed to use this service", e.allowedDomain)
}
u.Status = domain.UserStatusValid
return e.sqlClient.FindOrCreateUser(u)
}

// RegistrationRequest is the request
type RegistrationRequest struct {
Name string
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(domain.UserInvitation{Hash: 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.Name = req.Name
user.Status = domain.UserStatusValid
err = e.sqlClient.UpdateInvitation(invitation, &domain.UserInvitation{
Status: domain.InvitationStatusUsed,
ToUser: user,
})
return invitation.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