diff --git a/attendees.go b/attendees.go index e592183..55000b5 100644 --- a/attendees.go +++ b/attendees.go @@ -11,7 +11,7 @@ import ( type Attendee struct { AttendeeId int64 - UserId int64 `json:"-"` + GroupId int64 `json:"-"` Name string Date time.Time `json:"-"` } @@ -66,10 +66,10 @@ func (aeu AttendeeInUseError) Error() string { return "Attendee in use (by suggestion)" } -func GetAttendees(userid int64, date time.Time) (*[]*Attendee, error) { +func GetAttendees(groupid int64, date time.Time) (*[]*Attendee, error) { var attendees []*Attendee - _, err := DB.Select(&attendees, "SELECT * from attendees WHERE UserId=? AND Date=?", userid, date) + _, err := DB.Select(&attendees, "SELECT * from attendees WHERE GroupId=? AND Date=?", groupid, date) if err != nil { return nil, err } @@ -105,7 +105,7 @@ func InsertAttendee(a *Attendee) error { return err } - existing, err := transaction.SelectInt("SELECT count(*) from attendees where UserId=? AND Name=? AND Date=?", a.UserId, a.Name, a.Date) + existing, err := transaction.SelectInt("SELECT count(*) from attendees where GroupId=? AND Name=? AND Date=?", a.GroupId, a.Name, a.Date) if err != nil { transaction.Rollback() return err @@ -130,10 +130,10 @@ func InsertAttendee(a *Attendee) error { return nil } -func GetAttendee(attendeeid int64, userid int64, date time.Time) (*Attendee, error) { +func GetAttendee(attendeeid int64, groupid int64, date time.Time) (*Attendee, error) { var a Attendee - err := DB.SelectOne(&a, "SELECT * from attendees where UserId=? AND AttendeeId=? AND Date=?", userid, attendeeid, date) + err := DB.SelectOne(&a, "SELECT * from attendees where GroupId=? AND AttendeeId=? AND Date=?", groupid, attendeeid, date) if err != nil { return nil, err } @@ -147,7 +147,7 @@ func DeleteAttendee(a *Attendee) error { } // Ensure attendee isn't used in any suggestions - suggestions, err := GetAttendeesSuggestions(transaction, a.UserId, a.Date, a.AttendeeId) + suggestions, err := GetAttendeesSuggestions(transaction, a.GroupId, a.Date, a.AttendeeId) if err != nil { transaction.Rollback() return err @@ -199,7 +199,7 @@ func AttendeeHandler(w http.ResponseWriter, r *http.Request) { return } attendee.AttendeeId = -1 - attendee.UserId = user.UserId + attendee.GroupId = user.GroupId attendee.Date = today err = InsertAttendee(&attendee) @@ -223,7 +223,7 @@ func AttendeeHandler(w http.ResponseWriter, r *http.Request) { } else if r.Method == "GET" { var al AttendeeList - attendees, err := GetAttendees(user.UserId, today) + attendees, err := GetAttendees(user.GroupId, today) if err != nil { WriteError(w, 999 /*Internal Error*/) log.Print(err) @@ -244,7 +244,7 @@ func AttendeeHandler(w http.ResponseWriter, r *http.Request) { return } - attendee, err := GetAttendee(attendeeid, user.UserId, today) + attendee, err := GetAttendee(attendeeid, user.GroupId, today) if err != nil { WriteError(w, 3 /*Invalid Request*/) return diff --git a/db.go b/db.go index 4f1afa9..c631c63 100644 --- a/db.go +++ b/db.go @@ -17,6 +17,7 @@ func initDB() *gorp.DbMap { dbmap := &gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} dbmap.AddTableWithName(User{}, "users").SetKeys(true, "UserId") + dbmap.AddTableWithName(Group{}, "groups").SetKeys(true, "GroupId") dbmap.AddTableWithName(Session{}, "sessions").SetKeys(true, "SessionId") dbmap.AddTableWithName(Attendee{}, "attendees").SetKeys(true, "AttendeeId") dbmap.AddTableWithName(Suggestion{}, "suggestions").SetKeys(true, "SuggestionId") diff --git a/errors.go b/errors.go index ae7c35a..91265a0 100644 --- a/errors.go +++ b/errors.go @@ -20,6 +20,7 @@ var error_codes = map[int]string{ 6: "Suggestion Exists", 7: "Attendee In Use", 8: "Suggestion Not Last", + 9: "Group Exists", 999: "Internal Error", } diff --git a/groups.go b/groups.go new file mode 100644 index 0000000..e85e6ad --- /dev/null +++ b/groups.go @@ -0,0 +1,243 @@ +package main + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" +) + +type Group struct { + GroupId int64 + Name string + Password string `db:"-"` + PasswordHash string `json:"-"` +} + +type GroupList struct { + Groups *[]*Group `json:"groups"` +} + +type GroupExistsError struct{} + +func (gee GroupExistsError) Error() string { + return "Group exists" +} + +func (g *Group) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(g) +} + +func (g *Group) Read(json_str string) error { + dec := json.NewDecoder(strings.NewReader(json_str)) + return dec.Decode(g) +} + +func (gl *GroupList) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(gl) +} + +func (g *Group) HashPassword() { + password_hasher := sha256.New() + io.WriteString(password_hasher, g.Password) + g.PasswordHash = fmt.Sprintf("%x", password_hasher.Sum(nil)) + g.Password = "" +} + +func GetGroup(groupid int64) (*Group, error) { + var g Group + + err := DB.SelectOne(&g, "SELECT * from groups where GroupId=?", groupid) + if err != nil { + return nil, err + } + return &g, nil +} + +func ValidGroup(groupid int64, password string) (bool, error) { + var test Group + test.Password = password + test.HashPassword() + + group, err := GetGroup(groupid) + if err != nil { + return false, err + } else { + return group.PasswordHash == test.PasswordHash, nil + } +} + +func GetGroups() (*[]*Group, error) { + var groups []*Group + + _, err := DB.Select(&groups, "SELECT * from groups") + if err != nil { + return nil, err + } + return &groups, nil +} + +func GetGroupByName(groupname string) (*Group, error) { + var g Group + + err := DB.SelectOne(&g, "SELECT * from groups where Name=?", groupname) + if err != nil { + return nil, err + } + return &g, nil +} + +func InsertGroup(g *Group) error { + transaction, err := DB.Begin() + if err != nil { + return err + } + + existing, err := transaction.SelectInt("SELECT count(*) from groups where Name=?", g.Name) + if err != nil { + transaction.Rollback() + return err + } + if existing > 0 { + transaction.Rollback() + return GroupExistsError{} + } + + err = transaction.Insert(g) + if err != nil { + transaction.Rollback() + return err + } + + err = transaction.Commit() + if err != nil { + transaction.Rollback() + return err + } + + return nil +} + +func GroupHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + group_json := r.PostFormValue("group") + if group_json == "" { + WriteError(w, 3 /*Invalid Request*/) + return + } + + var group Group + err := group.Read(group_json) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + group.GroupId = -1 + group.HashPassword() + + err = InsertGroup(&group) + if err != nil { + if _, ok := err.(GroupExistsError); ok { + WriteError(w, 9 /*Group Exists*/) + } else { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + } + return + } + + w.WriteHeader(201 /*Created*/) + err = group.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else if r.Method == "GET" { + var gl GroupList + + groups, err := GetGroups() + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + gl.Groups = groups + err = (&gl).Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else if r.Method == "PUT" { + user, err := GetUserFromSession(r) + if err != nil { + WriteError(w, 1 /*Not Signed In*/) + return + } + + groupid, err := GetURLID(r.URL.Path) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + if groupid != user.GroupId { + WriteError(w, 2 /*Unauthorized Access*/) + return + } + + group_json := r.PostFormValue("group") + if group_json == "" { + WriteError(w, 3 /*Invalid Request*/) + return + } + + group, err := GetGroup(groupid) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + // Save old PWHash in case the new password is bogus + old_pwhash := group.PasswordHash + + err = group.Read(group_json) + if err != nil || group.GroupId != groupid { + WriteError(w, 3 /*Invalid Request*/) + return + } + + // If the user didn't create a new password for the group, keep the old one + if group.Password != BogusPassword { + group.HashPassword() + } else { + group.Password = "" + group.PasswordHash = old_pwhash + } + + count, err := DB.Update(group) + if count != 1 || err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + err = group.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else { /* DELETE not implemented*/ + WriteError(w, 3 /*Invalid Request*/) + return + } +} diff --git a/js/actions/GroupActions.js b/js/actions/GroupActions.js new file mode 100644 index 0000000..56770bc --- /dev/null +++ b/js/actions/GroupActions.js @@ -0,0 +1,135 @@ +var GroupConstants = require('../constants/GroupConstants'); + +var ErrorActions = require('./ErrorActions'); + +var models = require('../models'); +var Group = models.Group; +var Error = models.Error; + +function createGroup(group) { + return { + type: GroupConstants.CREATE_GROUP, + group: group + } +} + +function groupCreated(group) { + return { + type: GroupConstants.GROUP_CREATED, + group: group + } +} + +function fetchGroups() { + return { + type: GroupConstants.FETCH_GROUPS + } +} + +function groupsFetched(groups) { + return { + type: GroupConstants.GROUPS_FETCHED, + groups: groups + } +} + +function updateGroup(group) { + return { + type: GroupConstants.UPDATE_GROUP, + group: group + } +} + +function groupUpdated(group) { + return { + type: GroupConstants.GROUP_UPDATED, + group: group + } +} + +function fetchAll() { + return function (dispatch) { + dispatch(fetchGroups()); + + $.ajax({ + type: "GET", + dataType: "json", + url: "group/", + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + dispatch(groupsFetched(data.groups.map(function(json) { + var g = new Group(); + g.fromJSON(json); + return g; + }))); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +function create(group) { + return function(dispatch) { + dispatch(createGroup(group)); + $.ajax({ + type: "POST", + dataType: "json", + url: "group/", + data: {group: group.toJSON()}, + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + var u = new Group(); + u.fromJSON(data); + dispatch(groupCreated(u)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +function update(group) { + return function (dispatch) { + dispatch(updateGroup()); + + $.ajax({ + type: "PUT", + dataType: "json", + url: "group/"+group.GroupId+"/", + data: {group: group.toJSON()}, + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + var g = new Group(); + g.fromJSON(data); + dispatch(groupUpdated(u)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +module.exports = { + fetchAll: fetchAll, + create: create, + update: update +}; diff --git a/js/components/LunchApp.js b/js/components/LunchApp.js index f4cc2e9..cdb06a4 100644 --- a/js/components/LunchApp.js +++ b/js/components/LunchApp.js @@ -10,18 +10,21 @@ var TopBarContainer = require('../containers/TopBarContainer'); var RecordLunchContainer = require('../containers/RecordLunchContainer'); var AccountSettingsModalContainer = require('../containers/AccountSettingsModalContainer'); var LunchStatsContainer = require('../containers/LunchStatsContainer'); -var NewUserModalContainer = require('../containers//NewUserModalContainer'); +var NewUserModalContainer = require('../containers/NewUserModalContainer'); +var NewGroupModalContainer = require('../containers/NewGroupModalContainer'); module.exports = React.createClass({ displayName: "LunchApp", getInitialState: function() { return { showNewUserModal: false, + showNewGroupModal: false, showAccountSettingsModal: false }; }, componentDidMount: function() { this.props.tryResumingSession(); + this.props.fetchGroups(); }, handleAccountSettings: function() { this.setState({showAccountSettingsModal: true}); @@ -41,6 +44,15 @@ module.exports = React.createClass({ handleNewUserCanceled: function() { this.setState({showNewUserModal: false}); }, + handleCreateNewGroup: function() { + this.setState({showNewGroupModal: true}); + }, + handleNewGroupCreated: function() { + this.setState({showNewGroupModal: false}); + }, + handleNewGroupCanceled: function() { + this.setState({showNewGroupModal: false}); + }, render: function() { var mainContent; if (this.props.user.isUser()) @@ -65,12 +77,17 @@ module.exports = React.createClass({