Skip to content

Commit

Permalink
[cw,am|#14] implemented user registration, login/auth, introduced
Browse files Browse the repository at this point in the history
in-memory session token cache, modified user table to include hashed
password and salt, implement session methods for correlating userIds
to session tokens and vice versa

❀‿❀ -- yay!!

(づ。◕‿‿◕。)づ -- wheeeeee

╰(◡‿◡✿╰) -- whoooooo

TODOS:
* use CookieJar instead of Cookie?
* standardize form vs. data for requests
* implement session token expiration policy goroutine
* actually send responses to clients (send session token cookie as
  well)
* write unit test for all of this ;___;
  • Loading branch information
connorwalsh committed Feb 22, 2018
1 parent 02e00a4 commit bddad2f
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 13 deletions.
1 change: 1 addition & 0 deletions server/consts/actions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package consts

const (
LOGIN = "login"
CREATE = "create"
READ = "read"
UPDATE = "update"
Expand Down
18 changes: 12 additions & 6 deletions server/core/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ const (

type API struct {
*Logger
Config *env.Config
Version string
Router *web.Router
db *sql.DB
Config *env.Config
Version string
Router *web.Router
Sessions *Sessions
db *sql.DB
}

func NewAPI(config *env.Config, db *sql.DB) *API {
Expand All @@ -34,6 +35,8 @@ func NewAPI(config *env.Config, db *sql.DB) *API {

api.BuildRouter()

api.Sessions = NewSessions()

return &api
}

Expand All @@ -53,15 +56,18 @@ func (a *API) BuildRouter() {

// === Public API ===

// User Register /Login
Post(pubPrefix+"/register", a.CreateUser).
Post(pubPrefix+"/login", a.Login).

// Plural type Reads
Get(pubPrefix+"/users", a.GetUsers).
Get(pubPrefix+"/poets", a.GetPoets).
Get(pubPrefix+"/poems", a.GetPoems).
Get(pubPrefix+"/issues", a.GetIssues).
Get(pubPrefix+"/committees", a.GetCommittees).

// User CRUD
Post(pubPrefix+"/user", a.CreateUser).
// User RUD
Get(pubPrefix+"/user/:"+API_ID_PATH_PARAM, a.GetUser).
Put(pubPrefix+"/user/:"+API_ID_PATH_PARAM, a.UpdateUser).
Delete(pubPrefix+"/user/:"+API_ID_PATH_PARAM, a.DeleteUser).
Expand Down
10 changes: 9 additions & 1 deletion server/core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ package core
const (
POET_DIR = "/poets"

// API CONSTANTS
/*
API CONSTANTS
*/

LOGIN_USERNAME_PARAM = "username"
LOGIN_PASSWORD_PARAM = "password"

SESSION_TOKEN_COOKIE_NAME = "session_token"

POET_FILES_FORM_KEY = "src[]"
POET_PROG_FILENAME = "program"
POET_PARAMS_FILENAME = "parameters"
Expand Down
97 changes: 95 additions & 2 deletions server/core/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,75 @@ func (a *API) CreateUser(rw web.ResponseWriter, req *web.Request) {
// appropriate data from the db. Once we have the user, we will proceed as
// normally below. ✲´*。.❄¨¯`*✲。❄。*。✲´*。.❄¨¯`*✲。❄。*。✲´*。.❄¨¯`*✲。❄。*。

// assign user id
user.Id = uuid.NewV4().String()

// insert data into db tables
err = user.Create(user.Id, a.db)
if err != nil {
a.Error(err.Error())

// send response

return
}

// send success response

fmt.Println("TODO CREATE USER")
a.Info("user %s successfully created!", user.Username)
}

func (a *API) Login(rw web.ResponseWriter, req *web.Request) {
var (
err error
)

err = req.ParseMultipartForm(30 << 20)
if err != nil {
a.Error(err.Error())

// TODO send response

return
}

fmt.Println(req.Form)

// get the username, password from request
user := &types.User{
Username: req.PostFormValue(LOGIN_USERNAME_PARAM),
Password: req.PostFormValue(LOGIN_PASSWORD_PARAM),
}

fmt.Println(user)

err = user.Validate(consts.LOGIN)
if err != nil {
a.Error(err.Error())

// send response

return
}

// authenticate user with password
err = user.Authenticate(a.db)
if err != nil {
a.Error(err.Error())

// return response

return
}

// get session token
sessionToken := a.Sessions.GetTokenByUser(user.Id)

a.Info("user %s successfully logged in!", user.Username)
// return response with session token
fmt.Println(sessionToken)

// TODO send successful response WITH SESSION TOKEN IN COOKIES
}

func (a *API) GetUser(rw web.ResponseWriter, req *web.Request) {
Expand Down Expand Up @@ -141,6 +205,35 @@ func (a *API) CreatePoet(rw web.ResponseWriter, req *web.Request) {
}{}
)

// anyone who sends this request *must* have a session token in their
// request header (or cookies?) since only logged in users can create poets.

// get session token from cookie (maybe use golang CookieJar)
tokenCookie, err := req.Cookie(SESSION_TOKEN_COOKIE_NAME)
if err != nil {
// handle this error
a.Error("User Error: %s", err.Error())

// TODO return response

return
}

token := tokenCookie.Value

// get username from token
userId, validToken := a.Sessions.GetUserByToken(token)
if !validToken {
err = fmt.Errorf("invalid session token!")

// handle this error
a.Error("User Error: %s", err.Error())

// TODO return response

return
}

// parse multipart-form from request
err = req.ParseMultipartForm(30 << 20)
if err != nil {
Expand Down Expand Up @@ -246,7 +339,7 @@ func (a *API) CreatePoet(rw web.ResponseWriter, req *web.Request) {

// initialize poet struct
poet := &types.Poet{
Designer: "TODO NEED TO GET THE USER UUID",
Designer: userId,
Name: req.PostFormValue(POET_NAME_PARAM),
Description: req.PostFormValue(POET_DESCRIPTION_PARAM),
Language: req.PostFormValue(POET_LANGUAGE_PARAM),
Expand Down
72 changes: 72 additions & 0 deletions server/core/sessions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package core

import (
"sync"
"time"

uuid "github.com/satori/go.uuid"
)

type Sessions struct {
sync.Mutex
TokenToUser map[string]string
UserToToken map[string]string
TokenLastSeen map[string]time.Time
}

func NewSessions() *Sessions {
return &Sessions{
TokenToUser: map[string]string{},
UserToToken: map[string]string{},
TokenLastSeen: map[string]time.Time{},
}
}

func (s *Sessions) GetTokenByUser(userID string) string {
var (
token string
exists bool
)

// lock writer (since this will be called concurrently)
s.Lock()
defer s.Unlock()

token, exists = s.UserToToken[userID]
if !exists {
// create new session token for user
token = uuid.NewV4().String()
s.TokenToUser[token] = userID
s.UserToToken[userID] = token
}

// update the token last seen
s.TokenLastSeen[token] = time.Now()

return token
}

func (s *Sessions) GetUserByToken(token string) (string, bool) {
var (
userId string
exists bool
)

s.Lock()
defer s.Unlock()

userId, exists = s.TokenToUser[token]
if exists {
// update the token last seen
s.TokenLastSeen[token] = time.Now()
}

return userId, exists
}

// TODO we eventually want to have a go-routine constantly running in the background
// at a specified interval which will expire and evict session tokens if no requests
// have been made by a user in a certain time window.
func (s *Sessions) ExpireSessions() {

}
61 changes: 57 additions & 4 deletions server/types/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/connorwalsh/new-yorken-poesry-magazine/server/consts"
"github.com/connorwalsh/new-yorken-poesry-magazine/server/utils"
_ "github.com/lib/pq"
uuid "github.com/satori/go.uuid"
)

type User struct {
Expand All @@ -25,6 +26,7 @@ func (u *User) Validate(action string) error {

// perform validation on a per action basis
switch action {
case consts.LOGIN:
case consts.CREATE:
case consts.UPDATE:
case consts.DELETE:
Expand All @@ -51,6 +53,7 @@ func (u *User) Validate(action string) error {

// package level globals for storing prepared sql statements
var (
userAuthStmt *sql.Stmt
userCreateStmt *sql.Stmt
userReadStmt *sql.Stmt
userReadAllStmt *sql.Stmt
Expand All @@ -64,6 +67,7 @@ func CreateUsersTable(db *sql.DB) error {
id UUID NOT NULL UNIQUE,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
salt UUID NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
PRIMARY KEY (id)
)`
Expand All @@ -78,34 +82,83 @@ func CreateUsersTable(db *sql.DB) error {

func (u *User) Create(id string, db *sql.DB) error {
var (
err error
hashedPassword string
salt string
err error
)

// we assume that all validation/sanitization has already been called

// assign id
u.Id = id

// generate salt for password
salt = uuid.NewV4().String()

// salt password
hashedPassword = utils.SaltPassword(u.Password, salt)

// prepare statement if not already done so.
if userCreateStmt == nil {
// create statement
stmt := `INSERT INTO users (
id, username, password, email
) VALUES ($1, $2, $3, $4)`
id, username, password, salt, email
) VALUES ($1, $2, $3, $4, $5)`
userCreateStmt, err = db.Prepare(stmt)
if err != nil {
return err
}
}

_, err = userCreateStmt.Exec(u.Id, u.Username, u.Password, u.Email)
_, err = userCreateStmt.Exec(u.Id, u.Username, hashedPassword, salt, u.Email)
if err != nil {
return err
}

return nil
}

func (u *User) Authenticate(db *sql.DB) error {
var (
hashedPassword string
salt string
err error
)

if userAuthStmt == nil {
// auth stmt
stmt := `SELECT id, password, salt FROM users WHERE username = $1`
userAuthStmt, err = db.Prepare(stmt)
if err != nil {
return err
}
}

// assume that auth validation for user has been performed

// run the prepared stmt over args (username)
err = userAuthStmt.
QueryRow(u.Username).
Scan(&u.Id, &hashedPassword, &salt)
switch {
case err == sql.ErrNoRows:
return fmt.Errorf("incorrect username or password AAHHH")
case err != nil:
return err
}

// hash provided user password
passwd := utils.SaltPassword(u.Password, salt)

// ensure that our hashed provided password matches our hashed saved password
if passwd != hashedPassword {
// oops, wrong password
return fmt.Errorf("incorrect username or password")
}

return nil
}

func (u *User) Read(db *sql.DB) error {
var (
err error
Expand Down
Loading

0 comments on commit bddad2f

Please sign in to comment.