Compare commits

..

32 commits

Author SHA1 Message Date
cce6811df2
Correct parsing of memos 2023-03-14 12:55:23 -05:00
63d372c2d5
Change Zcash scan to use parser 2023-03-14 10:17:31 -05:00
e437da2841
Implement memo parser 2023-03-10 15:31:47 -06:00
44f14d6abd
Separate periodic tasks from API server 2023-03-04 15:55:42 -06:00
91b5a841f9
Change confirmation window 2023-02-28 14:58:41 -06:00
25fad17363
Implement enhancements for #3 2023-02-28 11:19:08 -06:00
6a766ee0d8
Add batch load of translation 2023-02-16 07:49:05 -06:00
de3293f6ec
Add upsert of language component 2023-02-02 15:43:54 -06:00
fb82923949
Add language endpoints 2023-02-02 15:14:28 -06:00
9564e9fa18
Merge branch 'security' 2023-02-01 12:51:34 -06:00
683f49d069
Fix #5 2023-02-01 12:49:33 -06:00
42957547a9
Merge branch 'notifier' 2023-01-30 15:36:25 -06:00
f348416b28
Documentation update 2023-01-30 15:34:27 -06:00
ddb451383b
Implement payments enhancements and tests 2023-01-30 15:29:21 -06:00
9d6d000d27
Version bump 2023-01-27 11:19:35 -06:00
0e50abffe9
Merge branch 'security' 2023-01-27 11:18:15 -06:00
59ff5a29c7
Implement test 2023-01-27 11:15:03 -06:00
a17e8d6f2a
Implement BLAKE3 for PIN hashing 2023-01-27 11:01:05 -06:00
6d14ccd48a
Implement pin hardening 2023-01-26 12:13:17 -06:00
e6d3646fa8
Merge branch 'notifier' 2023-01-25 11:21:15 -06:00
3afe350816
Version bump 2023-01-25 11:20:19 -06:00
bde97f9211
Correct expiring owners query for paid 2023-01-24 18:34:22 -06:00
a5f6c1efff
Merge branch 'notifier' 2023-01-24 14:12:34 -06:00
814d4c9ee5
Version bump 2023-01-24 14:11:34 -06:00
f2c04ec8d5
Correct types 2023-01-24 13:54:21 -06:00
1e2784f7db
Implement SMTP configuration 2023-01-24 13:27:32 -06:00
a8e1c1b4d8
Update Changelog 2023-01-24 10:21:17 -06:00
1a100fd8ca
Correct owner expiration query 2023-01-24 10:20:00 -06:00
789211b06f
Correct Xero expiration query 2023-01-24 10:18:16 -06:00
927b213dff
Prepare library 2023-01-23 17:09:54 -06:00
4530c95895
Implement function to find expiring owners 2023-01-23 16:56:46 -06:00
0cec845339
Merge branch 'dev' for WooCommerce integration 2023-01-09 10:03:29 -06:00
19 changed files with 575 additions and 118 deletions

View file

@ -4,6 +4,57 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- New type to handle UI translation objects
- New endpoints for API to get/set translation
- Tests for translation endpoints
- Formal parser of ZGo-related tokens in memos
### Changed
- Remove old code for PIN generation
- Xero reference field to include the amount of ZEC received
- Separate periodic tasks from API server
- Zcash transaction monitoring changed to use memo parser
## [1.2.5] - 2023-02-01
### Fixed
- Replaced the PIN generation with the cryptographically-secure `crypto-rng`.
## [1.2.4] - 2023-01-30
### Changed
- Enhance payments to account for early payments on active sessions.
## [1.2.3] - 2023-01-27
### Changed
- Implement `BLAKE3` for PIN hashing.
## [1.2.2] - 2023-01-25
### Fixed
- Corrected selection criteria for expiring owners query
## [1.2.1] - 2023-01-24
### Added
- New configuration fields for SMTP
### Fixed
- Owner expiration query
- Xero token expiration query
## [1.2.0] - 2023-01-09 ## [1.2.0] - 2023-01-09
### Added ### Added

View file

@ -1,4 +1,4 @@
Copyright (c) 2022 Vergara Technologies LLC Copyright (c) 2023 Vergara Technologies LLC
======================================================= =======================================================
Bootstrap Open Source Licence ("BOSL") v. 1.0 Bootstrap Open Source Licence ("BOSL") v. 1.0

View file

@ -1,5 +1,7 @@
# ZGo Back End # ZGo Back End
[![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page)
The API server behind the [ZGo.cash](https://zgo.cash) app. The API server behind the [ZGo.cash](https://zgo.cash) app.
## Dependencies ## Dependencies

View file

@ -3,7 +3,8 @@
module Server where module Server where
import Config import Config
import Control.Concurrent (forkIO)
--import Control.Concurrent (forkIO)
import Database.MongoDB import Database.MongoDB
import Network.Wai.Handler.Warp (defaultSettings, setPort) import Network.Wai.Handler.Warp (defaultSettings, setPort)
import Network.Wai.Handler.WarpTLS (runTLS, tlsSettings) import Network.Wai.Handler.WarpTLS (runTLS, tlsSettings)
@ -30,12 +31,12 @@ main = do
if j if j
then putStrLn "Connected to MongoDB!" then putStrLn "Connected to MongoDB!"
else fail "MongoDB connection failed!" else fail "MongoDB connection failed!"
_ <- forkIO (setInterval 60 (checkZcashPrices pipe (c_dbName loadedConfig))) {-_ <- forkIO (setInterval 60 (checkZcashPrices pipe (c_dbName loadedConfig)))-}
_ <- forkIO (setInterval 75 (scanZcash loadedConfig pipe)) {-_ <- forkIO (setInterval 75 (scanZcash loadedConfig pipe))-}
_ <- forkIO (setInterval 90 (scanPayments loadedConfig pipe)) {-_ <- forkIO (setInterval 90 (scanPayments loadedConfig pipe))-}
_ <- forkIO (setInterval 60 (checkPayments pipe (c_dbName loadedConfig))) {-_ <- forkIO (setInterval 60 (checkPayments pipe (c_dbName loadedConfig)))-}
_ <- forkIO (setInterval 60 (expireOwners pipe (c_dbName loadedConfig))) {-_ <- forkIO (setInterval 60 (expireOwners pipe (c_dbName loadedConfig)))-}
_ <- forkIO (setInterval 60 (updateLogins pipe loadedConfig)) {-_ <- forkIO (setInterval 60 (updateLogins pipe loadedConfig))-}
let appRoutes = routes pipe loadedConfig let appRoutes = routes pipe loadedConfig
case myTlsSettings of case myTlsSettings of
Nothing -> scotty (c_port loadedConfig) appRoutes Nothing -> scotty (c_port loadedConfig) appRoutes

31
app/Tasks.hs Normal file
View file

@ -0,0 +1,31 @@
{-# LANGUAGE OverloadedStrings #-}
module Tasks where
import Config
import Database.MongoDB
import ZGoBackend
main :: IO ()
main = do
putStrLn "ZGo Recurring Tasks"
putStrLn "Reading config..."
loadedConfig <- loadZGoConfig "zgo.cfg"
pipe <- connect $ host (c_dbHost loadedConfig)
j <-
access
pipe
master
(c_dbName loadedConfig)
(auth (c_dbUser loadedConfig) (c_dbPassword loadedConfig))
if j
then do
putStrLn "Connected to MongoDB!"
checkZcashPrices pipe (c_dbName loadedConfig)
scanZcash' loadedConfig pipe
scanPayments loadedConfig pipe
checkPayments pipe (c_dbName loadedConfig)
expireOwners pipe (c_dbName loadedConfig)
updateLogins pipe loadedConfig
close pipe
else fail "MongoDB connection failed!"

View file

@ -1,10 +1,10 @@
name: zgo-backend name: zgo-backend
version: 1.2.0 version: 1.3.0
git: "https://git.vergara.tech/Vergara_Tech/zgo-backend" git: "https://git.vergara.tech/Vergara_Tech/zgo-backend"
license: BOSL license: BOSL
author: "Rene Vergara" author: "Rene Vergara"
maintainer: "rene@vergara.network" maintainer: "rene@vergara.network"
copyright: "Copyright (c) 2022 Vergara Technologies LLC" copyright: "Copyright (c) 2023 Vergara Technologies LLC"
extra-source-files: extra-source-files:
- README.md - README.md
@ -58,6 +58,10 @@ library:
- blake3 - blake3
- memory - memory
- ghc-prim - ghc-prim
- network
- crypto-rng
- megaparsec
- uuid
executables: executables:
zgo-backend-exe: zgo-backend-exe:
@ -85,6 +89,7 @@ executables:
- configurator - configurator
- warp-tls - warp-tls
- warp - warp
- megaparsec
zgo-token-refresh: zgo-token-refresh:
main: TokenRefresh.hs main: TokenRefresh.hs
source-dirs: app source-dirs: app
@ -111,7 +116,25 @@ executables:
- configurator - configurator
- warp-tls - warp-tls
- warp - warp
- megaparsec
zgo-tasks:
main: Tasks.hs
source-dirs: app
ghc-options:
- -main-is Tasks
- -threaded
- -rtsopts
- -with-rtsopts=-N
- -Wall
dependencies:
- base
- mongoDB
- zgo-backend
- scotty
- warp-tls
- warp
- time
- megaparsec
tests: tests:
zgo-backend-test: zgo-backend-test:

View file

@ -6,6 +6,7 @@ import qualified Data.ByteString as BS
import Data.Configurator import Data.Configurator
import Data.SecureMem import Data.SecureMem
import qualified Data.Text as T import qualified Data.Text as T
import Network.Socket (PortNumber)
data Config = data Config =
Config Config
@ -21,6 +22,10 @@ data Config =
, c_useTls :: Bool , c_useTls :: Bool
, c_certificate :: String , c_certificate :: String
, c_key :: String , c_key :: String
, c_smtpHost :: String
, c_smtpPort :: Integer
, c_smtpUser :: String
, c_smtpPwd :: String
} }
deriving (Eq, Show) deriving (Eq, Show)
@ -39,6 +44,10 @@ loadZGoConfig path = do
useTls <- require config "tls" useTls <- require config "tls"
cert <- require config "certificate" cert <- require config "certificate"
key <- require config "key" key <- require config "key"
mailHost <- require config "smtpHost"
mailPort <- require config "smtpPort"
mailUser <- require config "smtpUser"
mailPwd <- require config "smtpPwd"
return $ return $
Config Config
dbHost dbHost
@ -53,3 +62,7 @@ loadZGoConfig path = do
useTls useTls
cert cert
key key
mailHost
mailPort
mailUser
mailPwd

91
src/LangComponent.hs Normal file
View file

@ -0,0 +1,91 @@
{-# LANGUAGE OverloadedStrings #-}
module LangComponent where
import Data.Aeson
import Data.Aeson.KeyMap
import qualified Data.Bson as B
import Data.ByteString.Builder.Extra (AllocationStrategy)
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE
import Database.MongoDB
import Xero (Xero(x_clientId))
-- | Type to represent a UI components text variables in different languages
data LangComponent =
LangComponent
{ lc_id :: Maybe ObjectId
, lc_lang :: T.Text
, lc_component :: T.Text
, lc_data :: Data.Aeson.Object
}
deriving (Show, Eq)
instance ToJSON LangComponent where
toJSON (LangComponent i l c d) =
case i of
Just oid ->
object
["_id" .= show oid, "language" .= l, "component" .= c, "data" .= d]
Nothing ->
object
[ "_id" .= ("" :: String)
, "language" .= l
, "component" .= c
, "data" .= d
]
instance FromJSON LangComponent where
parseJSON =
withObject "LangComponent" $ \obj -> do
l <- obj .: "language"
c <- obj .: "component"
d <- obj .: "data"
pure $ LangComponent Nothing l c d
instance Val LangComponent where
val (LangComponent i l c d) =
if isJust i
then Doc
[ "_id" =: i
, "language" =: l
, "component" =: c
, "data" =: (TL.toStrict . TLE.decodeUtf8 . encode) d
]
else Doc
[ "language" =: l
, "component" =: c
, "data" =: (TL.toStrict . TLE.decodeUtf8 . encode) d
]
cast' (Doc d) = do
i <- B.lookup "_id" d
l <- B.lookup "language" d
c <- B.lookup "component" d
dt <- B.lookup "data" d
pure $
LangComponent
i
l
c
(fromMaybe
Data.Aeson.KeyMap.empty
((decode . TLE.encodeUtf8 . TL.fromStrict) dt))
-- Database Actions
findLangComponent :: T.Text -> T.Text -> Action IO (Maybe Document)
findLangComponent lang component =
findOne (select ["language" =: lang, "component" =: component] "langcomps")
loadLangComponent :: LangComponent -> Action IO ()
loadLangComponent lc = do
let langComp = val lc
case langComp of
Doc x ->
upsert
(select
["language" =: lc_lang lc, "component" =: lc_component lc]
"langcomps")
x
_ -> error "Couldn't parse language JSON"

View file

@ -292,3 +292,12 @@ findOwner zaddy = findOne (select ["address" =: zaddy] "owners")
findOwnerById :: T.Text -> Action IO (Maybe Document) findOwnerById :: T.Text -> Action IO (Maybe Document)
findOwnerById i = findOwnerById i =
findOne (select ["_id" =: (read (T.unpack i) :: ObjectId)] "owners") findOne (select ["_id" =: (read (T.unpack i) :: ObjectId)] "owners")
-- | Function to find Owners about to expire
findExpiringOwners :: UTCTime -> Action IO [Document]
findExpiringOwners now =
rest =<<
find
(select
["paid" =: True, "expiration" =: ["$lte" =: addUTCTime 172800 now]]
"owners")

View file

@ -6,6 +6,8 @@ module User where
import Control.Monad import Control.Monad
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Crypto.RNG
import Crypto.RNG.Utils
import Data.Aeson import Data.Aeson
import qualified Data.Bson as B import qualified Data.Bson as B
import Data.Maybe import Data.Maybe
@ -99,11 +101,11 @@ validateUser session =
(select ["session" =: session] "users") (select ["session" =: session] "users")
["$set" =: ["validated" =: True]] ["$set" =: ["validated" =: True]]
generatePin :: Int -> IO T.Text generatePin :: IO String
generatePin s = do generatePin = do
let g = mkStdGen s rngState <- newCryptoRNGState
pure $ runCryptoRNGT rngState $
T.pack (padLeft (show . head $ randomRs (1 :: Integer, 10000000) g) '0' 7) randomString 7 ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
-- | Helper function to pad a string to a given length -- | Helper function to pad a string to a given length
padLeft :: String -> Char -> Int -> String padLeft :: String -> Char -> Int -> String

View file

@ -283,8 +283,7 @@ findToken a = findOne (select ["address" =: a] "xerotokens")
findExpiringTokens :: UTCTime -> Action IO [Document] findExpiringTokens :: UTCTime -> Action IO [Document]
findExpiringTokens now = findExpiringTokens now =
rest =<< rest =<<
find find (select ["refExpires" =: ["$lte" =: addUTCTime 172800 now]] "xerotokens")
(select ["refExpires" =: ["$lte" =: addUTCTime 1728000 now]] "xerotokens")
-- | Function to request accesstoken -- | Function to request accesstoken
requestXeroToken :: Pipe -> T.Text -> Xero -> T.Text -> T.Text -> IO Bool requestXeroToken :: Pipe -> T.Text -> Xero -> T.Text -> T.Text -> IO Bool
@ -411,8 +410,9 @@ getXeroInvoice pipe dbName inv address = do
Right iData -> return $ Just (head $ xir_invs iData) Right iData -> return $ Just (head $ xir_invs iData)
_ -> return Nothing _ -> return Nothing
payXeroInvoice :: Pipe -> T.Text -> T.Text -> T.Text -> Double -> IO () payXeroInvoice ::
payXeroInvoice pipe dbName inv address amt = do Pipe -> T.Text -> T.Text -> T.Text -> Double -> Double -> IO ()
payXeroInvoice pipe dbName inv address amt zec = do
token <- access pipe master dbName $ findToken address token <- access pipe master dbName $ findToken address
let aToken = t_access <$> (token >>= cast' . Doc) let aToken = t_access <$> (token >>= cast' . Doc)
let aCode = t_code <$> (token >>= cast' . Doc) let aCode = t_code <$> (token >>= cast' . Doc)
@ -427,7 +427,8 @@ payXeroInvoice pipe dbName inv address amt = do
[ "Invoice" .= object ["InvoiceNumber" .= inv] [ "Invoice" .= object ["InvoiceNumber" .= inv]
, "Account" .= object ["Code" .= fromMaybe "" aCode] , "Account" .= object ["Code" .= fromMaybe "" aCode]
, "Date" .= utctDay today , "Date" .= utctDay today
, "Reference" .= ("Paid in Zcash through ZGo" :: String) , "Reference" .=
("Paid in Zcash through ZGo: " ++ show zec ++ " ZEC" :: String)
, "Amount" .= amt , "Amount" .= amt
]) $ ]) $
addRequestHeader "Accept" "application/json" $ addRequestHeader "Accept" "application/json" $

View file

@ -6,6 +6,7 @@
module ZGoBackend where module ZGoBackend where
import qualified BLAKE3 as BLK
import Config import Config
import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent (forkIO, threadDelay)
import Control.Exception (try) import Control.Exception (try)
@ -14,6 +15,7 @@ import Control.Monad.IO.Class
import Data.Aeson import Data.Aeson
import Data.Array import Data.Array
import qualified Data.Bson as B import qualified Data.Bson as B
import qualified Data.ByteArray as BA
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as C import qualified Data.ByteString.Char8 as C
@ -31,6 +33,7 @@ import Data.Time.Clock
import Data.Time.Clock.POSIX import Data.Time.Clock.POSIX
import Data.Time.Format import Data.Time.Format
import Data.Typeable import Data.Typeable
import qualified Data.UUID as U
import qualified Data.Vector as V import qualified Data.Vector as V
import Data.Vector.Internal.Check (doChecks) import Data.Vector.Internal.Check (doChecks)
import Data.Word import Data.Word
@ -38,6 +41,7 @@ import Database.MongoDB
import Debug.Trace import Debug.Trace
import GHC.Generics import GHC.Generics
import Item import Item
import LangComponent
import Network.HTTP.Simple import Network.HTTP.Simple
import Network.HTTP.Types.Status import Network.HTTP.Types.Status
import Network.Wai (Request, pathInfo) import Network.Wai (Request, pathInfo)
@ -52,6 +56,7 @@ import System.Random
import Test.QuickCheck import Test.QuickCheck
import Test.QuickCheck.Instances import Test.QuickCheck.Instances
import Test.QuickCheck.Property (Result(ok)) import Test.QuickCheck.Property (Result(ok))
import Text.Megaparsec (runParser)
import Text.Regex import Text.Regex
import Text.Regex.Base import Text.Regex.Base
import User import User
@ -352,6 +357,27 @@ zToZGoTx (ZcashTx t a aZ bh bt c conf m) = do
ZGoTx Nothing nAddy sess conf bt a t m ZGoTx Nothing nAddy sess conf bt a t m
else ZGoTx Nothing "" "" conf bt a t m else ZGoTx Nothing "" "" conf bt a t m
zToZGoTx' :: Config -> Pipe -> ZcashTx -> IO ()
zToZGoTx' config pipe (ZcashTx t a aZ bh bt c conf m) = do
when (conf < 100) $ do
let zM = runParser pZGoMemo (T.unpack t) m
case zM of
Right zM' -> do
let tx =
ZGoTx
Nothing
(fromMaybe "" $ m_address zM')
(maybe "" U.toText $ m_session zM')
conf
bt
a
t
m
if m_payment zM'
then upsertPayment pipe (c_dbName config) tx
else access pipe master (c_dbName config) $ upsertZGoTx "txs" tx
Left e -> error "Failed to parse ZGo memo"
-- |Type to model a price in the ZGo database -- |Type to model a price in the ZGo database
data ZGoPrice = data ZGoPrice =
ZGoPrice ZGoPrice
@ -424,14 +450,20 @@ addUser _ _ _ _ _ Nothing = return () --`debug` "addUser got Nothing"
addUser nodeUser nodePwd p db node (Just tx) = do addUser nodeUser nodePwd p db node (Just tx) = do
isNew <- liftIO $ isUserNew p db tx isNew <- liftIO $ isUserNew p db tx
when isNew $ do when isNew $ do
let newPin = unsafePerformIO (generatePin (fromIntegral $ blocktime tx)) newPin <- liftIO generatePin
_ <- sendPin nodeUser nodePwd node (address tx) newPin _ <- sendPin nodeUser nodePwd node (address tx) (T.pack newPin)
let pinHash =
BLK.hash
[ BA.pack . BS.unpack . C.pack . T.unpack $
T.pack newPin <> session tx :: BA.Bytes
]
insert_ insert_
"users" "users"
[ "address" =: address tx [ "address" =: address tx
, "session" =: session tx , "session" =: session tx
, "blocktime" =: blocktime tx , "blocktime" =: blocktime tx
, "pin" =: newPin , "pin" =:
(T.pack . show $ (pinHash :: BLK.Digest BLK.DEFAULT_DIGEST_LEN))
, "validated" =: False , "validated" =: False
] ]
@ -790,6 +822,10 @@ routes pipe config = do
post "/api/validateuser" $ do post "/api/validateuser" $ do
providedPin <- param "pin" providedPin <- param "pin"
sess <- param "session" sess <- param "session"
let pinHash =
BLK.hash
[ BA.pack . BS.unpack . C.pack . T.unpack $ providedPin <> sess :: BA.Bytes
]
user <- liftAndCatchIO $ run (findUser sess) user <- liftAndCatchIO $ run (findUser sess)
case user of case user of
Nothing -> status noContent204 --`debug` "No user match" Nothing -> status noContent204 --`debug` "No user match"
@ -798,7 +834,10 @@ routes pipe config = do
case parsedUser of case parsedUser of
Nothing -> status noContent204 --`debug` "Couldn't parse user" Nothing -> status noContent204 --`debug` "Couldn't parse user"
Just pUser -> do Just pUser -> do
let ans = upin pUser == T.pack providedPin let ans =
upin pUser ==
(T.pack . show $
(pinHash :: BLK.Digest BLK.DEFAULT_DIGEST_LEN))
if ans if ans
then do then do
liftAndCatchIO $ run (validateUser sess) liftAndCatchIO $ run (validateUser sess)
@ -1010,6 +1049,23 @@ routes pipe config = do
oId <- param "id" oId <- param "id"
liftAndCatchIO $ run (deleteOrder oId) liftAndCatchIO $ run (deleteOrder oId)
status ok200 status ok200
-- Get language for component
get "/api/getlang" $ do
component <- param "component"
lang <- param "lang"
txtPack' <- liftAndCatchIO $ run (findLangComponent lang component)
let txtPack = cast' . Doc =<< txtPack'
case txtPack of
Nothing -> status noContent204
Just tP -> do
status ok200
Web.Scotty.json $ toJSON (tP :: LangComponent)
post "/api/setlang" $ do
langComp <- jsonData
_ <-
liftAndCatchIO $
mapM (run . loadLangComponent) (langComp :: [LangComponent])
status created201
-- | Make a Zcash RPC call -- | Make a Zcash RPC call
makeZcashCall :: makeZcashCall ::
@ -1106,9 +1162,19 @@ scanZcash config pipe = do
-- | Function to filter transactions -- | Function to filter transactions
isRelevant :: Text.Regex.Regex -> ZcashTx -> Bool isRelevant :: Text.Regex.Regex -> ZcashTx -> Bool
isRelevant re t isRelevant re t
| zconfirmations t < 10 && (matchTest re . T.unpack . zmemo) t = True | zconfirmations t < 100 && (matchTest re . T.unpack . zmemo) t = True
| otherwise = False | otherwise = False
-- | New function to scan transactions with parser
scanZcash' :: Config -> Pipe -> IO ()
scanZcash' config pipe = do
myTxs <-
listTxs (c_nodeUser config) (c_nodePwd config) (c_nodeAddress config) 1
case myTxs of
Right txs -> mapM_ (zToZGoTx' config pipe) txs
Left e -> do
putStrLn $ "Error scanning node transactions: " ++ T.unpack e
-- | Function to scan loaded viewing keys for payments -- | Function to scan loaded viewing keys for payments
scanPayments :: Config -> Pipe -> IO () scanPayments :: Config -> Pipe -> IO ()
scanPayments config pipe = do scanPayments config pipe = do
@ -1159,6 +1225,7 @@ scanPayments config pipe = do
(qexternalInvoice xO) (qexternalInvoice xO)
(qaddress xO) (qaddress xO)
(qtotal xO) (qtotal xO)
(qtotalZec xO)
"WC" -> do "WC" -> do
let wOwner = fst $ head sResult ! 2 let wOwner = fst $ head sResult ! 2
wooT <- wooT <-
@ -1261,7 +1328,10 @@ payOwner p d x =
let parsedUser = parseUserBson =<< user let parsedUser = parseUserBson =<< user
let zaddy = maybe "" uaddress parsedUser let zaddy = maybe "" uaddress parsedUser
owner <- access pipe master db $ findOwner zaddy owner <- access pipe master db $ findOwner zaddy
let ownerId = o_id =<< (cast' . Doc) =<< owner let foundOwner = (cast' . Doc) =<< owner
case foundOwner of
Nothing -> error "Couldn't find owner to mark as paid"
Just fOwn -> do
if pdelta pmt > 90000000 if pdelta pmt > 90000000
then do then do
_ <- _ <-
@ -1270,14 +1340,15 @@ payOwner p d x =
master master
db db
(modify (modify
(select ["_id" =: ownerId] "owners") (select ["_id" =: o_id fOwn] "owners")
[ "$set" =: [ "$set" =:
[ "paid" =: True [ "paid" =: True
, "invoices" =: True , "invoices" =: True
, "expiration" =: , "expiration" =:
posixSecondsToUTCTime calculateExpiration
(fromInteger fOwn
(pblocktime pmt + pdelta pmt - 90000000)) (pdelta pmt - 90000000)
(pblocktime pmt)
] ]
]) ])
markPaymentDone pipe db pmt markPaymentDone pipe db pmt
@ -1288,15 +1359,24 @@ payOwner p d x =
master master
db db
(modify (modify
(select ["_id" =: ownerId] "owners") (select ["_id" =: o_id fOwn] "owners")
[ "$set" =: [ "$set" =:
[ "paid" =: True [ "paid" =: True
, "expiration" =: , "expiration" =:
posixSecondsToUTCTime calculateExpiration
(fromInteger (pblocktime pmt + pdelta pmt)) fOwn
(pdelta pmt)
(pblocktime pmt)
] ]
]) ])
markPaymentDone pipe db pmt markPaymentDone pipe db pmt
calculateExpiration :: Owner -> Integer -> Integer -> UTCTime
calculateExpiration o delta blocktime =
if opaid o
then addUTCTime
(secondsToNominalDiffTime (fromIntegral delta))
(oexpiration o)
else posixSecondsToUTCTime (fromIntegral $ delta + blocktime)
expireOwners :: Pipe -> T.Text -> IO () expireOwners :: Pipe -> T.Text -> IO ()
expireOwners pipe db = do expireOwners pipe db = do

View file

@ -6,9 +6,15 @@ module ZGoTx where
import Data.Aeson import Data.Aeson
import qualified Data.Bson as B import qualified Data.Bson as B
import Data.Char
import Data.Maybe
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.UUID as U
import Data.Void
import Database.MongoDB import Database.MongoDB
import GHC.Generics import GHC.Generics
import Text.Megaparsec hiding (State)
import Text.Megaparsec.Char
-- | Type to model a ZGo transaction -- | Type to model a ZGo transaction
data ZGoTx = data ZGoTx =
@ -92,3 +98,100 @@ instance Val ZGoTx where
, "txid" =: t , "txid" =: t
, "memo" =: m , "memo" =: m
] ]
-- | Type to represent and parse ZGo memos
data ZGoMemo =
ZGoMemo
{ m_session :: Maybe U.UUID
, m_address :: Maybe T.Text
, m_payment :: Bool
}
deriving (Eq, Show)
data MemoToken
= Login !U.UUID
| PayMsg !U.UUID
| Address !T.Text
| Msg !T.Text
deriving (Show, Eq)
type Parser = Parsec Void T.Text
pSession :: Parser MemoToken
pSession = do
string "ZGO"
pay <- optional $ char 'p'
string "::"
s <- some $ hexDigitChar <|> char '-'
let u = U.fromString s
case u of
Nothing -> fail "Invalid UUID"
Just u' -> do
if isJust pay
then pure $ PayMsg u'
else pure $ Login u'
pSaplingAddress :: Parser MemoToken
pSaplingAddress = do
string "zs"
a <- some alphaNumChar
if length a /= 76
then fail "Failed to parse Sapling address"
else pure $ Address $ T.pack ("zs" <> a)
pMsg :: Parser MemoToken
pMsg = do
Msg . T.pack <$>
some
(alphaNumChar <|> punctuationChar <|> symbolChar <|>
charCategory OtherSymbol)
pMemo :: Parser MemoToken
pMemo = do
optional spaceChar
pSession <|> pSaplingAddress <|> pMsg
isMemoToken :: T.Text -> MemoToken -> Bool
isMemoToken kind t =
case kind of
"session" ->
case t of
PayMsg i -> True
Login j -> True
_ -> False
"address" ->
case t of
Address a -> True
_ -> False
"payment" ->
case t of
PayMsg i -> True
_ -> False
_ -> False
pZGoMemo :: Parser ZGoMemo
pZGoMemo = do
tks <- some pMemo
pure $ ZGoMemo (isSession tks) (isAddress tks) (isPayment tks)
where
isPayment [] = False
isPayment tks =
not (null tks) &&
case head tks of
PayMsg x -> True
_ -> isPayment $ tail tks
isAddress [] = Nothing
isAddress tks =
if not (null tks)
then case head tks of
Address x -> Just x
_ -> isAddress $ tail tks
else Nothing
isSession [] = Nothing
isSession tks =
if not (null tks)
then case head tks of
Login x -> Just x
PayMsg y -> Just y
_ -> isSession $ tail tks
else Nothing

View file

@ -17,7 +17,7 @@
# #
# resolver: ./custom-snapshot.yaml # resolver: ./custom-snapshot.yaml
# resolver: https://example.com/snapshots/2018-01-01.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml
resolver: lts-19.33 resolver: lts-20.8
#url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/4.yaml #url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/4.yaml
# User packages to be built. # User packages to be built.
@ -45,6 +45,7 @@ extra-deps:
- git: https://github.com/reach-sh/haskell-hexstring.git - git: https://github.com/reach-sh/haskell-hexstring.git
commit: 085c16fb21b9f856a435a3faab980e7e0b319341 commit: 085c16fb21b9f856a435a3faab980e7e0b319341
- blake3-0.2@sha256:d1146b9a51ccfbb0532780778b6d016a614e3d44c05d8c1923dde9a8be869045,2448 - blake3-0.2@sha256:d1146b9a51ccfbb0532780778b6d016a614e3d44c05d8c1923dde9a8be869045,2448
- crypto-rng-0.3.0.1@sha256:04f4ae75943ecad8b794950985054130f272d65a588b6b6528f56df0bfedc4dc,1565
# Override default flag values for local packages and extra-deps # Override default flag values for local packages and extra-deps
# flags: {} # flags: {}

View file

@ -22,9 +22,16 @@ packages:
size: 1433 size: 1433
original: original:
hackage: blake3-0.2@sha256:d1146b9a51ccfbb0532780778b6d016a614e3d44c05d8c1923dde9a8be869045,2448 hackage: blake3-0.2@sha256:d1146b9a51ccfbb0532780778b6d016a614e3d44c05d8c1923dde9a8be869045,2448
- completed:
hackage: crypto-rng-0.3.0.1@sha256:04f4ae75943ecad8b794950985054130f272d65a588b6b6528f56df0bfedc4dc,1565
pantry-tree:
sha256: 1caccafe35d1ae3063f057c31188742a8e794f4f4e4530bab4019c0a514ee54f
size: 455
original:
hackage: crypto-rng-0.3.0.1@sha256:04f4ae75943ecad8b794950985054130f272d65a588b6b6528f56df0bfedc4dc,1565
snapshots: snapshots:
- completed: - completed:
sha256: 6d1532d40621957a25bad5195bfca7938e8a06d923c91bc52aa0f3c41181f2d4 sha256: bfafe5735ccb74527d754b1f9999ded72d7c3a6c3a88529449661431ccfbd6cc
size: 619204 size: 649327
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/33.yaml url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/8.yaml
original: lts-19.33 original: lts-20.8

View file

@ -19,6 +19,7 @@ import Data.Time.Clock
import Data.Time.Clock.POSIX import Data.Time.Clock.POSIX
import Database.MongoDB import Database.MongoDB
import Item import Item
import LangComponent
import Network.HTTP.Simple import Network.HTTP.Simple
import Network.HTTP.Types.Status import Network.HTTP.Types.Status
import Order import Order
@ -139,7 +140,8 @@ main = do
"\128737MSG\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e\n\nZGOp::ad8477d3-4fdd-4c97-90b2-76630b5f77e1" "\128737MSG\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e\n\nZGOp::ad8477d3-4fdd-4c97-90b2-76630b5f77e1"
describe "PIN generator" $ do describe "PIN generator" $ do
it "should give a 7 digit" $ do it "should give a 7 digit" $ do
length (T.unpack (unsafePerformIO (generatePin 1010))) `shouldBe` 7 pin <- generatePin
length pin `shouldBe` 7
describe "API endpoints" $ do describe "API endpoints" $ do
beforeAll_ (startAPI loadedConfig) $ do beforeAll_ (startAPI loadedConfig) $ do
describe "Price endpoint" $ do describe "Price endpoint" $ do
@ -256,7 +258,7 @@ main = do
req <- req <-
testPost testPost
"/api/wootoken" "/api/wootoken"
[("ownerid", Just "627ad3492b05a76be5000001")] [("ownerid", Just "627ad3492b05a76be3000001")]
res <- httpLBS req res <- httpLBS req
getResponseStatus res `shouldBe` accepted202 getResponseStatus res `shouldBe` accepted202
it "authenticate with incorrect owner" $ do it "authenticate with incorrect owner" $ do
@ -264,7 +266,9 @@ main = do
testPublicGet testPublicGet
"/auth" "/auth"
[ ("ownerid", Just "62cca13f5530331e2a900001") [ ("ownerid", Just "62cca13f5530331e2a900001")
, ("token", Just "89bd9d8d69a674e0f467cc8796ed151a") , ( "token"
, Just
"0c1702c16c7bd7e075b8bb129b24888a5cc2181fa1eb4ce9190cfcb625ecf0ee")
, ("siteurl", Just "aHR0cHM6Ly93d3cudGVjcHJvdmFsLmNvbS8") , ("siteurl", Just "aHR0cHM6Ly93d3cudGVjcHJvdmFsLmNvbS8")
] ]
res <- httpJSON req res <- httpJSON req
@ -284,7 +288,9 @@ main = do
testPublicGet testPublicGet
"/auth" "/auth"
[ ("ownerid", Just "627ad3492b05a76be3000001") [ ("ownerid", Just "627ad3492b05a76be3000001")
, ("token", Just "89bd9d8d69a674e0f467cc8796ed151a") , ( "token"
, Just
"0c1702c16c7bd7e075b8bb129b24888a5cc2181fa1eb4ce9190cfcb625ecf0ee")
, ("siteurl", Just "aHR0cHM6Ly93d3cudGVjcHJvdmFsLmNvbS8") , ("siteurl", Just "aHR0cHM6Ly93d3cudGVjcHJvdmFsLmNvbS8")
] ]
res <- httpJSON req res <- httpJSON req
@ -294,7 +300,9 @@ main = do
testPublicGet testPublicGet
"/auth" "/auth"
[ ("ownerid", Just "627ad3492b05a76be3000001") [ ("ownerid", Just "627ad3492b05a76be3000001")
, ("token", Just "89bd9d8d69a674e0f467cc8796ed151a") , ( "token"
, Just
"0c1702c16c7bd7e075b8bb129b24888a5cc2181fa1eb4ce9190cfcb625ecf0ee")
, ("siteurl", Just "aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8") , ("siteurl", Just "aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8")
] ]
res <- httpJSON req res <- httpJSON req
@ -304,7 +312,9 @@ main = do
testPublicGet testPublicGet
"/woopayment" "/woopayment"
[ ("ownerid", Just "627ad3492b05a76be3000001") [ ("ownerid", Just "627ad3492b05a76be3000001")
, ("token", Just "89bd9d8d69a674e0f467cc8796ed151a") , ( "token"
, Just
"0c1702c16c7bd7e075b8bb129b24888a5cc2181fa1eb4ce9190cfcb625ecf0ee")
, ("siteurl", Just "aHR0cHM6Ly93d3cudGVjcHJvdmFsLmNvbS8") , ("siteurl", Just "aHR0cHM6Ly93d3cudGVjcHJvdmFsLmNvbS8")
, ("order_id", Just "1234") , ("order_id", Just "1234")
, ("currency", Just "usd") , ("currency", Just "usd")
@ -314,6 +324,28 @@ main = do
] ]
res <- httpJSON req res <- httpJSON req
getResponseStatus (res :: Response A.Value) `shouldBe` ok200 getResponseStatus (res :: Response A.Value) `shouldBe` ok200
describe "Language endpoint" $ do
it "existing component" $ do
req <-
testGet
"/api/getlang"
[("lang", Just "en-US"), ("component", Just "login")]
res <- httpJSON req
getResponseStatus (res :: Response A.Value) `shouldBe` ok200
it "wrong component" $ do
req <-
testGet
"/api/getlang"
[("lang", Just "en-US"), ("component", Just "test")]
res <- httpLBS req
getResponseStatus res `shouldBe` noContent204
it "wrong language" $ do
req <-
testGet
"/api/getlang"
[("lang", Just "fr-FR"), ("component", Just "login")]
res <- httpLBS req
getResponseStatus res `shouldBe` noContent204
around handleDb $ around handleDb $
describe "Database actions" $ do describe "Database actions" $ do
describe "authentication" $ do describe "authentication" $ do
@ -441,7 +473,7 @@ main = do
let s = parseZGoTxBson =<< t let s = parseZGoTxBson =<< t
let conf = maybe 0 confirmations s let conf = maybe 0 confirmations s
conf `shouldSatisfy` (> 0) conf `shouldSatisfy` (> 0)
xit "payments are added to db" $ \p -> do it "payments are added to db" $ \p -> do
_ <- _ <-
access access
p p
@ -454,7 +486,7 @@ main = do
let s = (cast' . Doc) =<< t let s = (cast' . Doc) =<< t
let payDelta = maybe 0 pdelta s let payDelta = maybe 0 pdelta s
payDelta `shouldSatisfy` (> 0) payDelta `shouldSatisfy` (> 0)
xit "owners are marked as paid" $ \p -> do it "owners are marked as paid" $ \p -> do
let myUser = let myUser =
User User
(Just (read "6272a90f2b05a74cf1000002" :: ObjectId)) (Just (read "6272a90f2b05a74cf1000002" :: ObjectId))
@ -658,7 +690,7 @@ startAPI config = do
"zs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e" "zs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e"
"35bfb9c2-9ad2-4fe5-adda-99d63b8dcdcd" "35bfb9c2-9ad2-4fe5-adda-99d63b8dcdcd"
1613487 1613487
"1234567" "8ea140fbb30615d6cae383c4f62f3ad9afb10b804f63138d5b53990bd56e0162"
False False
_ <- _ <-
access access
@ -697,34 +729,7 @@ startAPI config = do
True True
False False
False False
(UTCTime (fromGregorian 2022 4 16) (secondsToDiffTime 0)) (UTCTime (fromGregorian 2023 2 6) (secondsToDiffTime 0))
False
""
""
let myOwner1 =
Owner
(Just (read "627ad3492b05a76be5000001"))
"zs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e"
"Test shop"
"usd"
False
0
False
0
"Bubba"
"Gibou"
"bubba@zgo.cash"
"1 Main St"
"Mpls"
"Minnesota"
"55401"
""
"bubbarocks.io"
"United States"
True
False
False
(UTCTime (fromGregorian 2022 4 16) (secondsToDiffTime 0))
False False
"" ""
"" ""
@ -765,16 +770,16 @@ startAPI config = do
case itemTest of case itemTest of
Doc iT -> access pipe master "test" (insert_ "items" iT) Doc iT -> access pipe master "test" (insert_ "items" iT)
_ -> fail "Couldn't save test Item in DB" _ -> fail "Couldn't save test Item in DB"
let myWooToken = --let myWooToken =
WooToken --WooToken
Nothing --Nothing
(read "627ad3492b05a76be3000001") --(read "627ad3492b05a76be3000001")
"89bd9d8d69a674e0f467cc8796ed151a" --"89bd9d8d69a674e0f467cc8796ed151a"
Nothing --Nothing
let wooTest = val myWooToken --let wooTest = val myWooToken
case wooTest of --case wooTest of
Doc wT -> access pipe master "test" (insert_ "wootokens" wT) --Doc wT -> access pipe master "test" (insert_ "wootokens" wT)
_ -> fail "Couldn't save test WooToken in DB" --_ -> fail "Couldn't save test WooToken in DB"
threadDelay 1000000 threadDelay 1000000
putStrLn "Test server is up!" putStrLn "Test server is up!"

View file

@ -5,13 +5,13 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: zgo-backend name: zgo-backend
version: 1.1.1 version: 1.3.0
synopsis: Haskell Back-end for the ZGo point-of-sale application synopsis: Haskell Back-end for the ZGo point-of-sale application
description: Please see the README on GitLab at <https://gitlab.com/pitmutt/zgo-backend#readme> description: Please see the README at <https://git.vergara.tech/Vergara_Tech//zgo-backend#readme>
category: Web category: Web
author: Rene Vergara author: Rene Vergara
maintainer: rene@vergara.network maintainer: rene@vergara.network
copyright: Copyright (c) 2022 Vergara Technologies LLC copyright: Copyright (c) 2023 Vergara Technologies LLC
license: BOSL license: BOSL
license-file: LICENSE license-file: LICENSE
build-type: Simple build-type: Simple
@ -22,12 +22,13 @@ extra-source-files:
source-repository head source-repository head
type: git type: git
location: https://gitlab.com/pitmutt/zgo-backend location: https://git.vergara.tech/Vergara_Tech/zgo-backend
library library
exposed-modules: exposed-modules:
Config Config
Item Item
LangComponent
Order Order
Owner Owner
Payment Payment
@ -51,13 +52,16 @@ library
, bytestring , bytestring
, configurator , configurator
, containers , containers
, crypto-rng
, ghc-prim , ghc-prim
, hexstring , hexstring
, http-conduit , http-conduit
, http-types , http-types
, jwt , jwt
, megaparsec
, memory , memory
, mongoDB , mongoDB
, network
, quickcheck-instances , quickcheck-instances
, random , random
, regex-base , regex-base
@ -68,6 +72,7 @@ library
, text , text
, time , time
, unordered-containers , unordered-containers
, uuid
, vector , vector
, wai , wai
, wai-cors , wai-cors
@ -78,6 +83,7 @@ library
executable zgo-backend-exe executable zgo-backend-exe
main-is: Server.hs main-is: Server.hs
other-modules: other-modules:
Tasks
TokenRefresh TokenRefresh
Paths_zgo_backend Paths_zgo_backend
hs-source-dirs: hs-source-dirs:
@ -90,6 +96,7 @@ executable zgo-backend-exe
, configurator , configurator
, http-conduit , http-conduit
, http-types , http-types
, megaparsec
, mongoDB , mongoDB
, scotty , scotty
, securemem , securemem
@ -101,10 +108,31 @@ executable zgo-backend-exe
, zgo-backend , zgo-backend
default-language: Haskell2010 default-language: Haskell2010
executable zgo-tasks
main-is: Tasks.hs
other-modules:
Server
TokenRefresh
Paths_zgo_backend
hs-source-dirs:
app
ghc-options: -main-is Tasks -threaded -rtsopts -with-rtsopts=-N -Wall
build-depends:
base
, megaparsec
, mongoDB
, scotty
, time
, warp
, warp-tls
, zgo-backend
default-language: Haskell2010
executable zgo-token-refresh executable zgo-token-refresh
main-is: TokenRefresh.hs main-is: TokenRefresh.hs
other-modules: other-modules:
Server Server
Tasks
Paths_zgo_backend Paths_zgo_backend
hs-source-dirs: hs-source-dirs:
app app
@ -116,6 +144,7 @@ executable zgo-token-refresh
, configurator , configurator
, http-conduit , http-conduit
, http-types , http-types
, megaparsec
, mongoDB , mongoDB
, scotty , scotty
, securemem , securemem

View file

@ -10,3 +10,7 @@ port = 3000
tls = false tls = false
certificate = "/path/to/cert.pem" certificate = "/path/to/cert.pem"
key = "/path/to/key.pem" key = "/path/to/key.pem"
smtpHost = "127.0.0.1"
smtpPort = 1025
smtpUser = "contact@zgo.cash"
smtpPwd = "uib3K8BkCPexl_wr5bYfrg"

View file

@ -10,3 +10,7 @@ port = 3000
tls = false tls = false
certificate = "/path/to/cert.pem" certificate = "/path/to/cert.pem"
key = "/path/to/key.pem" key = "/path/to/key.pem"
smtpHost = "127.0.0.1"
smtpPort = 1025
smtpUser = "contact@zgo.cash"
smtpPwd = "uib3K8BkCPexl_wr5bYfrg"