pyc-website

main website for pyc inc.

git clone https://9o.is/git/pyc-website.git

commit adba2f571723c1d4584efdf9c6806746ec8eb747
parent b8552f29bd1cf7a334a2ad5a494c9c6eaec8a925
Author: Jul <jul@9o.is>
Date:   Mon, 14 Jul 2014 17:25:35 -0400

ATM owner can now verify user purchase limit with one-time password

Diffstat:
Msrc/main/scala/inc/pyc/config/Site.scala | 5+++++
Msrc/main/scala/inc/pyc/lib/HtmlEmail.scala | 6+++---
Asrc/main/scala/inc/pyc/lib/StringUtils.scala | 17+++++++++++++++++
Msrc/main/scala/inc/pyc/model/Atm.scala | 21++++++++++++++++++++-
Msrc/main/scala/inc/pyc/model/User.scala | 4++++
Msrc/main/scala/inc/pyc/model/field/PurchaseLimitField.scala | 6++++++
Msrc/main/scala/inc/pyc/snippet/AdminSnip.scala | 37+++++++++++++++++++++++++++++++++++--
Msrc/main/scala/inc/pyc/snippet/UserSnip.scala | 26+++++++++++++++++++++++---
Msrc/main/webapp/app/App.js | 11+++++++++++
Msrc/main/webapp/app/test/App.spec.js | 18+-----------------
Msrc/main/webapp/less/custom.less | 18+++++++++++++++++-
Msrc/main/webapp/less/styles.less | 2+-
Msrc/main/webapp/settings.html | 24++++++++++++++++++++++++
Msrc/main/webapp/settings/admin/verify.html | 2+-
Asrc/main/webapp/settings/view/purchase_limits.html | 27+++++++++++++++++++++++++++
15 files changed, 195 insertions(+), 29 deletions(-)

diff --git a/src/main/scala/inc/pyc/config/Site.scala b/src/main/scala/inc/pyc/config/Site.scala @@ -70,6 +70,10 @@ object Site extends Locs { // Settings for Admins val verifyID = MenuLoc(Menu.i("Verify ID") / "settings" / "admin" / "verify" >> SettingsGroup >> HasRole("admin")) + // Settings for ATM Owners + val viewPurchaseLimits = MenuLoc(Menu.i("View Purchase Limits") / "settings" / "view" / "purchase_limits" + >> SettingsGroup >> HasRole("atm_owner")) + // ATM pages private val atmProfileParamMenu = Menu.param[Atm]( @@ -96,6 +100,7 @@ object Site extends Locs { password.menu, purchaseLimit.menu, verifyID.menu, + viewPurchaseLimits.menu, emailResetToken.menu, atmProfileParamMenu, Menu.i("Error") / "error" >> Hidden, diff --git a/src/main/scala/inc/pyc/lib/HtmlEmail.scala b/src/main/scala/inc/pyc/lib/HtmlEmail.scala @@ -47,12 +47,12 @@ object HtmlEmail { def simpleMessage( title: String, subtitle: String, - body: String): Box[NodeSeq] = { + body: NodeSeq): Box[NodeSeq] = { - create(body.take(50), title, subtitle, + create(body.text.take(80), title, subtitle, <span> <br/> - {Text(body)} + {body} <br/> </span> ) diff --git a/src/main/scala/inc/pyc/lib/StringUtils.scala b/src/main/scala/inc/pyc/lib/StringUtils.scala @@ -0,0 +1,16 @@ +package inc.pyc +package lib + +object StringUtils { + + def randomString(len: Int): String = { + val rand = new scala.util.Random(System.nanoTime) + val sb = new StringBuilder(len) + val ab = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + for (i <- 0 until len) { + sb.append(ab(rand.nextInt(ab.length))) + } + sb.toString + } + +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/model/Atm.scala b/src/main/scala/inc/pyc/model/Atm.scala @@ -5,6 +5,7 @@ import lib._ import field._ import net.liftweb._ import common.Box +import util.Helpers import json._ import JsonDSL._ import mongodb.record._ @@ -27,6 +28,7 @@ class Atm private () extends MongoRecord[Atm] with ObjectIdPk[Atm] { object website extends StringField(this, 255) object model extends StringField(this, 64) object times extends BsonRecordListField(this, TimeOpen) + object ownerObj extends ObjectIdRefField(this, User) object country extends CountryField(this) { override def defaultValue = Countries.USA @@ -129,6 +131,22 @@ object FeaturedAtm { private val name = "Coffeetime@Hellasbakery" lazy val atm: Atm = Atm.findByName(name) openOr { + + val ownerEmail = "paul@nybitcoingroup.com" + val ownerUser = "Paul_Paterakis" + val ownerFname = "Paul" + val ownerLname = "Paterakis" + + val owner: User = User.findByEmail(ownerEmail) openOr { + User.createRecord + .email(ownerEmail) + .username(ownerUser) + .fname(ownerFname) + .lname(ownerLname) + .roles("atm_owner" :: Nil) + .password(Helpers.randomString(20), true) + .save() + } val times = create("8:00", "18:00") :: @@ -147,7 +165,8 @@ object FeaturedAtm { .state("New York") .postal("12304") .phone("(518) 698-9070") - .owner("Paul Paterakis") + .owner(ownerFname + " " + ownerLname) + .ownerObj(owner.id.get) .website("http://www.coffeetimehellas.com/") .model("skyhook") .times(times) diff --git a/src/main/scala/inc/pyc/model/User.scala b/src/main/scala/inc/pyc/model/User.scala @@ -42,6 +42,10 @@ class User private () extends ProtoAuthUser[User] with ObjectIdPk[User] with USA } object phoneverified extends BooleanField(this, false) + + object verifypass extends StringField(this, 15) { + override def defaultValue = StringUtils.randomString(15) + } def whenCreated: DateTime = new DateTime(id.get.getTime) diff --git a/src/main/scala/inc/pyc/model/field/PurchaseLimitField.scala b/src/main/scala/inc/pyc/model/field/PurchaseLimitField.scala @@ -71,4 +71,10 @@ trait USAUserVerification[A <: Record[A]] { def limit1000: Boolean = purchaseLimit.get == USAPurchaseLimit.D1000 || purchaseLimit.get == USAPurchaseLimit.D3000_Pending def limit3000: Boolean = purchaseLimit.get == USAPurchaseLimit.D3000 || purchaseLimit.get == USAPurchaseLimit.Unlimited_Pending def unlimited: Boolean = purchaseLimit.get == USAPurchaseLimit.Unlimited + + def userLimitAsString = + if(unlimited) "Unlimited" + else if(limit3000) "$3,000" + else if(limit1000) "$1,000" + else "$500" } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/snippet/AdminSnip.scala b/src/main/scala/inc/pyc/snippet/AdminSnip.scala @@ -5,6 +5,7 @@ import xml._ import lib._ import model._ import model.field.USAPurchaseLimit._ +import StringUtils._ import config.Site import net.liftweb._ import http._ @@ -51,7 +52,22 @@ class VerifyId extends AdminSnip { user.update val subtitle = s"Congratulations $fname, you've been verified!" - val body = subtitle + " Your new bitcoin buy limit is " + user.purchaseLimit.get + val body = + <div> + {subtitle} + <br/> + <h2>Your new bitcoin purchase limit is {user.purchaseLimit.get}</h2> + <br/> + To purchase bitcoin from a store ATM, you may need to authenticate face-to-face + with the store manager or an authorized employee using a one-time password + available in the <a href={Site.settings.fullUrl}>settings page of your PYC account</a>. + Note, this password is not related to the bitcoin you possess; it's only a way to + prove to the store owner you can purchase up to {user.purchaseLimit.get}. + This is a requirement in only some states. + For information about the authorization process, read more about FinCen MSB requirements. + We apologize for the strict regulations and thank you for your interest in buying bitcoin. + </div> + HtmlEmail.simpleMessage(title, subtitle, body) } else { @@ -63,7 +79,7 @@ class VerifyId extends AdminSnip { val JString(reason) = model \ "reason" val subtitle = s"Sorry $fname, your verification failed!" val body = s"$subtitle $reason -- Your bitcoin buy limit remains at ${user.purchaseLimit.get}" - HtmlEmail.simpleMessage(title, subtitle, body) + HtmlEmail.simpleMessage(title, subtitle, Text(body)) } html map (sendMail( @@ -85,4 +101,21 @@ class VerifyId extends AdminSnip { ("username" -> user.username.get) ~ ("status" -> user.purchaseLimit.get.toString) ))) +} + + + +class PurchaseLimitList extends AngularSnippet with Logger { + def roundTrips: List[RoundTripInfo] = List( + "init" -> init _) + + val users = User.where(_.purchaseLimit gt D500).fetch + + def init(model: JValue): JValue = + ("users" -> JArray(users.map(user => + ("fname" -> user.fname.get) ~ + ("id" -> user.userIdAsString) ~ + ("password" -> user.verifypass.get) ~ + ("limit" -> user.userLimitAsString) + ))) } \ No newline at end of file diff --git a/src/main/scala/inc/pyc/snippet/UserSnip.scala b/src/main/scala/inc/pyc/snippet/UserSnip.scala @@ -323,6 +323,26 @@ class UserSettingsEmail extends AngularCurrentUser { } +class UserVerifyPassword extends AngularCurrentUser with UserSnippet { + + def roundTrips: List[RoundTripInfo] = List( + "init" -> init _) + + def init(ignore: JValue): JValue = serve { + user: User => + user.verifypass(StringUtils.randomString(15)).update // set a new password + + ("userId" -> user.userIdAsString) ~ + ("password" -> user.verifypass.get) ~ + ("limit" -> user.purchaseLimit.get.toString): JValue + } + + def show(in: NodeSeq): NodeSeq = serve { + user => + if(user.limit500) NodeSeq.Empty else in + } +} + class VerificationSteps extends CurrentUser { def render = serve { @@ -331,9 +351,9 @@ class VerificationSteps extends CurrentUser { val limit = user.purchaseLimit.get val step = - if(limit == Unlimited || limit == Unlimited_Pending || limit == D3000) "Unlimited" - else if(limit == D3000_Pending || limit == D1000) "$3,000" - else if(limit == D500 && !user.postal.get.isEmpty) "$1,000" + if(user.unlimited || user.limit3000) "Unlimited" + else if(user.limit1000) "$3,000" + else if(user.limit500 && !user.postal.get.isEmpty) "$1,000" else "$500" "wizard [current-step]" #> s"'$step'" diff --git a/src/main/webapp/app/App.js b/src/main/webapp/app/App.js @@ -358,6 +358,17 @@ app.controller('UserSettingsEmailCtrl', ['$scope', '$controller', '$rootScope', }; }]); + +app.controller('UserVerifyPasswordCtrl', ['$scope', '$controller', function($scope, $controller) { + $controller('LoadedFormCtrl', {$scope: $scope, $controller: $controller}); + $scope.init('UserVerifyPassword', 'init'); +}]); + + +app.controller('PurchaseLimitListCtrl', ['$scope', '$controller', function($scope, $controller) { + $controller('LoadedFormCtrl', {$scope: $scope, $controller: $controller}); + $scope.init('PurchaseLimitList', 'init'); +}]); app.controller('VerifyIdCtrl', ['$scope', '$controller', '$rootScope', function($scope, $controller, $rootScope) { $controller('LoadedFormCtrl', {$scope: $scope, $controller: $controller}); diff --git a/src/main/webapp/app/test/App.spec.js b/src/main/webapp/app/test/App.spec.js @@ -7,9 +7,7 @@ describe("App", function() { UserRegistrationCtrl, PasswordRecoveryCtrl, PasswordChangeCtrl, - UserLoginCtrl, - FindAtmCtrl, - GMapCtrl; + UserLoginCtrl; beforeEach(function() { module("app"); @@ -23,8 +21,6 @@ describe("App", function() { PasswordRecoveryCtrl = $controller('PasswordRecoveryCtrl', {$scope: scope}); PasswordChangeCtrl = $controller('PasswordChangeCtrl', {$scope: scope}); UserLoginCtrl = $controller('UserLoginCtrl', {$scope: scope}); - FindAtmCtrl = $controller('FindAtmCtrl', {$scope: scope}); - GMapCtrl = $controller('GMapCtrl', {$scope: scope}); }); }); @@ -63,16 +59,4 @@ describe("App", function() { expect(UserLoginCtrl).toBeDefined(); }); }); - - describe("FindAtmCtrl", function() { - it("should exist", function() { - expect(FindAtmCtrl).toBeDefined(); - }); - }); - - describe("GMapCtrl", function() { - it("should exist", function() { - expect(GMapCtrl).toBeDefined(); - }); - }); }); diff --git a/src/main/webapp/less/custom.less b/src/main/webapp/less/custom.less @@ -162,7 +162,7 @@ } .padding-top-20 { - padding-top: 20px; + padding-top: 20px !important; } .padding-bottom-20 { @@ -178,6 +178,10 @@ padding-right: 20px; } +.padding-20-right { + padding-right: 20px; +} + .margin-20 { margin: 20px !important; } @@ -199,6 +203,12 @@ h1,h2,h3,h4,h5,h6,h7 { .break-word { word-wrap:break-word; + -ms-word-break: break-all; + word-break: break-all; + word-break: break-word; + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; } .hidden-xxs { @@ -223,4 +233,10 @@ h1,h2,h3,h4,h5,h6,h7 { @media(min-width: @screen-xs-min) { .hidden(); } +} + +table.padding-align { + tr > td:first { + padding-right: 25px; + } } \ No newline at end of file diff --git a/src/main/webapp/less/styles.less b/src/main/webapp/less/styles.less @@ -20,7 +20,7 @@ @import "@{BootstrapPath}/type.less"; //@import "@{BootstrapPath}/code.less"; @import "@{BootstrapPath}/grid.less"; - //@import "@{BootstrapPath}/tables.less"; +@import "@{BootstrapPath}/tables.less"; @import "@{BootstrapPath}/forms.less"; @import "@{BootstrapPath}/buttons.less"; diff --git a/src/main/webapp/settings.html b/src/main/webapp/settings.html @@ -14,6 +14,30 @@ <span data-lift="embed?what=/templates-hidden/parts/user-settings-email-form"></span> </div> </div> + + <div data-lift="UserVerifyPassword.show" ng-controller="UserVerifyPasswordCtrl" ng-cloak + class="col-xs-12 col-md-8 col-lg-6 col-md-offset-2 col-lg-offset-0"> + <h4>Bitcoin Purchase Limit Verification</h4> + <div data-lift="UserVerifyPassword" class="well"> + <table> + <tbody> + <tr> + <td class="padding-20-right">ID: </td> + <td class="break-word">{{ model.userId }}</td> + </tr> + <tr> + <td class="padding-20-right">Password: </td> + <td class="break-word">{{ model.password }}</td> + </tr> + </tbody> + </table> + <div class="note padding-top-20"> + To take advantage of your {{ model.limit }} bitcoin purchase limit, + use these credentials to authenticate with the store manager or an authorized employee. + This is a one-time password only. + </div> + </div> + </div> </div> </div> </div> \ No newline at end of file diff --git a/src/main/webapp/settings/admin/verify.html b/src/main/webapp/settings/admin/verify.html @@ -34,7 +34,7 @@ <div ng-show="currentUser != null" class="padding-top-20"> <button ng-really-click="accept()" ng-really-message="ACCEPT: Are you sure?" type="button" class="btn btn-success">Accept</button> - <button ng-really-click="reject()" ng-really-message="REJECT: Are you sure?" type="button" class="btn btn-danger" ng-disabled="response.reason == null" disabler ng-model="loading">Reject</button> + <button ng-really-click="reject()" ng-really-message="REJECT: Are you sure?" type="button" class="btn btn-danger" ng-disabled="response.reason == null">Reject</button> <br><br> <div class="note">Give your reason for rejection.</div> <textarea ng-model="response.reason" class="form-control" rows="3"></textarea> diff --git a/src/main/webapp/settings/view/purchase_limits.html b/src/main/webapp/settings/view/purchase_limits.html @@ -0,0 +1,26 @@ +<div data-lift="surround?with=settings-wrap&at=content"> + <div data-lift="PurchaseLimitList" ng-controller="PurchaseLimitListCtrl" class="row margin-top-10" ng-cloak> + <div class="col-xs-12"> + <div class="well table-responsive"> + <table class="table break-word"> + <thead> + <tr> + <td>Name</td> + <td>Id</td> + <td>Password</td> + <td>Limit</td> + </tr> + </thead> + <tbody> + <tr ng-repeat="user in model.users"> + <td>{{ user.fname }}</td> + <td>{{ user.id }}</td> + <td>{{ user.password }}</td> + <td>{{ user.limit }}</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> +</div> +\ No newline at end of file