@@ -5,11 +5,30 @@ import (
"database/sql"
"html/template"
"log"
"strings"

"github.com/jmoiron/sqlx"
sq "github.com/Masterminds/squirrel"
"github.com/murder-hobos/murder-hobos/util"
)

// SpellDatastore describes valid methods we have on our database
// pertaining to Spells
type SpellDatastore interface {
GetAllCannonSpells() (*[]Spell, error)
GetCannonSpellByName(name string) (*Spell, error)
SearchCannonSpells(name string) (*[]Spell, error)
FilterCannonSpells(level, school string) (*[]Spell, error)

GetAllUserSpells(userID int) (*[]Spell, error)
GetUserSpellByName(userID int, name string) (*Spell, error)
SearchUserSpells(userID int, name string) (*[]Spell, error)
FilterUserSpells(userID int, level, school string) (*[]Spell, error)

GetSpellByID(id int) (*Spell, error)
GetSpellClasses(spellID int) (*[]Class, error)
CreateSpell(uid int, spell Spell) (id int, err error)
}

// Spell represents our database version of a spell
type Spell struct {
ID int `db:"id"`
@@ -64,95 +83,166 @@ func (s *Spell) HTMLDescription() template.HTML {
return template.HTML(s.Description)
}

// GetAllSpells returns a slice of all spells in the database.
// userID can be 0. If otherwise specified, spells with the corresponding sourceID are
// included in the result.
// includeCannon chooses whether or not to include cannon(PHB, EE, SCAG) spells.
func (db *DB) GetAllSpells(userID int, includeCannon bool) (*[]Spell, error) {
// verify arguments
if userID == 0 && !includeCannon {
return nil, ErrNoResult
}
if userID < 0 {
return nil, ErrInvalidID
// LevelStr provides the spell's level as a string, with "Cantrip" for level 0
func (s *Spell) LevelStr() string {
if s.Level == "0" {
return "Cantrip"
}
return s.Level
}

var ids []int
if userID > 0 {
ids = append(ids, userID)
// GetAllCannonSpells returns a list of every cannon spell object
// in our database (PHB, EE, SCAG)
func (db *DB) GetAllCannonSpells() (*[]Spell, error) {
spells := &[]Spell{}
if err := db.Select(spells, `SELECT * FROM CannonSpells`); err != nil {
log.Printf("model: GetAllCannonSpells: %s", err.Error())
return nil, err
}
if includeCannon {
ids = append(ids, cannonIDs...)
return spells, nil
}

// GetAllUserSpells gets a list of every spell that a
// specified user has created in our database
func (db *DB) GetAllUserSpells(userID int) (*[]Spell, error) {
if userID <= 0 {
return nil, ErrInvalidID
}

query, args, err := sqlx.In(`SELECT * FROM Spell WHERE source_id IN (?);`, ids)
spells := &[]Spell{}
err := db.Select(spells, `SELECT * FROM Spell WHERE source_id=?`, userID)
if err != nil {
log.Printf("Error preparing sqlx.In statement: %s\n", err.Error())
return nil, err
}
query = db.Rebind(query)
return spells, nil
}

// SearchCannonSpells gets a list of cannon spells with names similar
// to `name`
func (db *DB) SearchCannonSpells(name string) (*[]Spell, error) {
query := `SELECT * FROM CannonSpells
WHERE name LIKE CONCAT ('%', ?, '%')
ORDER BY name ASC`
spells := &[]Spell{}
if err := db.Select(spells, query, args...); err != nil {
if err := db.Select(spells, query, name); err != nil {
log.Printf("Error executing query %s\n %s\n", query, err.Error())
return nil, err
}

return spells, nil
}

// GetSpellByID searches db for a Spell row with a matching id
func (db *DB) GetSpellByID(id int) (*Spell, error) {
if id <= 0 {
// SearchUserSpells gets a list of a user's spells with names similar
// to `name`
func (db *DB) SearchUserSpells(userID int, name string) (*[]Spell, error) {
// don't hit the db with bunk query
if userID <= 0 {
return nil, ErrInvalidID
}
if name == "" {
return nil, ErrNoResult
}

spells := &[]Spell{}
err := db.Select(spells, `SELECT * FROM Spell
WHERE source_id=?
AND name LIKE CONCAT ('%', ?, '%')
ORDER BY name ASC;`, userID, name)
if err != nil {
log.Printf("model: SearchUserSpellByName: %s\n", err.Error())
return nil, err
}

return spells, nil
}

// GetCannonSpellByName returns a single cannon spell with matching name
func (db *DB) GetCannonSpellByName(name string) (*Spell, error) {
if name == "" {
return nil, ErrNoResult
}

s := &Spell{}
if err := db.Get(s, "SELECT * FROM Spell WHERE id=?", id); err != nil {
err := db.Get(s, "SELECT * FROM CannonSpells WHERE name=?", name)
if err != nil {
return nil, err
}
return s, nil
}

// GetSpellByName searches the datastore for a spell with the matching name.
// userID may be 0 for no user. If otherwise specified, search is restricted to that user's spells.
// includeCannon decides whether or not to search cannon spells.
// These options exist to enable different users to create spells with the same name,
// or the same name as cannon spells if they so choose
// NOTE: specifying nil userID and false for isCannon returns no result (hopefully obvious)
func (db *DB) GetSpellByName(name string, userID int, isCannon bool) (*Spell, error) {
// verify arguments before hitting the db
// GetUserSpellByName returns a single cannon spell with matching name
func (db *DB) GetUserSpellByName(userID int, name string) (*Spell, error) {
if userID <= 0 {
return nil, ErrInvalidID
}
if name == "" {
return nil, ErrNoResult
}
if userID < 0 {
return nil, ErrInvalidID

s := &Spell{}
err := db.Get(s, "SELECT * FROM Spell WHERE source_id=? AND name=?", userID, name)
if err != nil {
return nil, err
}
if userID == 0 && !isCannon {
return s, nil
}

// FilterCannonSpells returns a list of cannon spells matching
// the search critera. If an empty argument is passed to one of the
// filters, that argument is not considered for filtering.
func (db *DB) FilterCannonSpells(level, school string) (*[]Spell, error) {
if level == "" && school == "" {
return nil, ErrNoResult
}

var ids []int
if userID > 0 { // If given a specific user, only search that
ids = append(ids, userID)
} else { // at this point isCannon must be true
ids = append(ids, cannonIDs...)
eqs := sq.Eq{}
if level != "" {
eqs["level"] = level
}
if school != "" {
eqs["school"] = school
}

query, args, err := sqlx.In(`SELECT * FROM Spell
WHERE name=? AND
source_id in (?);`,
name, ids)
query, args, err := sq.Select("*").From("CannonSpells").Where(eqs).ToSql()

spells := &[]Spell{}
err = db.Select(spells, query, args...)
if err != nil {
log.Printf("Error preparing sqlx.In statement: %s\n", err.Error())
return nil, err
}
query = db.Rebind(query)
return spells, nil
}

s := &Spell{}
if err := db.Get(s, query, args...); err != nil {
log.Printf("Error executing query %s\n %s\n", query, err.Error())
// FilterUserSpells returns a list of user spells matching
// the search critera. If an empty argument is passed to one of the
// filters, that argument is not considered for filtering.
// NOTE: name is given as a search param, not matched exactly
func (db *DB) FilterUserSpells(userID int, level, school string) (*[]Spell, error) {
if userID <= 0 {
return nil, ErrInvalidID
}
if level == "" && school == "" {
return nil, ErrNoResult
}

eqs := sq.Eq{}
eqs["source_id"] = userID

if level != "" {
eqs["level"] = level
}
if school != "" {
eqs["school"] = school
}

query, args, err := sq.Select("*").From("Spell").Where(eqs).ToSql()

spells := &[]Spell{}
err = db.Select(spells, query, args...)
if err != nil {
return nil, err
}
return s, nil
return spells, nil
}

// GetSpellClasses searches the database and returns a slice of
@@ -176,3 +266,37 @@ func (db *DB) GetSpellClasses(spellID int) (*[]Class, error) {
}
return cs, nil
}

// GetSpellByID returns a single spell with matching id
func (db *DB) GetSpellByID(id int) (*Spell, error) {
if id <= 0 {
return nil, ErrNoResult
}
s := &Spell{}
if err := db.Get(s, "SELECT * FROM Spell WHERE id=?", id); err != nil {
return nil, err
}
return s, nil
}

// CreateSpell adds a spell to the database, created by specified user
func (db *DB) CreateSpell(uid int, spell Spell) (id int, err error) {
// EWW SO UGLY BUT I WANT <BR>S IN DESCRIPTION AND I'M TOO LAZY RIGHT NOW
// TO WRITE A CONVERTER FROM \n TO <BR>
d := strings.Replace(spell.Description, "<script>", "", -1)
desc := strings.Replace(d, "</script>", "", -1)

res, err := db.Exec(`INSERT INTO Spell (name, level, school, cast_time, duration, `+"`range`, "+
`comp_verbal, comp_somatic, comp_material, material_desc, concentration,
ritual, description, source_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
spell.Name, spell.Level, spell.School, spell.CastTime, spell.Duration,
spell.Range, spell.Verbal, spell.Somatic, spell.Material, spell.MaterialDesc,
spell.Concentration, spell.Ritual, desc, spell.SourceID)
if err != nil {
return 0, err
}
if i, err := res.LastInsertId(); err != nil {
return int(i), nil
}
return 0, err
}
@@ -1,8 +1,74 @@
package model

import (
"log"

"golang.org/x/crypto/bcrypt"
)

// UserDatastore describes methods available to our database
// pertaining to users
type UserDatastore interface {
GetUserByUsername(name string) (*User, bool)
GetUserByID(id int) (*User, bool)
CreateUser(name, password string) (*User, bool)
}

// User represents a user in our application
type User struct {
ID int `db:"id"`
Username string `db:"username"`
Password []byte `db:"password"`
}

// GetUserByUsername returns a user object with matching
// username
func (db *DB) GetUserByUsername(name string) (*User, bool) {
u := &User{}
err := db.Get(u, `SELECT id, username, password
FROM User
WHERE username = ?`,
name)
if err != nil {
return nil, false
}
return u, true
}

// GetUserByID returns a user object with matchin
// id if found
func (db *DB) GetUserByID(id int) (*User, bool) {
u := &User{}
err := db.Get(u, `SELECT id, username, password
FROM User
WHERE id=?`, id)
if err != nil {
return nil, false
}
return u, true
}

// CreateUser adds a user to our database
func (db *DB) CreateUser(name, password string) (*User, bool) {
p, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
log.Printf("CreateUser: %s", err.Error())
return nil, false
}
res, err := db.Exec(`INSERT INTO `+"`User`"+` (username, password) VALUES (?, ?)`,
name, p)
if err != nil {
log.Printf("CreateUser: %s", err.Error())
return nil, false
}
id, err := res.LastInsertId()
if err != nil {
log.Printf("CreateUser: %s", err.Error())
return nil, false
}
user := &User{
ID: int(id),
Username: name,
}
return user, true
}
@@ -0,0 +1,141 @@
package routes

import (
"context"
"log"
"net/http"
"os"
"time"

jwt "github.com/dgrijalva/jwt-go"
"golang.org/x/crypto/bcrypt"
)

// Claims is a struct that represents the data stored in our
// tokens
type Claims struct {
UID int `json:"uid"`
Username string `json:"username"`
jwt.StandardClaims
}

func (env *Env) loginIndex(w http.ResponseWriter, r *http.Request) {
// already authenticated
if claims := r.Context().Value("Claims"); claims != nil {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}

errs := r.Context().Value("Errors")
log.Println(errs)
// We keep claims here because base template requires at least a nil claims
data := map[string]interface{}{
"Claims": nil,
"Errors": errs,
}

if t, ok := tmpls["login.html"]; ok {
t.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
}
}

func (env *Env) loginProcess(w http.ResponseWriter, r *http.Request) {
u := r.PostFormValue("username")
p := r.PostFormValue("password")

if u == "" || p == "" {
loginPageWithErrors(w, r, "Username or Password cannot be empty.")
return
}

//Authenticate user
user, ok := env.db.GetUserByUsername(u)
if !ok {
loginPageWithErrors(w, r, "Invalid Username or Password")
return
}

if err := bcrypt.CompareHashAndPassword(user.Password, []byte(p)); err != nil {
// Invalid password
loginPageWithErrors(w, r, "Invalid Username or Password")
return

}
assignToken(w, user.ID, user.Username)

http.Redirect(w, r, "/", http.StatusFound)
return
}

// Logs a user out by "deleting" their token
func (env *Env) logoutProcess(w http.ResponseWriter, r *http.Request) {
deleteCookie := http.Cookie{Name: "Auth", Value: "none", Expires: time.Now()}
http.SetCookie(w, &deleteCookie)
http.Redirect(w, r, "/", http.StatusFound)
}

// little utility
func loginPageWithErrors(w http.ResponseWriter, r *http.Request, errs ...string) {
r.Method = "GET"
ctx := context.WithValue(r.Context(), "Errors", errs)
http.Redirect(w, r.WithContext(ctx), "/login", http.StatusSeeOther)
return
}

// Creates a new user
func (env *Env) registerProcess(w http.ResponseWriter, r *http.Request) {
u := r.PostFormValue("username")
p := r.PostFormValue("password")
pc := r.PostFormValue("confirm-password")

if u == "" || p == "" || pc == "" {
loginPageWithErrors(w, r, "Username or Password cannot be empty.")
return
}

if p != pc {
loginPageWithErrors(w, r, "Passwords do not match.")
}

if user, ok := env.db.CreateUser(u, p); ok {
log.Println("User created")
assignToken(w, user.ID, user.Username)
http.Redirect(w, r, "/", http.StatusFound)
return
}

errorHandler(w, r, http.StatusInternalServerError)
}

func assignToken(w http.ResponseWriter, id int, uname string) {
expireToken := time.Now().Add(time.Hour * 1).Unix()
expireCookie := time.Now().Add(time.Hour * 1)

claims := Claims{
id,
uname,
jwt.StandardClaims{
ExpiresAt: expireToken,
Issuer: "localhost:8081",
},
}

// Generate signed token with our claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString([]byte(os.Getenv("TOKEN_SIGNING_KEY")))
if err != nil {
http.Error(w, "Error creating signed token", http.StatusInternalServerError)
}

// Store our token on client
cookie := http.Cookie{
Name: "Auth",
Value: signedToken,
Expires: expireCookie,
HttpOnly: true,
}
http.SetCookie(w, &cookie)

}
@@ -0,0 +1,112 @@
package routes

import (
"log"
"net/http"
"strconv"

"github.com/gorilla/mux"
"github.com/murder-hobos/murder-hobos/model"
"github.com/murder-hobos/murder-hobos/util"
)

func (env *Env) characterIndex(w http.ResponseWriter, r *http.Request) {
c := r.Context().Value("Claims")
claims := c.(Claims)

chars, err := env.db.GetAllCharacters(claims.UID)
if err != nil && err != model.ErrNoResult {
errorHandler(w, r, http.StatusInternalServerError)
}

data := map[string]interface{}{
"Claims": claims,
"Characters": chars,
}

if tmpl, ok := env.tmpls["characters.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
return
}

}

// Information about specific character
func (env *Env) characterDetails(w http.ResponseWriter, r *http.Request) {
c := r.Context().Value("Claims")
claims := c.(Claims)
name := mux.Vars(r)["characterName"]

char := &model.Character{}
c, err := env.db.GetCharacterByName(claims.UID, name)
if err != nil {
log.Printf("Error getting Character with name: %s\n", name)
log.Printf(err.Error())
errorHandler(w, r, http.StatusNotFound)
return
}

data := map[string]interface{}{
"Claims": claims,
"Character": char,
}

if tmpl, ok := env.tmpls["character-details.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for class-details\n")
return
}
}

func (env *Env) newCharacterIndex(w http.ResponseWriter, r *http.Request) {
claims, _ := r.Context().Value("Claims").(Claims)

data := map[string]interface{}{
"Claims": claims,
}

if tmpl, ok := env.tmpls["character-creator.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
log.Println("EXECUTED")
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for character-creator\n")
return
}
}

func (env *Env) newCharacterProcess(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims").(Claims)

name := r.PostFormValue("name")
//class := r.PostFormValue("class")
//level := r.PostFormValue("level")
race := r.PostFormValue("race")
a, _ := strconv.Atoi(r.PostFormValue("abilityMod"))
p, _ := strconv.Atoi(r.PostFormValue("profBonus"))
ability := util.ToNullInt64(a)
proficiency := util.ToNullInt64(p)

char := &model.Character{
Name: name,
Race: race,
SpellAbilityModifier: ability,
ProficienyBonus: proficiency,
UserID: claims.UID,
}

if _, err := env.db.CreateCharacter(claims.UID, char); err != nil {
log.Printf("CreateCharacter: %s\n", err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}
// if _, err := env.db.SetCharacterLevel(charID, className, level int); err != nil {
// errorHandler(w,r,http.StatusInternalServerError)
// }
r.Method = "GET"
http.Redirect(w, r, "/user/character", http.StatusFound)
}
@@ -0,0 +1,68 @@
package routes

import (
"log"
"net/http"

"github.com/gorilla/mux"
)

// lists all classes
func (env *Env) classIndex(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims")

cs, err := env.db.GetAllClasses()
if err != nil {
log.Println("Classes handler: " + err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}

data := map[string]interface{}{
"Claims": claims,
"Classes": cs,
}

if tmpl, ok := env.tmpls["classes.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for classes\n")
return
}
}

// Shows a list of all spells available to a class
func (env *Env) classDetails(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims")
name := mux.Vars(r)["className"]

class, err := env.db.GetClassByName(name)
if err != nil {
log.Printf("Error getting Class by name: %s\n", name)
log.Printf(err.Error())
errorHandler(w, r, http.StatusNotFound)
return
}

spells, err := env.db.GetClassSpells(class.ID)
if err != nil {
log.Println("Class-detail handler" + err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}

data := map[string]interface{}{
"Claims": claims,
"Class": class,
"Spells": spells,
}

if tmpl, ok := env.tmpls["class-details.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for class-details\n")
return
}
}
@@ -1 +1,53 @@
package routes

import (
"context"
"fmt"
"log"
"net/http"
"os"

jwt "github.com/dgrijalva/jwt-go"
)

// withClaims checks the request for a valid auth token.
// If valid, the Claims object is added to the request's context
func (env *Env) withClaims(fn http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Auth")
if err != nil {
fn.ServeHTTP(w, r)
return
}

token, err := jwt.ParseWithClaims(cookie.Value, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// Make sure token's signature wasn't changed
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected siging method")
}
return []byte(os.Getenv("TOKEN_SIGNING_KEY")), nil
})

if claims, ok := token.Claims.(*Claims); ok && token.Valid {
ctx := context.WithValue(r.Context(), "Claims", *claims)
fn.ServeHTTP(w, r.WithContext(ctx))
} else {
fn.ServeHTTP(w, r)
return
}
})
}

func (env *Env) authRequired(fn http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if c := r.Context().Value("Claims"); c != nil {
if _, ok := c.(Claims); ok {
log.Println("AUTHED")
fn.ServeHTTP(w, r)
}
} else {
http.Redirect(w, r, "/", http.StatusUnauthorized)
return
}
})
}
@@ -8,7 +8,7 @@ import (
"strings"

"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/justinas/alice"
"github.com/murder-hobos/murder-hobos/model"
)

@@ -17,17 +17,11 @@ var (
tmpls map[string]*template.Template
)

const (
sessionKey = "murder-hobos"
)

// Env is a struct that defines an enviornment for server request handling.
// It allows us to specify different combinations of datastores, templates,
// and session stores
type Env struct {
db model.Datastore
tmpls map[string]*template.Template
store *sessions.CookieStore
}

func init() {
@@ -58,163 +52,68 @@ func init() {
// Panics if unable to connect to datastore with given dsn
// (don't want the server to start without database access)
func New(dsn string) *mux.Router {
store := sessions.NewCookieStore([]byte("super-secret-key-that-is-totally-secure"))

db, err := model.NewDB(dsn)
if err != nil {
panic(err)
}
env := &Env{db, tmpls, store}
env := &Env{db, tmpls}

stdChain := alice.New(env.withClaims)
userChain := stdChain.Append(env.authRequired)
r := mux.NewRouter()

r.HandleFunc("/", indexHandler)
r.HandleFunc("/class/{className}", env.classDetailsHandler)
r.HandleFunc("/classes", env.classesHandler)
r.HandleFunc("/spell/{spellName}", env.spellDetailsHandler)
r.HandleFunc("/spells", env.spellsHandler)
// SPELL
r.Handle(`/spell/{spellName:[a-zA-Z '\-\/]+}`, stdChain.ThenFunc(env.spellDetails))
r.Handle("/spell", stdChain.ThenFunc(env.spellSearch)).Queries("name", "")
r.Handle("/spell", stdChain.ThenFunc(env.spellFilter)).Queries("school", "")
r.Handle("/spell", stdChain.ThenFunc(env.spellFilter)).Queries("level", "{level:[0-9]}")
r.Handle("/spell", stdChain.ThenFunc(env.spellFilter)).Queries("school", "", "level", "{level:[0-9]}")
r.Handle("/spell", stdChain.ThenFunc(env.spellIndex))

// CLASS
r.Handle("/class/{className}", stdChain.ThenFunc(env.classDetails))
r.Handle("/class", stdChain.ThenFunc(env.classIndex))

// AUTH
r.Handle("/login", stdChain.ThenFunc(env.loginIndex)).Methods("GET")
r.Handle("/login", stdChain.ThenFunc(env.loginProcess)).Methods("POST")
r.Handle("/register", stdChain.ThenFunc(env.registerProcess)).Methods("POST")
r.Handle("/logout", stdChain.ThenFunc(env.logoutProcess))

// USER
r.Handle("/user/spell/new", userChain.ThenFunc(env.newSpellIndex)).Methods("GET")
r.Handle("/user/spell/new", userChain.ThenFunc(env.newSpellProcess)).Methods("POST")
r.Handle(`/user/spell/{spellName:[a-zA-Z '\-\/]+}`, userChain.ThenFunc(env.userSpellDetails))
r.Handle("/user/spell", userChain.ThenFunc(env.userSpellSearch)).Queries("name", "")
r.Handle("/user/spell", userChain.ThenFunc(env.userSpellFilter)).Queries("school", "")
r.Handle("/user/spell", userChain.ThenFunc(env.userSpellFilter)).Queries("level", "{level:[0-9]}")
r.Handle("/user/spell", userChain.ThenFunc(env.userSpellFilter)).Queries("school", "", "level", "{level:[0-9]}")
r.Handle("/user/spell", userChain.ThenFunc(env.userSpellIndex))
r.Handle("/user/spell", userChain.ThenFunc(env.userSpellIndex))
r.Handle("/user/character", userChain.ThenFunc(env.characterIndex))
r.Handle("/user/character/new", userChain.ThenFunc(env.newCharacterIndex)).Methods("GET")
r.Handle("/user/character/new", userChain.ThenFunc(env.newCharacterProcess)).Methods("POST")
r.Handle("/user", userChain.ThenFunc(env.userProfileIndex))

// ROOT
r.Handle("/", stdChain.ThenFunc(rootIndex))

r.PathPrefix("/static").HandlerFunc(staticHandler)
return r
}

// Index doesn't really do much for now
func indexHandler(w http.ResponseWriter, r *http.Request) {
if tmpl, ok := tmpls["index.html"]; ok {
tmpl.ExecuteTemplate(w, "base", nil)
} else {
errorHandler(w, r, http.StatusInternalServerError)
}
}

// List all spells. We really should chache this eventually
// instead of hitting the db everytime
func (env *Env) spellsHandler(w http.ResponseWriter, r *http.Request) {
var userID int
includeCannon := true // want to default to true, not false

if i, ok := env.getIntFromSession(r, "userID"); ok {
userID = i
}
if b, ok := env.getBoolFromSession(r, "includeCannon"); ok {
includeCannon = b
}

spells, err := env.db.GetAllSpells(userID, includeCannon)
if err != nil {
if err.Error() == "empty slice passed to 'in' query" || err == model.ErrNoResult {
// do nothing, just show no results on page (already in template)
} else { // something happened
log.Println(err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}
}

if tmpl, ok := env.tmpls["spells.html"]; ok {
tmpl.ExecuteTemplate(w, "base", spells)
} else {
errorHandler(w, r, http.StatusInternalServerError)
}
}

// Show information about a single spell
func (env *Env) spellDetailsHandler(w http.ResponseWriter, r *http.Request) {
var userID int
includeCannon := true // want to default to true, not false

if i, ok := env.getIntFromSession(r, "userID"); ok {
userID = i
}
if b, ok := env.getBoolFromSession(r, "includeCannon"); ok {
includeCannon = b
}

name := mux.Vars(r)["spellName"]

s, err := env.db.GetSpellByName(name, userID, includeCannon)
if err != nil {
log.Printf("Error getting spell by name: %s, userID: %d, isCannon: %t\n", name, s.ID, true)
log.Printf(err.Error())
errorHandler(w, r, http.StatusNotFound)
return
}

cs, err := env.db.GetSpellClasses(s.ID)
// we shouldn't have an error at this point, we should have a spell
if err != nil {
log.Printf("Error getting spell classes with id %d\n", s.ID)
log.Println(err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}

if tmpl, ok := env.tmpls["spell-details.html"]; ok {
data := struct {
Spell *model.Spell
Classes *[]model.Class
}{
s,
cs,
}
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for spell-details\n")
return
}
}

// lists all classes
func (env *Env) classesHandler(w http.ResponseWriter, r *http.Request) {
cs, err := env.db.GetAllClasses()
if err != nil {
log.Println("Classes handler: " + err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}
func rootIndex(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims")

if tmpl, ok := env.tmpls["classes.html"]; ok {
tmpl.ExecuteTemplate(w, "base", cs)
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for classes\n")
return
data := map[string]interface{}{
"Claims": claims,
}
}

// Shows a list of all spells available to a class
func (env *Env) classDetailsHandler(w http.ResponseWriter, r *http.Request) {
name := mux.Vars(r)["className"]
c := &model.Class{}

c, err := env.db.GetClassByName(name)
if err != nil {
log.Printf("Error getting Class by name: %s\n", name)
log.Printf(err.Error())
errorHandler(w, r, http.StatusNotFound)
return
}

spells, err := env.db.GetClassSpells(c.ID)
if err != nil {
log.Println("Class-detail handler" + err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}

data := struct {
Class *model.Class
Spells *[]model.Spell
}{
c,
spells,
}
if tmpl, ok := env.tmpls["class-details.html"]; ok {
if tmpl, ok := tmpls["index.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for class-details\n")
return
}
}

@@ -252,49 +151,3 @@ func errorHandler(w http.ResponseWriter, r *http.Request, status int) {
vars := map[string]string{"Title": title, "Message": message}
tmpl.ExecuteTemplate(w, "base", vars)
}

// litle utils
func (env *Env) getStringFromSession(r *http.Request, key string) (string, bool) {
sess, err := env.store.Get(r, sessionKey)
if err != nil {
return "", false
}
val, ok := sess.Values[key]
if !ok {
return "", false
}
if s, ok := val.(string); ok {
return s, true
}
return "", false
}

func (env *Env) getIntFromSession(r *http.Request, key string) (int, bool) {
sess, err := env.store.Get(r, sessionKey)
if err != nil {
return 0, false
}
val, ok := sess.Values[key]
if !ok {
return 0, false
}
if i, ok := val.(int); ok {
return i, true
}
return 0, false
}

func (env *Env) getBoolFromSession(r *http.Request, key string) (bool, bool) {
sess, err := env.store.Get(r, sessionKey)
if err != nil {
return false, false
}
val, ok := sess.Values[key]
if !ok {
return false, false
}
if b, ok := val.(bool); ok {
return b, true
}
return false, false
}
@@ -0,0 +1,124 @@
package routes

import (
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/murder-hobos/murder-hobos/model"
)

func (env *Env) spellIndex(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims")

spells, err := env.db.GetAllCannonSpells()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}

data := map[string]interface{}{
"Spells": spells,
"Claims": claims,
}

if tmpl, ok := env.tmpls["spells.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
return
}
}

func (env *Env) spellFilter(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims")

level := r.FormValue("level")
school := r.FormValue("school")

spells, err := env.db.FilterCannonSpells(level, school)
if err != nil {
if err == model.ErrNoResult {
// do nothing, just show no results on page (already in template)
} else { // something happened
log.Printf("routes - cannonSpells: Error filtering cannon spells: %s\n", err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}
}

data := map[string]interface{}{
"Spells": spells,
"Claims": claims,
}

if tmpl, ok := env.tmpls["spells.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
}
}

func (env *Env) spellSearch(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims")
name := r.FormValue("name")

spells, err := env.db.SearchCannonSpells(name)
if err != nil {
if err == model.ErrNoResult {
// do nothing, just show no results on page (already in template)
} else { // something happened
log.Printf("routes - cannonSpells: Error filtering cannon spells: %s\n", err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}
}

data := map[string]interface{}{
"Spells": spells,
"Claims": claims,
}

if tmpl, ok := env.tmpls["spells.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
}
}

// Show information about a single spell
func (env *Env) spellDetails(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims")
name := mux.Vars(r)["spellName"]

spell, err := env.db.GetCannonSpellByName(name)
if err != nil {
log.Printf("Error getting spell by name: %s\n", name)
log.Printf(err.Error())
errorHandler(w, r, http.StatusNotFound)
return
}

classes, err := env.db.GetSpellClasses(spell.ID)
// we shouldn't have an error at this point, we should have a spell
if err != nil {
log.Printf("Error getting spell classes with id %d\n", spell.ID)
log.Println(err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}

data := map[string]interface{}{
"Spell": spell,
"Classes": classes,
"Claims": claims,
"IsCannon": true,
}

if tmpl, ok := env.tmpls["spell-details.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for spell-details\n")
return
}
}
@@ -0,0 +1,207 @@
package routes

import (
"log"
"net/http"
"strconv"

"github.com/gorilla/mux"
"github.com/murder-hobos/murder-hobos/model"
"github.com/murder-hobos/murder-hobos/util"
)

func (env *Env) userSpellIndex(w http.ResponseWriter, r *http.Request) {
c := r.Context().Value("Claims")
claims := c.(Claims)

spells, err := env.db.GetAllUserSpells(claims.UID)
if err != nil && err != model.ErrNoResult {
errorHandler(w, r, http.StatusInternalServerError)
}

data := map[string]interface{}{
"Claims": claims,
"Spells": spells,
}

if tmpl, ok := env.tmpls["user-spells.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
return
}
}

func (env *Env) userSpellFilter(w http.ResponseWriter, r *http.Request) {
c := r.Context().Value("Claims")
claims := c.(Claims)

level := r.FormValue("level")
school := r.FormValue("school")

spells, err := env.db.FilterUserSpells(claims.UID, level, school)
if err != nil {
if err == model.ErrNoResult {
// do nothing, just show no results on page (already in template)
} else { // something happened
log.Printf("routes - userSpells: Error filtering cannon spells: %s\n", err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}
}

data := map[string]interface{}{
"Spells": spells,
"Claims": claims,
}

if tmpl, ok := env.tmpls["user-spells.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
}
}

func (env *Env) userSpellSearch(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims").(Claims)
name := r.FormValue("name")

spells, err := env.db.SearchUserSpells(claims.UID, name)
if err != nil {
if err == model.ErrNoResult {
// do nothing, just show no results on page (already in template)
} else { // something happened
log.Printf("routes - cannonSpells: Error filtering cannon spells: %s\n", err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}
}

data := map[string]interface{}{
"Spells": spells,
"Claims": claims,
}

if tmpl, ok := env.tmpls["user-spells.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
}
}

// Show information about a single spell
func (env *Env) userSpellDetails(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims").(Claims)
name := mux.Vars(r)["spellName"]

spell, err := env.db.GetUserSpellByName(claims.UID, name)
if err != nil {
log.Printf("Error getting spell by name: %s\n", name)
log.Printf(err.Error())
errorHandler(w, r, http.StatusNotFound)
return
}

classes, err := env.db.GetSpellClasses(spell.ID)
// we shouldn't have an error at this point, we should have a spell
if err != nil {
log.Printf("Error getting spell classes with id %d\n", spell.ID)
log.Println(err.Error())
errorHandler(w, r, http.StatusInternalServerError)
return
}

data := map[string]interface{}{
"Spell": spell,
"Classes": classes,
"Claims": claims,
"IsUser": true,
}

if tmpl, ok := env.tmpls["spell-details.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for spell-details\n")
return
}
}

func (env *Env) newSpellProcess(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("Claims").(Claims)

r.ParseForm()
name := r.FormValue("name")
school := r.FormValue("school")
level := r.FormValue("level")
castTime := r.FormValue("castTime")
duration := r.FormValue("duration")
ran := r.FormValue("range")
verbal, _ := strconv.ParseBool(r.FormValue("verbal"))
somatic, _ := strconv.ParseBool(r.FormValue("somatic"))
material, _ := strconv.ParseBool(r.FormValue("material"))
materialDesc := util.ToNullString(r.FormValue("materialDesc"))
conc, _ := strconv.ParseBool(r.FormValue("concentration"))
ritual, _ := strconv.ParseBool(r.FormValue("ritual"))
desc := r.FormValue("spellDesc")
sourceID := claims.UID

spell := &model.Spell{
0,
name,
level,
school,
castTime,
duration,
ran,
verbal,
somatic,
material,
materialDesc,
conc,
ritual,
desc,
sourceID,
}
if _, err := env.db.CreateSpell(claims.UID, *spell); err != nil {
errorHandler(w, r, http.StatusInternalServerError)
log.Println(err.Error())
return
}
http.Redirect(w, r, "/user/spell", http.StatusFound)
}

func (env *Env) newSpellIndex(w http.ResponseWriter, r *http.Request) {
log.Println("newSpellIndex")
claims, _ := r.Context().Value("Claims").(Claims)

data := map[string]interface{}{
"Claims": claims,
}

if tmpl, ok := env.tmpls["spell-creator.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
log.Println("EXECUTED")
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for spell-creator\n")
return
}
}

func (env *Env) userProfileIndex(w http.ResponseWriter, r *http.Request) {
claims, _ := r.Context().Value("Claims").(Claims)

data := map[string]interface{}{
"Claims": claims,
}

if tmpl, ok := env.tmpls["profile.html"]; ok {
tmpl.ExecuteTemplate(w, "base", data)
} else {
errorHandler(w, r, http.StatusInternalServerError)
log.Printf("Error loading template for spell-creator\n")
return
}

}
@@ -0,0 +1,113 @@
body {
padding-top: 90px;
}

.panel-login {
border-color: #ccc;
-webkit-box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.2);
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.2);
}

.panel-login>.panel-heading {
color: #00415d;
background-color: #fff;
border-color: #fff;
text-align: center;
}

.panel-login>.panel-heading a {
text-decoration: none;
color: #666;
font-weight: bold;
font-size: 15px;
-webkit-transition: all 0.1s linear;
-moz-transition: all 0.1s linear;
transition: all 0.1s linear;
}

.panel-login>.panel-heading a.active {
color: #029f5b;
font-size: 18px;
}

.panel-login>.panel-heading hr {
margin-top: 10px;
margin-bottom: 0px;
clear: both;
border: 0;
height: 1px;
background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0));
background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0));
background-image: -ms-linear-gradient(left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0));
background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0));
}

.panel-login input[type="text"],
.panel-login input[type="email"],
.panel-login input[type="password"] {
height: 45px;
border: 1px solid #ddd;
font-size: 16px;
-webkit-transition: all 0.1s linear;
-moz-transition: all 0.1s linear;
transition: all 0.1s linear;
}

.panel-login input:hover,
.panel-login input:focus {
outline: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
border-color: #ccc;
}

.btn-login {
background-color: #59B2E0;
outline: none;
color: #fff;
font-size: 14px;
height: auto;
font-weight: normal;
padding: 14px 0;
text-transform: uppercase;
border-color: #59B2E6;
}

.btn-login:hover,
.btn-login:focus {
color: #fff;
background-color: #53A3CD;
border-color: #53A3CD;
}

.forgot-password {
text-decoration: underline;
color: #888;
}

.forgot-password:hover,
.forgot-password:focus {
text-decoration: underline;
color: #666;
}

.btn-register {
background-color: #1CB94E;
outline: none;
color: #fff;
font-size: 14px;
height: auto;
font-weight: normal;
padding: 14px 0;
text-transform: uppercase;
border-color: #1CB94A;
}

.btn-register:hover,
.btn-register:focus {
color: #fff;
background-color: #1CA347;
border-color: #1CA347;
}
@@ -0,0 +1,18 @@
$(function () {

$('#login-form-link').click(function (e) {
$("#login-form").delay(100).fadeIn(100);
$("#register-form").fadeOut(100);
$('#register-form-link').removeClass('active');
$(this).addClass('active');
e.preventDefault();
});
$('#register-form-link').click(function (e) {
$("#register-form").delay(100).fadeIn(100);
$("#login-form").fadeOut(100);
$('#login-form-link').removeClass('active');
$(this).addClass('active');
e.preventDefault();
});

});
@@ -30,23 +30,83 @@
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a href="/spells">Spells</a></li>
<li><a href="/classes">Classes</a></li>
<li><a href="/spell">Spells</a></li>
<li><a href="/class">Classes</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{{if .Claims}}
<div class="dropdown">
<button class="dropbtn">{{.Claims.Username}}</button>
<div class="dropdown-content">
<a href="/user">Profile</a>
<a href="/user/spell">Spells</a>
<a href="/user/character">Characters</a>
</div>
</div>
<li>
<a href="/logout">Logout</a>
</li>
{{else}}
<li><a href="/login">Login</a></li>
{{end}}
</ul>
</div>
<!--/.nav-collapse -->
</div>
</nav>
{{template "content" .}}
<script type="text/javascript" src="/static/js/jquery-3.1.1.min.js"></script>
<script type="text/javascript" src="/static/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/static/js/bootstrap.min.js"></script> {{template "scripts" .}}
<footer class="footer">
<hr/>
<div class="container">
<p class="text-muted">Created by: Jaden Young, Grant Moe, and Jordan Hartman.</p>
</div>
</footer>
</body>
<style>
.dropbtn {
background-color: #FC756F;
color: black;
padding: 16px;
font-size: 16px;
border: none;
cursor: pointer;
}

.dropdown {
position: relative;
display: inline-block;
}

.dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
}

.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}

.dropdown-content a:hover {
background-color: #f1f1f1
}

.dropdown:hover .dropdown-content {
display: block;
}

.dropdown:hover .dropbtn {
background-color: #3e8e41;
color: white;
}
</style>

</html>
{{end}}
@@ -0,0 +1,87 @@
{{define "title"}}Create Character - Murder Hobos{{end}} {{define "content"}}
<br>
<div class="page-header">
<h1>Character Creator</h1>
</div>
<div>
<form class="form" method="POST">
<div class="form-group" name="nameEntry">
<label>Name: </label>
<input type="text" name="name"></input>
</div>
<div class="form-group" name="classSelection">
<label>Class: </label>
<select class="form-control" name="class">
<option selected disabled value="">Select Class</option>
<option value="Bard">Bard</option>
<option value="Cleric">Cleric</option>
<option value="Druid">Druid</option>
<option value="Ranger">Ranger</option>
<option value="Sorcerer">Sorcerer</option>
<option value="Warlock">Warlock</option>
<option value="Wizard">Wizard</option>
<option value="Fighter">Fighter</option>
<option value="Pogue">Rogue</option>
</select>
</div>
<div class="form-group" name="LevelSelection">
<label>Level: </label>
<select class="form-control" name="level">
<option selected disabled value="">Select Level</option>
<option value="1">Level 1</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
<option value="4">Level 4</option>
<option value="5">Level 5</option>
<option value="6">Level 6</option>
<option value="7">Level 7</option>
<option value="8">Level 8</option>
<option value="9">Level 9</option>
<option value="10">Level 10</option>
<option value="11">Level 11</option>
<option value="12">Level 12</option>
<option value="13">Level 13</option>
<option value="14">Level 14</option>
<option value="15">Level 15</option>
<option value="16">Level 16</option>
<option value="17">Level 17</option>
<option value="18">Level 18</option>
<option value="19">Level 19</option>
<option value="20">Level 20</option>
</select>
</div>
<div class="form-group" name="raceEntry">
<label>Race: </label>
<input type="text" name="race"></input>
</div>
<div class="form-group" name="abilityModifier">
<label>Ability Modifier: </label>
<select class="form-control" name="abilityMod">
<option selected disabled value="">Select Ability Modifier</option>
<option value="-3">-3</option>
<option value="-2">-2</option>
<option value="-1">-1</option>
<option value="0">0</option>
<option value="1">+1</option>
<option value="2">+2</option>
<option value="3">+3</option>
<option value="4">+4</option>
<option value="5">+5</option>
</select>
</div>
<div class="form-group" name="profieBonus">
<label>Proficiency Bonus: </label>
<select class="form-control" name="profBonus">
<option selected disabled value="">Select Proficiency Bonus</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</div>
<input class="btn btn-primary" type="submit" value="Create Character"></input>
</form>
</div>
{{end}} {{define "scripts"}}{{end}}
@@ -0,0 +1,19 @@
{{define "title"}}{{.Character.Name}} - Murder Hobos{{end}} {{define "content"}}
<br>
<div class="container">
<div class="page-header">
<h1>The amazing <em><strong>{{.Character.Name}}</strong></em></h1>
</div>
<div class="row">
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-8">
<div class="list-type">
<ul>
<li><strong>Race: </strong>{{.Character.Race}}</li>
<li><strong>Spell Ability Modifier: </strong>{{.Character.SpellAbilityModifier}}</li>
<li><strong>Proficiency Bonus: </strong>{{.Character.ProficienyBonus}}</li>
</ul>
</div>
</div>
</div>
</div>
{{end}}
@@ -0,0 +1,24 @@
{{define "title"}}Murder Hobos{{end}} {{define "content"}}
<br>
<div class="container">
<div class="page-header">
<h1>Here are all of your characters.</h1>
</div>
<div class="row">
<div class="col-md-6">
<ul id="Character_List">
{{if .Character}} {{range .Character}}
<li>
<a class="Character" Tag="Character" href="/character/{{ .Name }}"> <strong>{{.Name}}</strong></a>
</li>
{{end}} {{else}}
<p>No results found!</p>
{{end}}
</ul>
</div>
<div class="col-md-6">
<a class="btn btn-success" href="/user/character/new">New</a>
</div>
</div>
</div>
{{end}}{{define "scripts"}}{{end}}
@@ -3,13 +3,11 @@
<div class="container">
<div class="page-header">
<h1>Lookie here we got classes.</h1>
<input type="text" id="mySearch" name="search" onkeyup="Sort()" placeholder="Search Classes..." title="Type in a name">
<button id="searchButton" onclick="Sort()">Search</button>
</div>
<div class="row">
<div class="col-lg-8 col-md-8 col-sm-10 col-xs-10 list-type">
<ul id="Class_List">
{{if .}} {{range .}}
{{if .Classes}} {{range .Classes}}
<li><a class="Classes" Tag="Class" href="/class/{{ .Name }}"> {{.Name}}</a></li>
{{end}} {{else}}
<p>No results found!</p>
@@ -18,4 +16,4 @@ <h1>Lookie here we got classes.</h1>
</div>
</div>
</div>
{{end}}
{{end}} {{define "scripts"}}{{end}}
@@ -5,4 +5,4 @@
<h2>{{.Message}}</h2>
</div>
</div>
{{end}}
{{end}} {{define "scripts"}}{{end}}
@@ -6,15 +6,15 @@ <h1>This is our main page</h1>
</div>
<div class="row">
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-8">
<img src="static/img/Logo1.jpg" class="img-thumbnail" alt="Main Logo" />
<img src="/static/img/Logo1.jpg" class="img-thumbnail" alt="Main Logo" />
</div>
</div>
<div class="row">
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-8">
<p class="text-center"><em>Wizardry's finest!</em></p>
<p class="text-center"><em>Azureus, Imperator of Fear</em></p>
<br/>
<p>I'm sure we'll have some cool stuff here someday.</p>
<h2>Feature Updates:</h2>
</div>
</div>
</div>
{{end}}
{{end}} {{define "scripts"}}{{end}}
@@ -0,0 +1,78 @@
{{define "title"}}Login{{end}} {{define "content"}}
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-login">
<div class="panel-heading">
<div class="row">
<div class="col-xs-6">
<a href="#" class="active" id="login-form-link">Login</a>
</div>
<div class="col-xs-6">
<a href="#" id="register-form-link">Register</a>
</div>
</div>
<hr>
</div>
<div class="panel-body">
<div class="row">
<div class="col-lg-12">
{{range .Errors}}
<div class="alert alert-danger">
{{.}}
</div>
{{end}}
<form id="login-form" action="/login" method="post" role="form" style="display: block;">
<div class="form-group">
<input type="text" name="username" id="username" tabindex="1" class="form-control" placeholder="Username" value="">
</div>
<div class="form-group">
<input type="password" name="password" id="password" tabindex="2" class="form-control" placeholder="Password">
</div>
<div class="form-group text-center">
<input type="checkbox" tabindex="3" class="" name="remember" id="remember">
<label for="remember"> Remember Me</label>
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<input type="submit" name="login-submit" id="login-submit" tabindex="4" class="form-control btn btn-login" value="Log In">
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-lg-12">
<div class="text-center">
<a href="/recover" tabindex="5" class="forgot-password">Forgot Password?</a>
</div>
</div>
</div>
</div>
</form>
<form id="register-form" action="/register" method="post" role="form" style="display: none;">
<div class="form-group">
<input type="text" name="username" id="username" tabindex="1" class="form-control" placeholder="Username" value="">
</div>
<div class="form-group">
<input type="password" name="password" id="password" tabindex="2" class="form-control" placeholder="Password">
</div>
<div class="form-group">
<input type="password" name="confirm-password" id="confirm-password" tabindex="2" class="form-control" placeholder="Confirm Password">
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<input type="submit" name="register-submit" id="register-submit" tabindex="4" class="form-control btn btn-register" value="Register Now">
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<link rel="stylesheet" href="/static/css/login.css">{{end}} {{define "scripts" }}<script type="text/javascript" src="/static/js/login.js"></script>{{end}}
@@ -0,0 +1,15 @@
{{define "title"}}Murder Hobos{{end}} {{define "content"}}
<br>
<div class="container">
<div class="page-header">
<h1>Here's Your Profile, {{.Claims.Username}}</h1>
</div>
<div class="row">
<div class="col-md-6">
<p>We will have some stuff for you here in the future!</p>
<p>For now, try hovering over your name in the navigation bar to have a look at your spells and characters.
</p>
</div>
</div>
</div>
{{end}}{{define "scripts"}}{{end}}
@@ -0,0 +1,98 @@
{{define "title"}}Create Spell - Murder Hobos{{end}} {{define "content"}}
<br>
<div class="page-header">
<h1>Spell Creator</h1>
</div>
<div>
<form class="form" method="POST">
<div class="form-group" name="nameEntry">
<labela>Name: </label>
<input required type="text" name="name"></input>
</div>
<div class="form-group" name="schoolSelection">
<label>School: </label>
<select required class="form-control" name="school">
<option selected disabled value="">Select School</option>
<option value="Abjuration">Abjuration</option>
<option value="Conjuration">Conjuration</option>
<option value="Divination">Divination</option>
<option value="Enchantment">Enchantment</option>
<option value="Evocation">Evocation</option>
<option value="Illusion">Illusion</option>
<option value="Necromancy">Necromancy</option>
<option value="Transmutation">Transmutation</option>
</select>
</div>
<div class="form-group" name="LevelSelection">
<label>Level: </label>
<select required class="form-control" name="level">
<option selected disabled value="">Select Level</option>
<option value="0">Cantrip</option>
<option value="1">Level 1</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
<option value="4">Level 4</option>
<option value="5">Level 5</option>
<option value="6">Level 6</option>
<option value="7">Level 7</option>
<option value="8">Level 8</option>
<option value="9">Level 9</option>
</select>
</div>
<div class="form-group" name="castTimeEntry">
<label>Cast Time: </label>
<input required type="text" name="castTime" value="castTime"></input>
</div>
<div class="form-group" name="durationEntry">
<label>Duration: </label>
<input required type="text" name="duration" value="duration"></input>
</div>

<div class="form-group" name="components">
<label>Components: </label>
<label>Verbal</label>
<select required class="form-control" name="verbal">
<option selected disabled value="">---</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label>Somatic</label>
<select required class="form-control" name="somatic">
<option selected disabled value="">---</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label>Material</label>
<select required class="form-control" name="material">
<option selected disabled value="">---</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label>Material Description: </label>
<input type="text" name="materialDesc" value="Enter Decription..."></input>
</div>

<div class="form-group" name="spellType">
<label>Spell Requirements: </label>
<div></div>
<label>Concentration</label>
<select required class="form-control" name="concentration">
<option selected disabled value="">---</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label>Ritual</label>
<select required class="form-control" name="ritual">
<option selected disabled value="">---</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
<div class="form-group" name="spellDescript">
<label>Spell Decription: </label>
<input required type="text" name="spellDesc" value="spellDesc"></input>
</div>
<input class="btn btn-primary" type="submit" value="Create Spell"></input>
</form>
</div>
{{end}}{{define "scripts"}}{{end}}
@@ -1,7 +1,7 @@
{{define "title"}}{{.Spell.Name}} - Murder Hobos{{end}} {{define "content"}}
<br>
<div class="container">
<div class="page-header">
<div class="page-header">
<h1>{{.Spell.Name}}</h1>
</div>
<div class="row">
@@ -33,4 +33,4 @@ <h1>{{.Spell.Name}}</h1>
</div>
</div>
</div>
{{end}}
{{end}} {{define "scripts"}}{{end}}
@@ -3,24 +3,69 @@
<div class="container">
<div class="page-header">
<h1>Look at all these spells.</h1>
<input type="text" id="mySearch" name="search" placeholder="Search Spells..." title="Type in a name">
<button id="searchButton" onclick="Sort()">Search</button>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-4">
<form class="form-inline" method="GET">
<div class="form-group">
<input class="form-control" type="text" id="spellSearch" name="name" placeholder="Spell name..." title="Search for spells by name"></input>
<input class="btn btn-primary" type="submit" id="searchButton" value="Search"></input>
</div>
</form>
<form class="form-inline" method="GET">
<div class="form-group">
<select class="form-control" name="school">
<option selected disabled value="">School</option>
<option value="Abjuration">Abjuration</option>
<option value="Conjuration">Conjuration</option>
<option value="Divination">Divination</option>
<option value="Enchantment">Enchantment</option>
<option value="Evocation">Evocation</option>
<option value="Illusion">Illusion</option>
<option value="Necromancy">Necromancy</option>
<option value="Transmutation">Transmutation</option>
</select>
</div>
<div class="form-group">
<select class="form-control" name="level">
<option selected disabled value="">Level</option>
<option value="0">Cantrip</option>
<option value="1">Level 1</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
<option value="4">Level 4</option>
<option value="5">Level 5</option>
<option value="6">Level 6</option>
<option value="7">Level 7</option>
<option value="8">Level 8</option>
<option value="9">Level 9</option>
</select>
</div>
<input class="btn btn-primary" type="submit" value="Filter"></input>
</form>
</div>
{{with .Claims}}
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-4">
<a href="/user/spell/new" class="btn btn-success">New</a>
</div>
{{end}}
</div>
<br>
<div class="table-responsive">
<table class="table">
<thread>
<tr>
<th>Name</th>
<th>Spell Level</th>
<th>School</th>
</tr>
</thread>
<tbody id="Spell_List">
{{if .}} {{range .}}
<tr>
<td><a class="Spells" Tag="Spell" href="/spell/{{ .Name }}">{{.Name}}</td>
<td>{{.Level}}</td>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>School</th>
<th>Level</th>
</tr>
</thead>
<tbody id="Spell_List">
{{if .Spells}} {{range .Spells}}
<tr>
<td><a class="Spells" Tag="Spell" href="/spell/{{ .Name }}">{{.Name}}</td>
<td>{{.School}}</td>
<td>{{.LevelStr}}</td>
</tr>
{{end}} {{else}}
<tr>
@@ -31,6 +76,7 @@ <h1>Look at all these spells.</h1>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
</div>
{{end}}
{{end}}{{define "scripts"}}{{end}}
@@ -0,0 +1,82 @@
{{define "title"}}Murder Hobos{{end}} {{define "content"}}
<br>
<div class="container">
<div class="page-header">
<h1>Look at all these spells.</h1>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-4">
<form class="form-inline" method="GET">
<div class="form-group">
<input class="form-control" type="text" id="spellSearch" name="name" placeholder="Spell name..." title="Search for spells by name"></input>
<input class="btn btn-primary" type="submit" id="searchButton" value="Search"></input>
</div>
</form>
<form class="form-inline" method="GET">
<div class="form-group">
<select class="form-control" name="school">
<option selected disabled value="">School</option>
<option value="Abjuration">Abjuration</option>
<option value="Conjuration">Conjuration</option>
<option value="Divination">Divination</option>
<option value="Enchantment">Enchantment</option>
<option value="Evocation">Evocation</option>
<option value="Illusion">Illusion</option>
<option value="Necromancy">Necromancy</option>
<option value="Transmutation">Transmutation</option>
</select>
</div>
<div class="form-group">
<select class="form-control" name="level">
<option selected disabled value="">Level</option>
<option value="0">Cantrip</option>
<option value="1">Level 1</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
<option value="4">Level 4</option>
<option value="5">Level 5</option>
<option value="6">Level 6</option>
<option value="7">Level 7</option>
<option value="8">Level 8</option>
<option value="9">Level 9</option>
</select>
</div>
<input class="btn btn-primary" type="submit" value="Filter"></input>
</form>
</div>
{{with .Claims}}
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-4">
<a href="/user/spell/new" class="btn btn-success">New</a>
</div>
{{end}}
</div>
<br>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>School</th>
<th>Level</th>
</tr>
</thead>
<tbody id="Spell_List">
{{if .Spells}} {{range .Spells}}
<tr>
<td><a class="Spells" Tag="Spell" href="/user/spell/{{ .Name }}">{{.Name}}</td>
<td>{{.School}}</td>
<td>{{.LevelStr}}</td>
</tr>
{{end}} {{else}}
<tr>
<th>Nothing</th>
<th>Here</th>
<th>At The moment</th>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{end}}{{define "scripts"}}{{end}}
@@ -12,6 +12,15 @@ func ToNullString(s string) sql.NullString {
return sql.NullString{String: s, Valid: s != ""}
}

// ToNullInt64 converts an int to an sql.NullInt64
func ToNullInt64(i interface{}) sql.NullInt64 {
if j, ok := i.(int64); ok {
return sql.NullInt64{Int64: j, Valid: true}
}
return sql.NullInt64{Int64: 0, Valid: false}

}

// CapitalizeAtIndex capitalizes a single char from a string at specified index
// If an error is encountered (normally index being out of range),
// ok will be set to false and the original string returned unaltered