controllers/user.go
package controllers
import (
"errors"
"net/http"
"net/mail"
"encoding/json"
"github.com/coduno/api/model"
"github.com/coduno/api/util"
"github.com/coduno/api/util/passenger"
"github.com/coduno/api/util/password"
"github.com/gorilla/mux"
"golang.org/x/net/context"
"google.golang.org/appengine/datastore"
)
type keyedUserWithState struct {
*model.KeyedUser
State string
}
func init() {
router.Handle("/user/company", ContextHandlerFunc(GetCompanyByUser))
router.Handle("/user", ContextHandlerFunc(WhoAmI))
router.Handle("/users", ContextHandlerFunc(User))
router.Handle("/users/{key}", ContextHandlerFunc(GetUser))
router.Handle("/users/{key}/profile", ContextHandlerFunc(GetProfileForUser))
router.Handle("/whoami", ContextHandlerFunc(WhoAmI))
}
func User(ctx context.Context, w http.ResponseWriter, r *http.Request) (status int, err error) {
switch r.Method {
case "POST":
return createUser(ctx, w, r)
case "GET":
p, ok := passenger.FromContext(ctx)
if !ok {
return http.StatusUnauthorized, nil
}
return getUsers(p, ctx, w, r)
default:
return http.StatusMethodNotAllowed, nil
}
}
func GetUser(ctx context.Context, w http.ResponseWriter, r *http.Request) (status int, err error) {
_, ok := passenger.FromContext(ctx)
if !ok {
return http.StatusUnauthorized, nil
}
var userKey *datastore.Key
if userKey, err = datastore.DecodeKey(mux.Vars(r)["key"]); err != nil {
return http.StatusInternalServerError, err
}
var user model.User
if err = datastore.Get(ctx, userKey, &user); err != nil {
return http.StatusInternalServerError, err
}
json.NewEncoder(w).Encode(user.Key(userKey))
return
}
func createUser(ctx context.Context, w http.ResponseWriter, r *http.Request) (status int, err error) {
var body = struct {
Address, Nick, Password, Company string
}{}
if err = json.NewDecoder(r.Body).Decode(&body); err != nil {
return http.StatusBadRequest, err
}
var companyKey *datastore.Key
if body.Company != "" {
companyKey, err = datastore.DecodeKey(body.Company)
if err != nil {
return http.StatusBadRequest, err
}
}
if err = util.CheckNick(body.Nick); err != nil {
return http.StatusBadRequest, err
}
var address *mail.Address
if address, err = mail.ParseAddress(body.Address); err != nil {
return http.StatusBadRequest, err
}
// Duplicate length check. If we move this after the conflict checks,
// we could end up returning with a short password after querying Datastore.
// The other way round, we would have to hash the password, and then throw it
// away because of possible conflicts.
pw := []byte(body.Password)
if err = password.CheckLen(pw); err != nil {
return http.StatusBadRequest, err
}
var emailConflict bool
if emailConflict, err = alreadyExists(ctx, "Address", address.Address); err != nil {
return http.StatusInternalServerError, err
}
if emailConflict {
return http.StatusConflict, errors.New("duplicate e-mail address")
}
var nickConflict bool
if nickConflict, err = alreadyExists(ctx, "Nick", body.Nick); err != nil {
return http.StatusInternalServerError, err
}
if nickConflict {
return http.StatusConflict, errors.New("duplicate nick")
}
var hashedPassword []byte
if hashedPassword, err = password.Hash(pw); err != nil {
return http.StatusInternalServerError, err
}
user := model.User{
Address: *address,
Nick: body.Nick,
HashedPassword: hashedPassword,
}
var key *datastore.Key
if companyKey == nil {
key, err = user.Put(ctx, nil)
} else {
// Bind user to company for eternity.
key, err = user.PutWithParent(ctx, companyKey)
}
if err != nil {
return http.StatusInternalServerError, err
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user.Key(key))
return http.StatusOK, nil
}
func getUsers(p *passenger.Passenger, ctx context.Context, w http.ResponseWriter, r *http.Request) (status int, err error) {
var u model.User
if err = datastore.Get(ctx, p.User, &u); err != nil {
return http.StatusInternalServerError, nil
}
if u.Company == nil {
return http.StatusUnauthorized, nil
}
var invitations model.Invitations
_, err = model.NewQueryForInvitation().Ancestor(u.Company).GetAll(ctx, &invitations)
if err != nil {
return http.StatusInternalServerError, err
}
cckeys, err := model.NewQueryForChallenge().Ancestor(u.Company).KeysOnly().GetAll(ctx, nil)
if err != nil {
return http.StatusInternalServerError, err
}
var resultKeys []*datastore.Key
for _, val := range cckeys {
rkeys, err := model.NewQueryForResult().Filter("Challenge =", val).KeysOnly().GetAll(ctx, nil)
if err != nil {
return http.StatusInternalServerError, err
}
resultKeys = append(resultKeys, rkeys...)
}
var users model.Users
keys, err := model.NewQueryForUser().GetAll(ctx, &users)
if err != nil {
return http.StatusInternalServerError, err
}
finishedUsers := make([]*datastore.Key, len(resultKeys))
for i := range resultKeys {
finishedUsers[i] = resultKeys[i].Parent().Parent()
}
// TODO(victorbalan): Don`t load invited users that have an result.
invitedUsers := make([]*datastore.Key, len(invitations))
for i, val := range invitations {
invitedUsers[i] = val.User
}
mappedStates := make(map[string]string)
for _, val := range invitedUsers {
mappedStates[val.Encode()] = "invited"
}
for _, val := range finishedUsers {
mappedStates[val.Encode()] = "coding"
}
usersWithState := make([]keyedUserWithState, len(users))
for i := range users {
usersWithState[i] = keyedUserWithState{
KeyedUser: &model.KeyedUser{
User: &users[i], Key: keys[i],
},
State: mappedStates[keys[i].Encode()],
}
}
json.NewEncoder(w).Encode(usersWithState)
return http.StatusOK, nil
}
func alreadyExists(ctx context.Context, property, value string) (exists bool, err error) {
k, err := model.NewQueryForUser().
KeysOnly().
Limit(1).
Filter(property+"=", value).
GetAll(ctx, nil)
if err != nil {
return false, err
}
return len(k) == 1, nil
}
// GetUsersByCompany queries the user accounts belonging to a company.
func GetUsersByCompany(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != "GET" {
return http.StatusMethodNotAllowed, nil
}
key, err := datastore.DecodeKey(r.URL.Query()["result"][0])
if err != nil {
return http.StatusBadRequest, err
}
var users model.Users
keys, err := model.NewQueryForUser().
Ancestor(key).
GetAll(ctx, &users)
if err != nil {
return http.StatusInternalServerError, err
}
json.NewEncoder(w).Encode(users.Key(keys))
return http.StatusOK, nil
}
func GetCompanyByUser(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != "GET" {
return http.StatusMethodNotAllowed, nil
}
p, ok := passenger.FromContext(ctx)
if !ok {
return http.StatusUnauthorized, nil
}
var u model.User
if err := datastore.Get(ctx, p.User, &u); err != nil {
return http.StatusInternalServerError, nil
}
if u.Company == nil {
return http.StatusUnauthorized, nil
}
// The account is associated with a company, so we return it.
var company model.Company
if err := datastore.Get(ctx, u.Company, &company); err != nil {
return http.StatusInternalServerError, err
}
json.NewEncoder(w).Encode(company.Key(u.Company))
return http.StatusOK, nil
}
func WhoAmI(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != "GET" {
return http.StatusMethodNotAllowed, nil
}
p, ok := passenger.FromContext(ctx)
if !ok {
return http.StatusUnauthorized, nil
}
var user model.User
if err := datastore.Get(ctx, p.User, &user); err != nil {
return http.StatusInternalServerError, err
}
json.NewEncoder(w).Encode(user.Key(p.User))
return http.StatusOK, nil
}