//returnData is a low level func that actually sends the data back to the client //this sends the response //ok is true if the message is "success", false if it is "error" //resCode is an http response code func returnData(ok bool, msgType string, msgData interface{}, resCode int, w http.ResponseWriter) { //build data to return as json o := returnObj{ Ok: ok, MsgType: msgType, MsgData: msgData, Datetime: timestamps.ISO8601(), } //set content type //since we only want the data to be interpreted as json since that is the only type of data we send back w.Header().Set("Content-Type", "application/json; charset=UTF-8") //set response code w.WriteHeader(resCode) //return json to client output, _ := json.Marshal(o) w.Write(output) return }
//Add saves a new user to the app func Add(w http.ResponseWriter, r *http.Request) { //get form values username := r.FormValue("username") password1 := r.FormValue("password1") password2 := r.FormValue("password2") addCards, _ := strconv.ParseBool(r.FormValue("addCards")) removeCards, _ := strconv.ParseBool(r.FormValue("removeCards")) chargeCards, _ := strconv.ParseBool(r.FormValue("chargeCards")) viewReports, _ := strconv.ParseBool(r.FormValue("reports")) isAdmin, _ := strconv.ParseBool(r.FormValue("admin")) isActive, _ := strconv.ParseBool(r.FormValue("active")) //check if this user already exists c := appengine.NewContext(r) _, _, err := exists(c, username) if err == nil { //user already exists //notify client output.Error(ErrUserAlreadyExists, "This username already exists. Please choose a different username.", w, r) return } //make sure passwords match if doStringsMatch(password1, password2) == false { output.Error(ErrPasswordsDoNotMatch, "The passwords you provided to not match.", w, r) return } //make sure password is long enough if len(password1) < minPwdLength { output.Error(ErrPasswordTooShort, "The password you provided is too short. It must be at least "+strconv.FormatInt(minPwdLength, 10)+" characters.", w, r) return } //hash the password hashedPwd := pwds.Create(password1) //create the user u := User{ Username: username, Password: hashedPwd, AddCards: addCards, RemoveCards: removeCards, ChargeCards: chargeCards, ViewReports: viewReports, Administrator: isAdmin, Active: isActive, Created: timestamps.ISO8601(), } //save to datastore incompleteKey := createNewUserKey(c) _, err = saveUser(c, incompleteKey, u) if err != nil { fmt.Fprint(w, err) return } //clear list of users saved in memcache since a new user was added memcacheutils.Delete(c, listOfUsersKey) //respond to client with success message output.Success("addNewUser", nil, w) return }
//Charge charges a credit card func Charge(w http.ResponseWriter, r *http.Request) { //get form values datastoreId := r.FormValue("datastoreId") customerName := r.FormValue("customerName") amount := r.FormValue("amount") invoice := r.FormValue("invoice") poNum := r.FormValue("po") //validation if len(datastoreId) == 0 { output.Error(ErrMissingInput, "A customer ID should have been submitted automatically but was not. Please contact an administrator.", w, r) return } if len(amount) == 0 { output.Error(ErrMissingInput, "No amount was provided. You cannot charge a card nothing!", w, r) return } //get amount as cents amountCents, err := getAmountAsIntCents(amount) if err != nil { output.Error(err, "An error occured while converting the amount to charge into cents. Please try again or contact an administrator.", w, r) return } //check if amount is greater than the minimum charge //min charge may be greater than 0 because of transactions costs //for example, stripe takes 30 cents...it does not make sense to charge a card for < 30 cents if amountCents < minCharge { output.Error(ErrChargeAmountTooLow, "You must charge at least "+strconv.FormatInt(minCharge, 10)+" cents.", w, r) return } //create context //need to adjust deadline in case stripe takes longer than 5 seconds //default timeout for a urlfetch is 5 seconds //sometimes charging a card through stripe api takes longer //calls seems to take roughly 2 seconds normally with a few near 5 seconds (old deadline) //the call might still complete via stripe but appengine will return to the gui that it failed //10 secodns is a bit over generous but covers even really strange senarios c := appengine.NewContext(r) c, _ = context.WithTimeout(c, 10*time.Second) //look up stripe customer id from datastore datastoreIdInt, _ := strconv.ParseInt(datastoreId, 10, 64) custData, err := findByDatastoreId(c, datastoreIdInt) if err != nil { output.Error(err, "An error occured while looking up the customer's Stripe information.", w, r) return } //make sure customer name matches //just another catch in case of strange errors and mismatched data if customerName != custData.CustomerName { output.Error(err, "The customer name did not match the data for the customer ID. Please log out and try again.", w, r) return } //get username of logged in user //used for tracking who processed a charge //for audits and reports session := sessionutils.Get(r) username := session.Values["username"].(string) //init stripe sc := createAppengineStripeClient(c) //build charge object chargeParams := &stripe.ChargeParams{ Customer: custData.StripeCustomerToken, Amount: amountCents, Currency: currency, Desc: "Charge for invoice: " + invoice + ", purchase order: " + poNum + ".", Statement: formatStatementDescriptor(), } //add metadata to charge //used for reports and receipts chargeParams.AddMeta("customer_name", customerName) chargeParams.AddMeta("datastore_id", datastoreId) chargeParams.AddMeta("customer_id", custData.CustomerId) chargeParams.AddMeta("invoice_num", invoice) chargeParams.AddMeta("po_num", poNum) chargeParams.AddMeta("charged_by", username) //process the charge chg, err := sc.Charges.New(chargeParams) //handle errors //*url.Error can be thrown if urlfetch reaches timeout (request took too long to complete) //*stripe.Error is a error with the stripe api and should return a human readable error message if err != nil { errorMsg := "" switch err.(type) { default: errorMsg = "There was an error processing this charge. Please check the Report to see if this charge was successful." break case *url.Error: errorMsg = "Charging this card timed out. The charge may have succeeded anyway. Please check the Report to see if this charge was successful." break case *stripe.Error: stripeErr := err.(*stripe.Error) errorMsg = stripeErr.Msg } output.Error(ErrStripe, errorMsg, w, r) return } //charge successful //save charge to memcache //less data to get from stripe if receipt is needed //errors are ignores since if we can't save this data to memcache we can always get it from the datastore/stripe memcacheutils.Save(c, chg.ID, chg) //save count of card types //used for negotiating rates with Stripe and just extra info saveChargeDetails(c, chg) //build struct to output a success message to the client out := chargeSuccessful{ CustomerName: customerName, Cardholder: custData.Cardholder, CardExpiration: custData.CardExpiration, CardLast4: custData.CardLast4, Amount: amount, Invoice: invoice, Po: poNum, Datetime: timestamps.ISO8601(), ChargeId: chg.ID, } output.Success("cardCharged", out, w) return }
//CreateAdmin saves the initial super-admin for the app //this user is used to log in and create new users //this user is created when the app is first deployed and used // done this way b/c we don't want to set a default password/username in the code func CreateAdmin(w http.ResponseWriter, r *http.Request) { //make sure the admin user doesnt already exist err := DoesAdminExist(r) if err == nil { notificationPage(w, "panel-danger", "Error", "The admin user already exists.", "btn-default", "/", "Go Back") return } //get form values pass1 := r.FormValue("password1") pass2 := r.FormValue("password2") //make sure they match if doStringsMatch(pass1, pass2) == false { notificationPage(w, "panel-danger", "Error", "The passwords id not match.", "btn-default", "/setup/", "Try Again") return } //make sure the password is long enough if len(pass1) < minPwdLength { notificationPage(w, "panel-danger", "Error", "The password you provided is too short. It must me at least "+strconv.FormatInt(minPwdLength, 10)+" characters.", "btn-default", "/setup/", "Try Again") return } //hash the password hashedPwd := pwds.Create(pass1) //create the user u := User{ Username: adminUsername, Password: hashedPwd, AddCards: true, RemoveCards: true, ChargeCards: true, ViewReports: true, Administrator: true, Active: true, Created: timestamps.ISO8601(), } //save to datastore c := appengine.NewContext(r) incompleteKey := createNewUserKey(c) completeKey, err := saveUser(c, incompleteKey, u) if err != nil { fmt.Fprint(w, err) return } //save user to session //this is how we authenticate users that are already signed in //the user is automatically logged in as the administrator user session := sessionutils.Get(r) if session.IsNew == false { notificationPage(w, "panel-danger", "Error", "An error occured while saving the admin user. Please clear your cookies and restart your browser.", "btn-default", "/setup/", "Try Again") return } sessionutils.AddValue(session, "username", adminUsername) sessionutils.AddValue(session, "user_id", completeKey.IntID()) sessionutils.Save(session, w, r) //show user main page http.Redirect(w, r, "/main/", http.StatusFound) return }
//Add adds a new card the the app engine datastore //this is done by validating the provided inputs, sending the card token to stripe, and saving the data to the datastore //the card token was generaged client side by the stipe-js // this is done so the card number and security code is never sent to the server // the server has no way of "touching" the card number for security //when the card token is sent to stripe, stripe generates a customer token which we store and use to process payments func Add(w http.ResponseWriter, r *http.Request) { //get form values customerId := r.FormValue("customerId") //a unique key, not the datastore id or stripe customer id customerName := r.FormValue("customerName") //user provided, could be company name/client name/may be same as cardholder cardholder := r.FormValue("cardholder") //name on card as it appears cardToken := r.FormValue("cardToken") //from stripejs cardExp := r.FormValue("cardExp") //from stripejs, not from html input cardLast4 := r.FormValue("cardLast4") //from stripejs, not from html input //make sure all form values were given if len(customerName) == 0 { output.Error(ErrMissingCustomerName, "You did not provide the customer's name.", w, r) return } if len(cardholder) == 0 { output.Error(ErrMissingCustomerName, "You did not provide the cardholer's name.", w, r) return } if len(cardToken) == 0 { output.Error(ErrMissingCardToken, "A serious error occured; the card token is missing. Please refresh the page and try again.", w, r) return } if len(cardExp) == 0 { output.Error(ErrMissingExpiration, "The card's expiration date is missing from Stripe. Please refresh the page and try again.", w, r) return } if len(cardLast4) == 0 { output.Error(ErrMissingLast4, "The card's last four digits are missing from Stripe. Please refresh the page and try again.", w, r) return } //init context c := appengine.NewContext(r) //if customerId was given, make sure it is unique //this id should be unique in the user's company's crm //the customerId is used to autofill the charge card panel when performing the api-like semi-automated charges if len(customerId) != 0 { _, err := FindByCustId(c, customerId) if err == nil { //customer already exists output.Error(ErrCustIdAlreadyExists, "This customer ID is already in use. Please double check your records or remove the customer with this customer ID first.", w, r) return } else if err != ErrCustomerNotFound { output.Error(err, "An error occured while verifying this customer ID does not already exist. Please try again or leave the customer ID blank.", w, r) return } } //init stripe sc := createAppengineStripeClient(c) //create the customer on stripe //assigns the card via the cardToken to this customer //this card is used when making charges to this customer custParams := &stripe.CustomerParams{Desc: customerName} custParams.SetSource(cardToken) cust, err := sc.Customers.New(custParams) if err != nil { stripeErr := err.(*stripe.Error) stripeErrMsg := stripeErr.Msg output.Error(ErrStripe, stripeErrMsg, w, r) return } //get username of logged in user //used for tracking who added a card, just for diagnostics session := sessionutils.Get(r) username := session.Values["username"].(string) //save customer & card data to datastore newCustKey := createNewCustomerKey(c) newCustomer := CustomerDatastore{ CustomerId: customerId, CustomerName: customerName, Cardholder: cardholder, CardExpiration: cardExp, CardLast4: cardLast4, StripeCustomerToken: cust.ID, DatetimeCreated: timestamps.ISO8601(), AddedByUser: username, } _, err = save(c, newCustKey, newCustomer) if err != nil { output.Error(err, "There was an error while saving this customer. Please try again.", w, r) return } //customer saved //return to client output.Success("createCustomer", nil, w) //resave list of cards in memcache //since a card was added, memcache is stale //clients will retreive new list when refreshing page/app memcacheutils.Delete(c, listOfCardsKey) return }