Add invitation logic and clean up DBProg error handling.
[?]
Jun 19, 2015, 10:20 PM
2XQD6KKKD6QVHFHAEMVE3XXY7X2T7BLTLL7XIILZAXNJJH2YONUQCDependencies
- [2]
V2VDN77HEnable postgres configuration via environment variable for Heroku. - [3]
JEOPOOPTDockerfile now builds correctly. - [4]
ZITLSTYXFix problems with SQL queries & depreciation function parsing. - [5]
OBFPJS2GProject successfully builds and tests under nix. - [6]
A6HKMINBAttempting to improve JSON handling. - [7]
7KZP4RHZSwitch from Data.Time to Data.Thyme - [8]
GKGVYBZGAdded JSON serialization to TimeLog - [9]
4IQVQL4TAdded client for payouts endpoint. - [10]
MXLZBRQNTrickle. - [11]
TLQ72DSJLenses, sqlite-simple - [12]
64C6AWH6Rename Ananke -> Quixotic, project reboot. - [13]
HE3JTXO3Added client call to payouts. - [14]
JKMHA2QGSQLite support is now relatively sane. - [15]
5XFJNUAZStart of addition of project infrastructure. - [16]
5W5M56VJMove library code to 'lib' - [17]
PBD7LZYQPostgres & auth are beginning to function. - [18]
WFZDMVUXRename ADB -> QDB - [19]
W35DDBFYFactor common JSON conversions up into client lib module. - [20]
Z7KS5XHHVery WIP. Wow. - [21]
4U7F3CPITHE GREAT RENAMING OF THINGS! - [22]
SPJCFHXWUpdate shell scripts to point to https://aftok.com and prompt for input. - [23]
LAROLAYUWIP - [24]
TZQJVHBAAdd auction functions to ADB. - [25]
WO2MINIFAuctions now compile! - [26]
LUM4VQJIIncrement. - [27]
7XN3I3QJAdd 'loggedIntervals' endpoint. - [28]
LD4GLVSFMore database stuff. - [29]
NJZ3DKZYTHEY CAN TALK! - [30]
BROSTG5KBeginning of modularization of server. - [31]
I2KHGVD4Require project permissions for access to most data. - [32]
O5FVTOM6Undo JSON silliness, enable a couple more routes. - [33]
GCVQD44VCreate amends endpoint, switch to UUID primary keys - [34]
QMRKFEPGRefactor QDB to use a free monad algebra instead. - [35]
FRPWIKCNAdded log event parsing to Quixotic.Database.SQLite - [36]
MWUPXTBFA few steps down a road to be abandoned. - [37]
WZUHEZSBStart of migration back toward snap. - [38]
75N3UJ4JMore progression toward lenses. - [39]
XMONXALYFix sqlite/readWorkIndex table name - [40]
BXGLKYRXAdded primitive user registration handler. - [41]
EMVTF2IWWIP moving back to snap. - [42]
64VI73NPServer now compiles using abstracted SQLite - [43]
VJPT6HDRFix remaining type errors after addition of login handler. - [44]
5DRIWGLUImproving TimeLog specs - [45]
TCOAKCGGCompleted conversion to snap. - [46]
IZEVQF62Work in progress replacing sqlite with postgres. - [47]
NVOCQVASInitial failing tests. - [48]
RPAJLHMTChange to use UUIDs instead of ints for primary keys. - [49]
TNR3TEHKSwitch to Postgres + snaplet arch compiles. - [50]
4ZLEDBK7Initial attempts at dockerizing, cabal isn't cooperating. - [*]
KNSI575VCleanup of EventLog types. - [*]
ADMKQQGCInitial empty Snap project. - [*]
EQXRXRZDChanged to use tasty instead of test-framework
Change contents
- replacement in Dockerfile at line 12
apt-get install -y --no-install-recommends cabal-install-1.22 ghc-7.8.4 happy-1.19.4 alex-3.1.3 \apt-get install -y --no-install-recommends cabal-install-1.22 ghc-7.10.2 happy-1.19.5 alex-3.1.4 \ - replacement in Dockerfile at line 16
ENV PATH /root/.cabal/bin:/opt/cabal/1.22/bin:/opt/ghc/7.8.4/bin:/opt/happy/1.19.4/bin:/opt/alex/3.1.3/bin:$PATHENV PATH /root/.cabal/bin:/opt/cabal/1.22/bin:/opt/ghc/7.10.2/bin:/opt/happy/1.19.5/bin:/opt/alex/3.1.4/bin:$PATH - replacement in aftok.cabal at line 12
Cabal-version: >= 1.18Cabal-version: >= 1.22 - replacement in aftok.cabal at line 33
base >= 4 && < 5, classy-prelude >= 0.10.1, aeson >= 0.8.0.2, attoparsec >= 0.12.1.2, base64-bytestring >= 1.0.0.1base >= 4.8.0, classy-prelude == 0.12.*, aeson == 0.8.*, attoparsec == 0.12.*, base64-bytestring == 1.0.* - replacement in aftok.cabal at line 41
, containers >= 0.5.5.1, containers >= 0.5.6 - replacement in aftok.cabal at line 43
, either >= 4.3.1, errors >= 1.4.7, either >= 4.4.1, errors >= 1.4 && < 1.5 - replacement in aftok.cabal at line 46
, groups >= 0.4, heaps >= 0.3.1, groups >= 0.4 && < 0.5, heaps >= 0.3.1 && < 0.4 - edit in aftok.cabal at line 49
, HsOpenSSL >= 0.11 && < 0.12 - replacement in aftok.cabal at line 51[5.68]→[5.2841:2913](∅→∅),[5.2841]→[5.2841:2913](∅→∅),[5.1474]→[5.1892:1909](∅→∅),[5.2913]→[5.1892:1909](∅→∅),[5.5120]→[5.1892:1909](∅→∅),[5.1892]→[5.1892:1909](∅→∅),[5.1909]→[5.2914:2996](∅→∅)
, lens >= 4.4.0.2, network-bitcoin >= 1.7.0, old-locale, postgresql-simple >= 0.4.9 && < 0.5, safe >= 0.3.8, lens >= 4.11 && < 4.12, network-bitcoin >= 1.8 && < 1.9, old-locale >= 1.0, postgresql-simple >= 0.4.10 && < 0.5, safe >= 0.3.9 && < 0.4 - edit in aftok.cabal at line 58
, sqlite-simple >= 0.4.8 && < 0.5 - replacement in aftok.cabal at line 59
, text >= 1.2, thyme >= 0.3.5, uuid >= 1.3, text >= 1.2.1 && < 1.3, thyme >= 0.3.5 && < 0.4, uuid >= 1.3.10 && < 1.4 - replacement in aftok.cabal at line 63[5.1084]→[5.1070:1081](∅→∅),[5.1200]→[5.1070:1081](∅→∅),[5.2906]→[5.1070:1081](∅→∅),[5.1070]→[5.1070:1081](∅→∅)
, wreq, wreq >= 0.4 - replacement in aftok.cabal at line 83
, hspec >= 1.8.1, hspec >= 2.1.7 - replacement in aftok.cabal at line 86
, QuickCheck >= 2.7, QuickCheck >= 2.8 - replacement in aftok.cabal at line 108
, classy-prelude >= 0.10.2, classy-prelude - edit in aftok.cabal at line 112
, HStringTemplate >= 0.8.3 - edit in aftok.cabal at line 114
, HsOpenSSL - replacement in aftok.cabal at line 116[5.52]→[5.7249:7297](∅→∅),[5.92]→[5.244:294](∅→∅),[5.7297]→[5.244:294](∅→∅),[5.244]→[5.244:294](∅→∅)
, mtl >= 2 && < 3, MonadCatchIO-transformers >= 0.2.1 && < 0.4, mtl >= 2.2 && < 3, MonadCatchIO-transformers >= 0.3 && < 0.4 - replacement in aftok.cabal at line 121[5.7326]→[5.7376:7427](∅→∅),[5.7376]→[5.7376:7427](∅→∅),[5.2334]→[5.443:545](∅→∅),[5.7427]→[5.443:545](∅→∅),[5.443]→[5.443:545](∅→∅),[5.545]→[5.7428:7467](∅→∅)
, snap >= 0.13 && < 0.14, snap-core >= 0.9 && < 0.10, snap-server >= 0.9 && < 0.10, snaplet-postgresql-simple >= 0.6, sendgrid-haskell >= 1.0, snap >= 0.14, snap-core >= 0.9 && < 0.11, snap-server >= 0.9 && < 0.11, snaplet-postgresql-simple >= 0.6 && < 0.11 - replacement in aftok.cabal at line 129
, uuid >= 1.3, uuid - edit in lib/Aftok/Auction.hs at line 9
import Data.Thyme.Clock as C - replacement in lib/Aftok/Auction.hs at line 19
, _auctionEnd :: UTCTime, _auctionEnd :: C.UTCTime - replacement in lib/Aftok/Auction.hs at line 30
, _bidTime :: UTCTime, _bidTime :: C.UTCTime - file deletion: SQLite.hs SQLite.hs SQLite.hs[5.40]→[5.62:95](∅→∅),[5.95]→[5.3612:3612](∅→∅),[5.988]→[5.1203:1236](∅→∅),[5.1236]→[5.3612:3612](∅→∅),[5.3590]→[5.4900:4933](∅→∅),[5.4933]→[5.3612:3612](∅→∅)
{-# LANGUAGE ScopedTypeVariables, OverloadedStrings, NoImplicitPrelude #-}{-# LANGUAGE TemplateHaskell #-}module Aftok.Database.SQLite (sqliteQDB) whereimport ClassyPreludeimport Control.Lensimport Data.Hourglassimport Database.SQLite.Simpleimport Database.SQLite.Simple.ToFieldimport qualified Text.Read as Rimport Aftokimport Aftok.Auctionsimport Aftok.Projectsimport Aftok.Databaseimport Aftok.TimeLogimport Aftok.Usersnewtype PLogEntry = PLogEntry LogEntrymakePrisms ''PLogEntryinstance ToRow PLogEntry wheretoRow (PLogEntry (LogEntry a e)) =toRow (a ^. address, e ^. (eventType . to eventName), e ^. eventTime)instance FromRow PLogEntry wherefromRow =let workEventParser = WorkEvent <$> (field >>= nameEvent) <*> fieldlogEntryParser = LogEntry <$> (fmap BtcAddr field) <*> workEventParserin fmap PLogEntry logEntryParsernewtype PAuction = PAuction AuctionmakePrisms ''PAuctioninstance FromRow PAuction wherefromRow =let auctionParser = Auction <$> fmap R.read field <*> fieldin fmap PAuction auctionParsernewtype PBid = PBid BidmakePrisms ''PBidinstance FromRow PBid wherefromRow =let bidParser = Bid <$> fmap UserId field <*> fmap Seconds field <*> fmap R.read field <*> fieldin fmap PBid bidParsernewtype PSeconds = PSeconds Secondsinstance ToField PSeconds wheretoField (PSeconds (Seconds i)) = toField inewtype PUserId = PUserId UserIdinstance ToField PUserId wheretoField (PUserId (UserId i)) = toField inewtype PAuctionId = PAuctionId AuctionIdinstance ToField PAuctionId wheretoField (PAuctionId (AuctionId i)) = toField i-- TODO: Record the user idrecordEvent' :: ProjectId -> UserId -> LogEntry -> ReaderT Connection IO ()recordEvent' _ _ logEntry = doconn <- asklift $ execute conn"INSERT INTO work_events (btc_addr, event_type, event_time) VALUES (?, ?, ?)"(logEntry ^. (from _PLogEntry))readWorkIndex' :: ProjectId -> ReaderT Connection IO WorkIndexreadWorkIndex' _ = doconn <- askrows <- lift $ query_ conn"SELECT btc_addr, event_type, event_time from work_events"lift . pure . workIndex $ fmap (^. _PLogEntry) rowsnewAuction' :: ProjectId -> Auction -> ReaderT Connection IO AuctionIdnewAuction' _ auc = doconn <- asklift $ execute conn"INSERT INTO auctions (raise_amount, end_time) VALUES (?, ?)"(show $ auc ^. raiseAmount, auc ^. auctionEnd)lift . fmap AuctionId $ lastInsertRowId connreadAuction' :: AuctionId -> ReaderT Connection IO (Maybe Auction)readAuction' aucId = doconn <- askrows <- lift $ query conn"SELECT raise_amount, end_time FROM auctions WHERE ROWID = ?"(Only $ PAuctionId aucId)lift . return . headMay $ fmap (^. _PAuction) rowsrecordBid' :: AuctionId -> Bid -> ReaderT Connection IO ()recordBid' aucId bid = doconn <- asklift $ execute conn"INSERT INTO bids (auction_id, user_id, bid_seconds, bid_amount, bid_time) values (?, ?, ?, ?, ?)"( PAuctionId aucId, PUserId $ bid ^. bidUser, PSeconds $ bid ^. bidSeconds, show $ bid ^. bidAmount, bid ^. bidTime)readBids' :: AuctionId -> ReaderT Connection IO [Bid]readBids' aucId = doconn <- askrows <- lift $ query conn"SELECT user_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ?"(Only $ PAuctionId aucId)lift . return $ fmap (^. _PBid) rowscreateUser' :: User -> ReaderT Connection IO UserIdcreateUser' u = doconn <- asklift $ execute conn"INSERT INTO users (btc_addr, email) VALUES (?, ?)"(u ^. (userAddress . address), u ^. userEmail)lift . fmap UserId $ lastInsertRowId connsqliteQDB :: QDB (ReaderT Connection IO)sqliteQDB = QDB{ recordEvent = recordEvent', readWorkIndex = readWorkIndex', newAuction = newAuction', readAuction = readAuction', recordBid = recordBid', readBids = readBids', createUser = createUser', findUser = \_ -> pure Nothing, findUserByUserName = \_ -> pure Nothing} - replacement in lib/Aftok/Database/PostgreSQL.hs at line 3
module Aftok.Database.PostgreSQL (QDBM(..)) wheremodule Aftok.Database.PostgreSQL (QDBM(), runQDBM) where - edit in lib/Aftok/Database/PostgreSQL.hs at line 10
import Control.Monad.Trans.Either - replacement in lib/Aftok/Database/PostgreSQL.hs at line 29
newtype QDBM a = QDBM { runQDBM :: ReaderT Connection IO a }newtype QDBM a = QDBM (ReaderT Connection (EitherT DBError IO) a) - edit in lib/Aftok/Database/PostgreSQL.hs at line 32
instance MonadIO QDBM whereliftIO = QDBM . lift . liftrunQDBM :: Connection -> QDBM a -> EitherT DBError IO arunQDBM conn (QDBM r) = runReaderT r conn - edit in lib/Aftok/Database/PostgreSQL.hs at line 69
emailParser :: FieldParser EmailemailParser f v = Email <$> fromField f v - edit in lib/Aftok/Database/PostgreSQL.hs at line 80[5.51]→[5.1605:1606](∅→∅),[5.136]→[5.1605:1606](∅→∅),[5.1605]→[5.1605:1606](∅→∅),[5.2338]→[5.170:271](∅→∅)
newtype PPid = PPid ProjectIdinstance ToField PPid wheretoField (PPid (ProjectId i)) = toField i - edit in lib/Aftok/Database/PostgreSQL.hs at line 84
newtype PUTCTime = PUTCTime C.UTCTimeinstance ToField PUTCTime wheretoField (PUTCTime t) = toField $ fromThyme t - replacement in lib/Aftok/Database/PostgreSQL.hs at line 99
Auction <$> fieldWith btcParser<*> fieldAuction <$> (fromRational <$> field)<*> fieldWith utcParser - replacement in lib/Aftok/Database/PostgreSQL.hs at line 107
<*> field<*> fieldWith utcParser - replacement in lib/Aftok/Database/PostgreSQL.hs at line 113
<*> field<*> (Email <$> field) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 123
<*> field<*> fieldWith utcParser - edit in lib/Aftok/Database/PostgreSQL.hs at line 127
invitationParser :: RowParser InvitationinvitationParser =Invitation <$> fieldWith pidParser<*> fieldWith uidParser<*> fieldWith emailParser<*> fieldWith utcParser<*> fmap (fmap toThyme) field - replacement in lib/Aftok/Database/PostgreSQL.hs at line 143
lift $ execute conn q dlift . lift $ execute conn q d - replacement in lib/Aftok/Database/PostgreSQL.hs at line 148
ids <- lift $ query conn q dids <- lift . lift $ query conn q d - edit in lib/Aftok/Database/PostgreSQL.hs at line 153
conn <- asklift . lift $ queryWith p conn q dtransactQDBM :: QDBM a -> QDBM atransactQDBM (QDBM rt) = QDBM $ do - replacement in lib/Aftok/Database/PostgreSQL.hs at line 159
lift $ queryWith p conn q dlift . EitherT $ withTransaction conn (runEitherT $ runReaderT rt conn) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 185
(pid, uid, PUTCTime e)(pid, uid, fromThyme e) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 190
(pid, uid, PUTCTime s, PUTCTime e)(pid, uid, fromThyme s, fromThyme e) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 194
(pid, uid, PUTCTime s)(pid, uid, fromThyme s) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 212
dbEval (ReadWorkIndex pid) = dodbEval (ReadWorkIndex (ProjectId pid)) = do - replacement in lib/Aftok/Database/PostgreSQL.hs at line 215
(Only $ PPid pid)(Only $ pid) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 222
(pid ^. (_ProjectId), auc ^. (raiseAmount.to PBTC), auc ^. auctionEnd)(pid ^. (_ProjectId), auc ^. (raiseAmount.to PBTC), auc ^. (auctionEnd.to fromThyme)) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 238
, bid ^. bidTime, bid ^. (bidTime.to fromThyme) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 249
(user' ^. (username._UserName), user' ^. (userAddress._BtcAddr), user' ^. userEmail)(user' ^. (username._UserName), user' ^. (userAddress._BtcAddr), user' ^. userEmail._Email) - edit in lib/Aftok/Database/PostgreSQL.hs at line 262
dbEval (CreateInvitation (ProjectId pid) (UserId uid) (Email e) t) = doinvCode <- liftIO randomInvCodevoid $ pexec"INSERT INTO invitations (project_id, invitor_id, invitee_email, invitation_key, invitation_time) \\VALUES (?, ?, ?, ?, ?)"(pid, uid, e, renderInvCode invCode, fromThyme t)pure invCode - edit in lib/Aftok/Database/PostgreSQL.hs at line 271
dbEval (FindInvitation ic) = doinvitations <- pquery invitationParser"SELECT project_id, invitor_id, invitee_email, invitation_time, acceptance_time \\FROM invitations WHERE invitation_key = ?"(Only $ renderInvCode ic)pure $ headMay invitationsdbEval (AcceptInvitation (UserId uid) ic t) = transactQDBM $ dovoid $ pexec"UPDATE invitations SET acceptance_time = ? WHERE invitation_key = ?"(fromThyme t, renderInvCode ic)void $ pexec"INSERT INTO project_companions (project_id, user_id, invited_by, joined_at) \\SELECT i.project_id, ?, i.invitor_id, ? \\FROM invitations i \\WHERE i.invitation_key = ?"(uid, fromThyme t, renderInvCode ic) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 293
(p ^. projectName, p ^. inceptionDate, p ^. (initiator._UserId), toJSON $ p ^. depf)(p ^. projectName, p ^. (inceptionDate.to fromThyme), p ^. (initiator._UserId), toJSON $ p ^. depf) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 314
-- FIXME, these are just placeholdersdbEval (OpForbidden _ reason _) = fail $ show reasondbEval (SubjectNotFound _) = fail "Subject of operation was not found."[5.1045]dbEval (RaiseDBError err _) = QDBM . lift $ left err - replacement in lib/Aftok/Database.hs at line 1
{-# LANGUAGE GADTs #-}{-# LANGUAGE GADTs, DeriveDataTypeable #-} - edit in lib/Aftok/Database.hs at line 7
import Data.AffineSpaceimport Data.Thyme.Clock as C - edit in lib/Aftok/Database.hs at line 33
CreateInvitation :: ProjectId -> InvitingUID -> Email -> C.UTCTime -> DBOp InvitationCodeFindInvitation :: InvitationCode -> DBOp (Maybe Invitation)AcceptInvitation :: UserId -> InvitationCode -> C.UTCTime -> DBOp () - replacement in lib/Aftok/Database.hs at line 48
OpForbidden :: forall x. UserId -> OpForbiddenReason -> DBOp x -> DBOp xSubjectNotFound :: forall x. DBOp x -> DBOp xRaiseDBError :: forall x. DBError -> DBOp x -> DBOp x - replacement in lib/Aftok/Database.hs at line 52
deriving (Eq, Show)| InvitationExpired| InvitationAlreadyAcceptedderiving (Eq, Show, Typeable)data DBError = OpForbidden UserId OpForbiddenReason| SubjectNotFoundderiving (Eq, Show, Typeable)instance Exception DBErrorraiseOpForbidden :: UserId -> OpForbiddenReason -> DBOp x -> DBOp xraiseOpForbidden uid r = RaiseDBError (OpForbidden uid r)raiseSubjectNotFound :: DBOp x -> DBOp xraiseSubjectNotFound = RaiseDBError SubjectNotFound - edit in lib/Aftok/Database.hs at line 97
addUserToProject :: ProjectId -> InvitingUID -> InvitedUID -> DBProg ()addUserToProject pid current new =withProjectAuth pid current $ AddUserToProject pid current new - replacement in lib/Aftok/Database.hs at line 103
else OpForbidden uid UserNotProjectMember actelse raiseOpForbidden uid UserNotProjectMember actaddUserToProject :: ProjectId -> InvitingUID -> InvitedUID -> DBProg ()addUserToProject pid current new =withProjectAuth pid current $ AddUserToProject pid current newcreateInvitation :: ProjectId -> InvitingUID -> Email -> C.UTCTime -> DBProg InvitationCodecreateInvitation pid current email t =withProjectAuth pid current $ CreateInvitation pid current email tfindInvitation :: InvitationCode -> DBProg (Maybe Invitation)findInvitation ic = fc $ FindInvitation icacceptInvitation :: UserId -> InvitationCode -> C.UTCTime -> DBProg ()acceptInvitation uid ic t = doinv <- findInvitation iclet act = AcceptInvitation uid ic tcase inv ofNothing ->fc $ raiseSubjectNotFound actJust i | t .-. (i ^. invitationTime) > fromSeconds (60 * 60 * 72 :: Int) ->fc $ raiseOpForbidden uid InvitationExpired actJust i | isJust (i ^. acceptanceTime) ->fc $ raiseOpForbidden uid InvitationAlreadyAccepted actJust i ->withProjectAuth (i ^. projectId) (i ^. invitingUser) act - replacement in lib/Aftok/Database.hs at line 134
createEvent p u l = withProjectAuth p u $ CreateEvent p u lcreateEvent p u l = withProjectAuth p u $ CreateEvent p u l - replacement in lib/Aftok/Database.hs at line 140
forbidden = OpForbidden uid UserNotEventLogger actmissing = SubjectNotFound actforbidden = raiseOpForbidden uid UserNotEventLogger actmissing = raiseSubjectNotFound act - replacement in lib/Aftok/Json.hs at line 83
qdbProjectJSON (projectId, project) = v1 $object [ "projectId" .= (tshow $ projectId ^. _ProjectId)qdbProjectJSON (pid, project) = v1 $object [ "projectId" .= (tshow $ pid ^. _ProjectId) - edit in lib/Aftok.hs at line 10
import Data.ByteString.Base64.URL as B64 - edit in lib/Aftok.hs at line 12
import Data.Thyme.Clock as C - replacement in lib/Aftok.hs at line 14
import Network.Bitcoin (BTC)import OpenSSL.Random - edit in lib/Aftok.hs at line 34
newtype Email = Email Text deriving (Show, Eq)makePrisms ''Email - replacement in lib/Aftok.hs at line 40
, _userEmail :: Text, _userEmail :: Email - edit in lib/Aftok.hs at line 47
type ProjectName = Text - replacement in lib/Aftok.hs at line 49
{ _projectName :: Text, _inceptionDate :: UTCTime{ _projectName :: ProjectName, _inceptionDate :: C.UTCTime - replacement in lib/Aftok.hs at line 56[5.3194]→[5.3194:3223](∅→∅),[5.3223]→[5.1024:1060](∅→∅),[5.1060]→[5.3251:3399](∅→∅),[5.3251]→[5.3251:3399](∅→∅)
data Invitation = Invitation{ _invitationProject :: ProjectId, _currentMember :: UserId, _sentAt :: UTCTime, _expiresAt :: UTCTime, _toAddr :: BtcAddr, _amount :: BTC}makeLenses ''Invitationnewtype InvitationCode = InvitationCode ByteString deriving (Eq)makePrisms ''InvitationCoderandomInvCode :: IO InvitationCoderandomInvCode = InvitationCode <$> randBytes 256 - replacement in lib/Aftok.hs at line 62
newtype InvitationId = InvitationId UUID deriving (Show, Eq)parseInvCode :: Text -> Either String InvitationCodeparseInvCode t = docode <- B64.decode . encodeUtf8 $ tif length code == 256then Right $ InvitationCode codeelse Left "Invitation code appears to be invalid." - replacement in lib/Aftok.hs at line 69
data Acceptance = Acceptance{ _acceptedInvitation :: InvitationId, _blockHeight :: Integer, _observedAt :: UTCTimerenderInvCode :: InvitationCode -> TextrenderInvCode (InvitationCode bs) = decodeUtf8 $ B64.encode bsdata Invitation = Invitation{ _projectId :: ProjectId, _invitingUser :: UserId, _invitedEmail :: Email, _invitationTime :: C.UTCTime, _acceptanceTime :: Maybe C.UTCTime - replacement in lib/Aftok.hs at line 79
makeLenses ''AcceptancemakeLenses ''Invitation - edit in server/Aftok/QConfig.hs at line 8
import qualified Network.Sendgrid.Api as Sendgrid - replacement in server/Aftok/QConfig.hs at line 10
import System.IO(FilePath)import System.IO (FilePath) - edit in server/Aftok/QConfig.hs at line 16
- edit in server/Aftok/QConfig.hs at line 22
, sendgridAuth :: Sendgrid.Authentication, templatePath :: System.IO.FilePath - edit in server/Aftok/QConfig.hs at line 40
<*> readSendgridAuth cfg<*> C.require cfg "templatePath"readSendgridAuth :: CT.Config -> IO Sendgrid.AuthenticationreadSendgridAuth cfg =Sendgrid.Authentication <$> C.require cfg "sendgridUser"<*> C.require cfg "sendgridKey" - replacement in server/Aftok/Snaplet/Auth.hs at line 7
import Data.Attoparsec.ByteString (parseOnly)import Data.UUID(fromASCIIBytes)import Data.Attoparsec.ByteString(parseOnly, takeByteString) - replacement in server/Aftok/Snaplet/Auth.hs at line 41[5.2162]→[5.11804:11872](∅→∅),[5.2100]→[5.2252:2346](∅→∅),[5.11872]→[5.2252:2346](∅→∅),[5.2252]→[5.2252:2346](∅→∅),[5.2346]→[5.11873:11997](∅→∅)
requireProjectId :: Handler App App ProjectIdrequireProjectId = dopidMay <- getParam "projectId"case ProjectId <$> (readMay =<< fmap decodeUtf8 pidMay) ofNothing -> snapError 400 "Value of parameter projectId could not be parsed to a valid value."Just pid -> pure pidrequireProjectId :: MonadSnap m => m ProjectIdrequireProjectId = domaybePid <- parseParam "projectId" pidParsermaybe (snapError 400 "Value of parameter \"projectId\" cannot be parsed as a valid UUID")puremaybePidwherepidParser = dobs <- takeByteStringpure $ ProjectId <$> fromASCIIBytes bs - edit in server/Aftok/Snaplet/Projects.hs at line 7
import Control.Lens - edit in server/Aftok/Snaplet/Projects.hs at line 9
import Data.Attoparsec.ByteString (takeByteString)import Data.Thyme.Clock as Cimport qualified Network.Sendgrid.Api as Sendgridimport System.IO (FilePath)import Text.StringTemplate - edit in server/Aftok/Snaplet/Projects.hs at line 17
import Aftok.QConfig - replacement in server/Aftok/Snaplet/Projects.hs at line 35
timestamp <- liftIO getCurrentTimesnapEval . createProject $ Project (cpn cp) timestamp uid (cpdepf cp)t <- liftIO C.getCurrentTimesnapEval . createProject $ Project (cpn cp) t uid (cpdepf cp) - edit in server/Aftok/Snaplet/Projects.hs at line 49
projectInviteHandler :: QConfig -> Handler App App ()projectInviteHandler cfg = douid <- requireUserIdpid <- requireProjectIdtoEmail <- parseParam "email" (fmap (Email . decodeUtf8) takeByteString)t <- liftIO C.getCurrentTime(Just u, Just p, invCode) <- snapEval $(,,) <$> findUser uid<*> findProject pid uid<*> createInvitation pid uid toEmail tinviteEmail <- liftIO $projectInviteEmail (templatePath cfg) (p ^. projectName) (u ^. userEmail) toEmail invCodemaybeSuccess <- liftIO $ Sendgrid.sendEmail (sendgridAuth cfg) inviteEmailmaybe(snapError 500 "The invitation record was created successfully, but the introductory email could not be sent.")(const $ pure ())maybeSuccessprojectInviteEmail :: System.IO.FilePath-> ProjectName-> Email -> Email-> InvitationCode-> IO Sendgrid.EmailMessageprojectInviteEmail templatePath pn from' to' invCode = dotemplates <- directoryGroup templatePathtemplate <- maybe (fail "Could not find template for invitation email") pure $getStringTemplate "invitation_email" templateslet setAttrs = setAttribute "invCode" (renderInvCode invCode)return $ Sendgrid.EmailMessage{ from = unpack $ from' ^. _Email, to = unpack $ to' ^. _Email, subject = unpack $ "Welcome to the "<>pn<>" Aftok!", text = render $ setAttrs template} - edit in server/Aftok/Snaplet/Projects.hs at line 85[5.3686]
- edit in server/Aftok/Snaplet/Users.hs at line 11
import Data.Thyme.Clock as C - edit in server/Aftok/Snaplet/Users.hs at line 25
, _invitationCodes :: [InvitationCode] - replacement in server/Aftok/Snaplet/Users.hs at line 31
let u = User <$> (UserName <$> v .: "username")<*> (BtcAddr <$> v .: "btcAddr")<*> v .: "email"in CU <$> u <*> (fromString <$> v .: "password")let parseUser = User <$> (UserName <$> v .: "username")<*> (BtcAddr <$> v .: "btcAddr")<*> (Email <$> v .: "email")parseInvitationCodes c = either(\e -> fail $ "Invitation code was rejected as invalid: " <> e)pure(traverse parseInvCode c)in CU <$> parseUser<*> (fromString <$> v .: "password")<*> (parseInvitationCodes =<< v .: "invitation_codes") - replacement in server/Aftok/Snaplet/Users.hs at line 46
registerHandler :: Handler App App ()registerHandler :: Handler App App UserId - edit in server/Aftok/Snaplet/Users.hs at line 49
-- allow any number of 'invitationCode' query parameters - edit in server/Aftok/Snaplet/Users.hs at line 51
t <- liftIO C.getCurrentTime - replacement in server/Aftok/Snaplet/Users.hs at line 53
createQUser = snapEval (createUser $ userData ^. cuser)createQUser = snapEval $ douserId <- createUser $ userData ^. cuservoid $ traverse (\c -> acceptInvitation userId c t) (userData ^. invitationCodes)return userId - replacement in server/Aftok/Snaplet/Users.hs at line 58
void $ either throwDenied (\_ -> createQUser) authUser[5.3800]either throwDenied (\_ -> createQUser) authUser - edit in server/Aftok/Snaplet.hs at line 10
import Control.Monad.Trans.Either - edit in server/Aftok/Snaplet.hs at line 12
import Data.Attoparsec.ByteString(Parser, parseOnly) - edit in server/Aftok/Snaplet.hs at line 14
import Aftok - replacement in server/Aftok/Snaplet.hs at line 33
getPostgresState = with db getsetLocalPostgresState s = local (set (db . snapletValue) s)getPostgresState = with db getsetLocalPostgresState s = local (set (db . snapletValue) s) - replacement in server/Aftok/Snaplet.hs at line 36
snapEval :: DBProg a -> Handler App App asnapEval p = liftPG . runReaderT . runQDBM $ interpret dbEval psnapEval :: (MonadSnap m, HasPostgres m) => DBProg a -> m asnapEval p = dolet handleDBError (OpForbidden (UserId uid) reason) =snapError 403 $ tshow reason <> " (User " <> tshow uid <> ")"handleDBError (SubjectNotFound) =snapError 404 "The subject of the requested operation could not be found."e <- liftPG $ \conn -> runEitherT (runQDBM conn $ interpret dbEval p)either handleDBError pure e - edit in server/Aftok/Snaplet.hs at line 56
parseParam :: MonadSnap m => ByteString -> Parser a -> m aparseParam name parser = domaybeBytes <- getParam namecase maybeBytes ofNothing -> snapError 400 $ "Parameter "<> tshow name <>" is required"Just bytes -> either(const . snapError 400 $ "Value of parameter "<> tshow name <>" could not be parsed to a valid value.")pure(parseOnly parser bytes) - replacement in server/Main.hs at line 32
appInit QConfig{..} = makeSnaplet "aftok" "Aftok Time Tracker" Nothing $ doappInit cfg = makeSnaplet "aftok" "Aftok Time Tracker" Nothing $ do - replacement in server/Main.hs at line 34
initCookieSessionManager authSiteKey "quookie" cookieTimeoutpgs <- nestSnaplet "db" db $ pgsInit' pgsConfiginitCookieSessionManager (authSiteKey cfg) "quookie" (cookieTimeout cfg)pgs <- nestSnaplet "db" db $ pgsInit' (pgsConfig cfg) - edit in server/Main.hs at line 40
projectCreateRoute = void $ method POST projectCreateHandlerlistProjectsRoute = serveJSON (fmap qdbProjectJSON) $ method GET projectListHandler - edit in server/Main.hs at line 43
projectRoute = serveJSON projectJSON $ method GET projectGetHandler - edit in server/Main.hs at line 47
payoutsRoute = serveJSON payoutsJSON $ method GET payoutsHandlerinviteRoute = void . method POST $ projectInviteHandler cfg - edit in server/Main.hs at line 52
projectCreateRoute = void $ method POST projectCreateHandlerprojectRoute = serveJSON projectJSON $ method GET projectGetHandlerlistProjectsRoute = serveJSON (fmap qdbProjectJSON) $ method GET projectListHandlerpayoutsRoute = serveJSON payoutsJSON $ method GET payoutsHandler - replacement in server/Main.hs at line 54
, ("events/:eventId/amend", amendEventRoute), ("projects", projectCreateRoute), ("projects", listProjectsRoute), ("projects/:projectId", projectRoute) - replacement in server/Main.hs at line 61
, ("projects", projectCreateRoute), ("projects", listProjectsRoute), ("projects/:projectId", projectRoute), ("projects/:projectId/payouts", payoutsRoute), ("projects/:projectId/payouts", payoutsRoute), ("projects/:projectId/invite", inviteRoute), ("events/:eventId/amend", amendEventRoute) - edit in sql/aftok-pg-002.sql at line 6[4.762]
alter table project_companionsadd joined_at timestamp with time zone not nulldefault (now() at time zone "UTC"); - file addition: aftok-pg-003.sql[54.1369]
create table invitations (id uuid primary key default uuid_generate_v4(),project_id uuid references projects(id) not null,invitor_id uuid references users (id) not null,invitee_email text not null,invitation_key text not null,invitation_time timestamp with time zone not null default (now() at time zone 'UTC'),acceptance_time timestamp with time zone);