Complete update to develop branch, commit:b4df0cefc2e8122e8bb55e72a2016cfb7a9ea12f

This commit is contained in:
Petar Bozin 2016-03-20 21:42:08 +01:00
parent c3936b3151
commit 40df926919
62 changed files with 16683 additions and 740 deletions

View File

@ -91,9 +91,9 @@ Address: ________________________
Us
________________________
Name: Simon Redfern
Title: CEO TESOBE / Music Pictures Ltd
Title: CEO TESOBE Ltd
Address: Osloerstrasse 16/17
Berlin 13359, German
Berlin 13359, Germany
@ -125,4 +125,4 @@ Address: Osloerstrasse 16/17
This work is licensed under a Creative Commons Attribution 3.0 Unported License.
This work is licensed under a Creative Commons Attribution 3.0 Unported License.

View File

@ -26,8 +26,7 @@ Please refer to the [wiki](https://github.com/OpenBankProject/OBP-API/wiki) to s
## LICENSE
This project is dual licensed under the AGPL V3 (see NOTICE) and a commercial license from TESOBE
Some files (OAuth related) are licensed under the Apache 2 license.
This project is dual licensed under the AGPL V3 (see NOTICE) and commercial licenses from TESOBE Ltd.
## SETUP
@ -85,7 +84,33 @@ To compile and run jetty, install Maven 3 and execute:
----
## Ubuntu
If you use Ubuntu (or a derivate) and encrypted home directories (e.g. you have ~/.Private), you might run into the following error when the project is built:
uncaught exception during compilation: java.io.IOException
[ERROR] File name too long
[ERROR] two errors found
[DEBUG] Compilation failed (CompilerInterface)
The current workaround is to move the project directory onto a different partition, e.g. under /opt/ .
# Databases:
The default database for testing etc is H2. PostgreSQL is used for the sandboxes (user accounts, metadata, transaction cache).
# Kafka (optional):
If Kafka connector is selected in props (connector=kafka), Kafka and Zookeeper have to be installed, as well as OBP-Kafka-Python (which can be either running from command-propmpt or from inside Docker container):
* Kafka and Zookeeper can be installed using system's default installer or by unpacking the archives (http://apache.mirrors.spacedump.net/kafka/ and http://apache.mirrors.spacedump.net/zookeeper/)
* OBP-Kafka-Python can be downloaded from https://github.com/OpenBankProject/OBP-Kafka-Python
# Lift
# We use liftweb http://www.liftweb.net/
# Advanced architecture: http://exploring.liftweb.net/master/index-9.html

View File

@ -161,6 +161,11 @@
<artifactId>pegdown</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>com.jason-goodwin</groupId>
<artifactId>authentikat-jwt_2.10</artifactId>
<version>0.4.1</version>
</dependency>
</dependencies>
<build>
@ -224,7 +229,7 @@
<jvmArgs>
<jvmArg>-DpackageLinkDefs=file://${project.build.directory}/packageLinkDefs.properties</jvmArg>
</jvmArgs>
<scalaVersion>${scala.version}</scalaVersion>
<scalaVersion>${scala.compiler}</scalaVersion>
<scalaCompatVersion>${scala.version}</scalaCompatVersion>
<recompileMode>incremental</recompileMode>
<useZincServer>true</useZincServer>

View File

@ -38,14 +38,8 @@ object LiftProjectBuild extends Build {
}).toMap
private lazy val PropertiesExpr = """.*\$\{(.*?)\}.*""".r
private lazy val TwoPropertiesExpr = """.*\$\{(.*?)\}.*\$\{(.*?)\}.*""".r
def populateProps(t: String) = t match {
case TwoPropertiesExpr(p, q) => {
t
.replace("${"+p+"}", pomProperties.get(p).getOrElse(throw new Exception("Cannot find property: '" + p + "' required by: '" + t + "' in: " + pomFile)))
.replace("${"+q+"}", pomProperties.get(q).getOrElse(throw new Exception("Cannot find property: '" + q + "' required by: '" + t + "' in: " + pomFile)))
}
case PropertiesExpr(p) => {
val replaceWith = pomProperties.get(p)
t.replace("${"+p+"}", replaceWith.getOrElse(throw new Exception("Cannot find property: '" + p + "' required by: '" + t + "' in: " + pomFile)))

View File

@ -30,7 +30,7 @@ db.url=jdbc:postgresql://localhost:5432/dbname?user=dbusername&password=thepassw
#our own remotely accessible URL
#this is needed for oauth to work. it's important to access the api over this url, e.g.
# if this is 127.0.0.1 don't use localhost to access it.
# (this needs to be an URL)
# (this needs to be a URL)
hostname=http://127.0.0.1:8080
#this is only useful for running the api locally via RunWebApp
@ -38,6 +38,14 @@ hostname=http://127.0.0.1:8080
#if you want to change the port when running via the command line, use "mvn -Djetty.port=8089 jetty:run" instead
dev.port=8080
#The start of the api path (before the version)
#It is *strongly* recommended not to change this - since Apps will be expecting the api at /obp/+version
#Including it here so we have a canonical source of the value
#This was introduced March 2016, some code might use hardcoded value instead.
#Default value is obp (highly recomended)
apiPathZero=obp
#sending mail out
#not need in dev mode, but important for production
mail.api.consumer.registered.sender.address=no-reply@example.com
@ -88,7 +96,7 @@ connection.password=thepassword
importer_secret=change_me
#set this to true if you want to have the api send a message to the hbci project to refresh transactions for an account
messageQueue.updateBankAccountsTransaction=true
messageQueue.updateBankAccountsTransaction=false
#the minimum time between updates in hours
messageQueue.updateTransactionsInterval=1
@ -114,13 +122,25 @@ webui_index_page_about_section_text = <p class="about-text"> \
</p>
# API Explorer url. Change this to the instance of Sofi linked to this API. e.g. sofi.openbankproject.com/api-explorer
webui_api_explorer_url = http://sofi.openbankproject.com/api-explorer
# API Explorer url. Change to your instance
webui_api_explorer_url = http://apiexplorer.openbankproject.com
# Sofi url. (AKA Social Finance) Change to your instance
webui_sofi_url = http://sofi.openbankproject.com
# Starting page of documentation. Change this if you have a specific landing page.
webui_api_documentation_url = https://github.com/OpenBankProject/OBP-API/wiki
# To display a custom message above the username / password box
# We currently use this to display example customer login in sandbox etc.
webui_login_page_special_instructions=
# Link for SDKs
webui_sdks_url = https://github.com/OpenBankProject/OBP-API/wiki/OAuth-Client-SDKS
## For partner logos and links
webui_main_partners=[\
{"logoUrl":"http://www.example.com/images/logo.png", "homePageUrl":"http://www.example.com", "altText":"Example 1"},\
@ -131,3 +151,13 @@ webui_main_style_sheet = /media/css/website.css
# Override certain elements (with important styles)
webui_override_style_sheet =
## API Options
apiOptions.getBranchesIsPublic = true
apiOptions.getAtmsIsPublic = true
apiOptions.getProductsIsPublic = true
# Default Bank. Incase the server wants to support a default bank so developers don't have to specify BANK_ID
# e.g. developers could use /my/accounts as well as /my/banks/BANK_ID/accounts
defaultBank.bank_id=THE_DEFAULT_BANK_ID

View File

@ -31,41 +31,45 @@ Berlin 13359, Germany
*/
package bootstrap.liftweb
import java.io.{File, FileInputStream}
import java.util.Locale
import javax.mail.internet.MimeMessage
import code.api.ResourceDocs1_4_0.ResourceDocs
import code.api._
import code.api.sandbox.SandboxApiCalls
import code.atms.MappedAtm
import code.branches.MappedBranch
import code.crm.MappedCrmEvent
import code.management.ImporterAPI
import code.management.AccountsAPI
import code.customer.{MappedCustomer, MappedCustomerMessage}
import code.kycdocuments.MappedKycDocument
import code.kycmedias.MappedKycMedia
import code.kycchecks.MappedKycCheck
import code.kycstatuses.MappedKycStatus
import code.socialmedia.MappedSocialMedia
import code.management.{AccountsAPI, ImporterAPI}
import code.metadata.comments.MappedComment
import code.metadata.counterparties.{MappedCounterpartyWhereTag, MappedCounterpartyMetadata}
import code.metadata.counterparties.{MappedCounterpartyMetadata, MappedCounterpartyWhereTag}
import code.metadata.narrative.MappedNarrative
import code.metadata.tags.MappedTag
import code.metadata.transactionimages.MappedTransactionImage
import code.metadata.wheretags.MappedWhereTag
import code.metrics.MappedMetric
import code.branches.{MappedBranch}
import code.atms.{MappedAtm}
import code.customer.{MappedCustomerMessage, MappedCustomer}
import code.products.MappedProduct
import code.tesobe.CashAccountAPI
import code.transactionrequests.MappedTransactionRequest
import net.liftweb._
import util._
import common._
import http._
import sitemap._
import Loc._
import mapper._
import net.liftweb.util.Helpers._
import net.liftweb.util.Schedule
import net.liftweb.util.Helpers
import java.util.Locale
import java.io.FileInputStream
import java.io.File
import javax.mail.internet.MimeMessage
import code.model._
import code.model.dataAccess._
import code.api._
import code.products.MappedProduct
import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks}
import code.tesobe.CashAccountAPI
import code.transactionrequests.MappedTransactionRequest
import net.liftweb.common._
import net.liftweb.http._
import net.liftweb.mapper._
import net.liftweb.sitemap.Loc._
import net.liftweb.sitemap._
import net.liftweb.util.Helpers._
import net.liftweb.util.{Helpers, Schedule, _}
import code.api.Constant._
/**
* A class that's instantiated early and run. It allows the application
@ -175,6 +179,7 @@ class Boot extends Loggable{
}
logger.info("running mode: " + runningMode)
logger.info(s"ApiPathZero (the bit before version) is $ApiPathZero")
// where to search snippets
LiftRules.addToPackages("code")
@ -182,13 +187,20 @@ class Boot extends Loggable{
//OAuth API call
LiftRules.statelessDispatch.append(OAuthHandshake)
// JWT auth endpoints
if(Props.getBool("allow_direct_login", true)) {
LiftRules.statelessDispatch.append(DirectLogin)
}
// Add the various API versions
LiftRules.statelessDispatch.append(v1_0.OBPAPI1_0)
LiftRules.statelessDispatch.append(v1_1.OBPAPI1_1)
LiftRules.statelessDispatch.append(v1_2.OBPAPI1_2)
// Can we depreciate the above?
LiftRules.statelessDispatch.append(v1_2_1.OBPAPI1_2_1)
LiftRules.statelessDispatch.append(v1_3_0.OBPAPI1_3_0)
LiftRules.statelessDispatch.append(v1_4_0.OBPAPI1_4_0)
LiftRules.statelessDispatch.append(v2_0_0.OBPAPI2_0_0)
//add management apis
LiftRules.statelessDispatch.append(ImporterAPI)
@ -198,6 +210,11 @@ class Boot extends Loggable{
LiftRules.statelessDispatch.append(CashAccountAPI)
LiftRules.statelessDispatch.append(BankMockAPI)
// Add Resource Docs
LiftRules.statelessDispatch.append(ResourceDocs)
// LiftRules.statelessDispatch.append(Metrics) TODO: see metric menu entry below
//add sandbox api calls only if we're running in sandbox mode
@ -282,13 +299,13 @@ class Boot extends Loggable{
// Make a transaction span the whole HTTP request
S.addAround(DB.buildLoanWrapper)
val useMessageQueue = Props.getBool("messageQueue.createBankAccounts", false)
if(useMessageQueue)
try {
try {
val useMessageQueue = Props.getBool("messageQueue.createBankAccounts", false)
if(useMessageQueue)
BankAccountCreationListener.startListen
} catch {
case e: java.lang.ExceptionInInitializerError => logger.warn(s"BankAccountCreationListener Exception: $e")
}
} catch {
case e: java.lang.ExceptionInInitializerError => logger.warn(s"BankAccountCreationListener Exception: $e")
}
Mailer.devModeSend.default.set( (m : MimeMessage) => {
logger.info("Would have sent email if not in dev mode: " + m.getContent)
@ -327,8 +344,8 @@ class Boot extends Loggable{
}
private def sendExceptionEmail(exception: Throwable): Unit = {
import Mailer.{From, PlainMailBodyType, Subject, To}
import net.liftweb.util.Helpers.now
import Mailer.{From, To, Subject, PlainMailBodyType}
val outputStream = new java.io.ByteArrayOutputStream
val printStream = new java.io.PrintStream(outputStream)
@ -390,5 +407,10 @@ object ToSchemify {
MappedBranch,
MappedAtm,
MappedProduct,
MappedCrmEvent)
MappedCrmEvent,
MappedKycDocument,
MappedKycMedia,
MappedKycCheck,
MappedKycStatus,
MappedSocialMedia)
}

View File

@ -28,7 +28,7 @@ Berlin 13359, Germany
Everett Sochowski : everett AT tesobe DOT com
Ayoub Benali: ayoub AT tesobe DOT com
*/
*/
package code.api
@ -43,6 +43,8 @@ import code.model.User
import code.api.OAuthHandshake._
import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.Extraction
import net.liftweb.util.Props
import code.api.Constant._
trait APIFailure{
val msg : String
@ -61,7 +63,7 @@ object APIFailure {
//that all stable versions retain the same behavior
case class UserNotFound(providerId : String, userId: String) extends APIFailure {
val responseCode = 400 //TODO: better as 404? -> would break some backwards compatibility (or at least the tests!)
//to reiterate the comment about preserving backwards compatibility:
//consider the case that an app may be parsing this string to decide what message to show their users
//e.g. when granting view permissions, an app may not give their users a choice of provider and only
@ -79,7 +81,8 @@ trait OBPRestHelper extends RestHelper with Loggable {
val VERSION : String
def vPlusVersion = "v" + VERSION
def apiPrefix = ("obp" / vPlusVersion).oPrefix(_)
def apiPrefix = (ApiPathZero / vPlusVersion).oPrefix(_)
/*
An implicit function to convert magically between a Boxed JsonResponse and a JsonResponse
@ -135,16 +138,25 @@ trait OBPRestHelper extends RestHelper with Loggable {
case Failure(msg, _, _) => errorJsonResponse(msg)
case _ => errorJsonResponse("oauth error")
}
} else fn(Empty)
} else if (Props.getBool("allow_direct_login", true) && isThereDirectLoginHeader) {
val user = DirectLogin.getUser
if (user.isDefined) {
fn(user)
} else {
fn(Empty)
}
} else {
fn(Empty)
}
}
class RichStringList(list: List[String]) {
val listLen = list.length
/**
* Normally we would use ListServeMagic's prefix function, but it works with PartialFunction[Req, () => Box[LiftResponse]]
* instead of the PartialFunction[Req, Box[User] => Box[JsonResponse]] that we need. This function does the same thing, really.
*/
* Normally we would use ListServeMagic's prefix function, but it works with PartialFunction[Req, () => Box[LiftResponse]]
* instead of the PartialFunction[Req, Box[User] => Box[JsonResponse]] that we need. This function does the same thing, really.
*/
def oPrefix(pf: PartialFunction[Req, Box[User] => Box[JsonResponse]]): PartialFunction[Req, Box[User] => Box[JsonResponse]] =
new PartialFunction[Req, Box[User] => Box[JsonResponse]] {
def isDefinedAt(req: Req): Boolean =

View File

@ -0,0 +1,23 @@
package code.api.ResourceDocs1_4_0
import code.api.OBPRestHelper
import net.liftweb.common.Loggable
object ResourceDocs extends OBPRestHelper with ResourceDocsAPIMethods with Loggable {
val VERSION = "1.4.0"
val routes = List(
ImplementationsResourceDocs.getResourceDocsObp,
ImplementationsResourceDocs.getResourceDocsSwagger
)
routes.foreach(route => {
oauthServe(apiPrefix{route})
})
}

View File

@ -0,0 +1,236 @@
package code.api.ResourceDocs1_4_0
import code.api.v1_4_0.{APIMethods140, JSONFactory1_4_0, OBPAPI1_4_0}
import net.liftweb.common.{Box, Full, Loggable}
import net.liftweb.http.rest.RestHelper
import net.liftweb.http.{JsonResponse, Req}
import net.liftweb.json.Extraction
import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.JsonDSL._
import net.liftweb.util.Props
import scala.collection.immutable.Nil
// JObject creation
import code.api.v1_2_1.{APIInfoJSON, APIMethods121, HostedBy, OBPAPI1_2_1}
import code.api.v1_3_0.{APIMethods130, OBPAPI1_3_0}
import code.api.v2_0_0.{APIMethods200, OBPAPI2_0_0}
import scala.collection.mutable.ArrayBuffer
// So we can include resource docs from future versions
import java.text.SimpleDateFormat
import code.api.util.APIUtil.{ResourceDoc, _}
import code.model._
trait ResourceDocsAPIMethods extends Loggable with APIMethods200 with APIMethods140 with APIMethods130 with APIMethods121{
//needs to be a RestHelper to get access to JsonGet, JsonPost, etc.
// We add previous APIMethods so we have access to the Resource Docs
self: RestHelper =>
val ImplementationsResourceDocs = new Object() {
val resourceDocs = ArrayBuffer[ResourceDoc]()
val emptyObjectJson : JValue = Nil
val apiVersion : String = "1_4_0"
val exampleDateString : String ="22/08/2013"
val simpleDateFormat : SimpleDateFormat = new SimpleDateFormat("dd/mm/yyyy")
val exampleDate = simpleDateFormat.parse(exampleDateString)
def getResourceDocsList(requestedApiVersion : String) : Option[List[ResourceDoc]] =
{
// Return a different list of resource docs depending on the version being called.
// For instance 1_3_0 will have the docs for 1_3_0 and 1_2_1 (when we started adding resource docs) etc.
logger.info(s"getResourceDocsList says requestedApiVersion is $requestedApiVersion")
val resourceDocs = requestedApiVersion match {
case "2.0.0" => Implementations2_0_0.resourceDocs ++ Implementations1_4_0.resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs
case "1.4.0" => Implementations1_4_0.resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs
case "1.3.0" => Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs
case "1.2.1" => Implementations1_2_1.resourceDocs
}
logger.info(s"There are ${resourceDocs.length} resource docs available to $requestedApiVersion")
val versionRoutes = requestedApiVersion match {
case "2.0.0" => OBPAPI2_0_0.routes
case "1.4.0" => OBPAPI1_4_0.routes
case "1.3.0" => OBPAPI1_3_0.routes
case "1.2.1" => OBPAPI1_2_1.routes
}
logger.info(s"There are ${versionRoutes.length} routes available to $requestedApiVersion")
// We only want the resource docs for which a API route exists else users will see 404s
// Get a list of the partial function classes represented in the routes available to this version.
val versionRoutesClasses = versionRoutes.map { vr => vr.getClass }
// Only return the resource docs that have available routes
val activeResourceDocs = resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass))
logger.info(s"There are ${activeResourceDocs.length} resource docs available to $requestedApiVersion")
// Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that.
Some(activeResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb)))
}
resourceDocs += ResourceDoc(
getResourceDocsObp,
apiVersion,
"getResourceDocsObp",
"GET",
"/resource-docs/obp",
"Get Resource Documentation in OBP format.",
"""Returns documentation about the RESTful resources on this server including example body for POST or PUT requests.
| Thus the OBP API Explorer (and other apps) can display and work with the API documentation.
| In the future this information will be used to create Swagger (WIP) and RAML files.
|<ul>
|<li> operation_id is concatenation of version and function and should be unque (the aim of this is to allow links to code) </li>
|<li> version references the version that the API call is defined in.</li>
|<li> function is the (scala) function.</li>
|<li> request_url is empty for the root call, else the path.</li>
|<li> summary is a short description inline with the swagger terminology. </li>
|<li> description may contain html markup (generated from markdown on the server).</li>
|</ul>
""",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil,
false,
false,
List(apiTagApiInfo)
)
// Provides resource documents so that API Explorer (or other apps) can display API documentation
// Note: description uses html markup because original markdown doesn't easily support "_" and there are multiple versions of markdown.
// TODO constrain version?
// strip the leading v
def cleanApiVersionString (version: String) : String = {version.stripPrefix("v").stripPrefix("V")}
def getResourceDocsObp : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "resource-docs" :: requestedApiVersion :: "obp" :: Nil JsonGet _ => {
user => {
for {
rd <- getResourceDocsList(cleanApiVersionString(requestedApiVersion))
} yield {
// Format the data as json
val json = JSONFactory1_4_0.createResourceDocsJson(rd)
// Return
successJsonResponse(Extraction.decompose(json))
}
}
}
}
resourceDocs += ResourceDoc(
getResourceDocsSwagger,
apiVersion,
"getResourceDocsSwagger",
"GET",
"/resource-docs/swagger",
"Get Resource Documentation in Swagger format. Work In Progress!",
"""Returns documentation about the RESTful resources on this server in Swagger format.
| Currently this is incomplete.
""",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil,
false,
false,
List(apiTagApiInfo)
)
def getResourceDocsSwagger : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "resource-docs" :: requestedApiVersion :: "swagger" :: Nil JsonGet _ => {
user => {
for {
rd <- getResourceDocsList(cleanApiVersionString(requestedApiVersion))
} yield {
// Format the data as json
val json = SwaggerJSONFactory.createSwaggerResourceDoc(rd)
// Return
successJsonResponse(Extraction.decompose(json))
}
}
}
}
if (Props.devMode) {
resourceDocs += ResourceDoc(
dummy(apiVersion),
apiVersion,
"testResourceDoc",
"GET",
"/dummy",
"I am only a test resource Doc",
"""
|
|#This should be H1
|
|##This should be H2
|
|###This should be H3
|
|####This should be H4
|
|Here is a list with two items:
|
|* One
|* Two
|
|There are underscores by them selves _
|
|There are _underscores_ around a word
|
|There are underscores_in_words
|
|There are 'underscores_in_words_inside_quotes'
|
|There are (underscores_in_words_in_brackets)
|
|_etc_...""",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil,
false,
false,
List(apiTagApiInfo))
}
def dummy(apiVersion : String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "dummy" :: Nil JsonGet json => {
user =>
val apiDetails: JValue = {
val hostedBy = new HostedBy("TESOBE", "contact@tesobe.com", "+49 (0)30 8145 3994")
val apiInfoJSON = new APIInfoJSON(apiVersion, gitCommit, hostedBy)
Extraction.decompose(apiInfoJSON)
}
Full(successJsonResponse(apiDetails, 200))
}
}
}
}

View File

@ -0,0 +1,88 @@
package code.api.ResourceDocs1_4_0
import code.api.util.APIUtil.ResourceDoc
import code.api.Constant._
import net.liftweb.json._
import net.liftweb.util.Props
import scala.collection.immutable.ListMap
object SwaggerJSONFactory {
case class ContactJson(
name: String,
url: String
)
case class InfoJson(
title: String,
description: String,
contact: ContactJson,
version: String
)
case class ResponseObjectSchemaJson(`$ref`: String)
case class ResponseObjectJson(description: Option[String], schema: Option[ResponseObjectSchemaJson])
case class MethodJson(tags: List[String],
summary: String,
operationId: String,
responses: Map[String, ResponseObjectJson])
case class PathsJson(get: MethodJson)
case class MessageJson(`type`: String)
case class CodeJson(`type`: String, format: String)
case class PropertiesJson(code: CodeJson, message: MessageJson)
case class ErrorDefinitionJson(`type`: String, required: List[String], properties: PropertiesJson)
case class DefinitionsJson(Error: ErrorDefinitionJson)
case class SwaggerResourceDoc(swagger: String,
info: InfoJson,
host: String,
basePath: String,
schemes: List[String],
paths: Map[String, Map[String, MethodJson]],
definitions: DefinitionsJson
)
def createSwaggerResourceDoc(resourceDocList: List[ResourceDoc]): SwaggerResourceDoc = {
implicit val formats = DefaultFormats
val contact = ContactJson("OBP", "https://openbankproject.com/")
val apiVersion = "v1.4.0"
val title = "Open Bank Project API"
val description = "An open source API for banks."
val info = InfoJson(title, description, contact, apiVersion)
// TODO check / improve host, basePath and version
val host = Props.get("hostname", "unknown host").replaceFirst("http://", "")
val basePath = s"/$ApiPathZero/" + apiVersion
val schemas = List("http")
val paths: ListMap[String, Map[String, MethodJson]] = resourceDocList.groupBy(x => x.requestUrl).toSeq.sortBy(x => x._1).map { mrd =>
val methods: Map[String, MethodJson] = mrd._2.map(rd =>
(rd.requestVerb,
MethodJson(
List(s"${rd.apiVersion.toString}"),
rd.summary,
s"${rd.apiVersion.toString}-${rd.apiFunction.toString}",
Map("200" -> ResponseObjectJson(Some("Success") , None), "400" -> ResponseObjectJson(Some("Error"), Some(ResponseObjectSchemaJson("#/definitions/Error"))))))
).toMap
(mrd._1, methods.toSeq.sortBy(m => m._1).toMap)
}(collection.breakOut)
val `type` = "object"
val required = List("code", "message")
val code = CodeJson("integer", "int32")
val message = MessageJson("string")
val properties = PropertiesJson(code, message)
val errorDef = ErrorDefinitionJson(`type`, required, properties)
val defs = DefinitionsJson(errorDef)
SwaggerResourceDoc("2.0", info, host, basePath, schemas, paths, defs)
}
}

View File

@ -0,0 +1,17 @@
package code.api
import code.api.util.ErrorMessages
import net.liftweb.common.Loggable
import net.liftweb.util.Props
// Note: Import this with: import code.api.Constant._
object Constant extends Loggable {
logger.info("Instantiating Constants")
final val HostName = Props.get("hostname").openOrThrowException(ErrorMessages.HostnameNotSpecified)
// This is the part before the version. Do not change this default!
final val ApiPathZero = Props.get("apiPathZero", "obp")
}

View File

@ -0,0 +1,313 @@
/**
Open Bank Project
Copyright 2011,2012 TESOBE / Music Pictures Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Open Bank Project (http://www.openbankproject.com)
Copyright 2011,2012 TESOBE / Music Pictures Ltd
This product includes software developed at
TESOBE (http://www.tesobe.com/)
by
Simon Redfern : simon AT tesobe DOT com
Everett Sochowski: everett AT tesobe DOT com
Ayoub Benali : ayoub AT tesobe Dot com
*/
package code.api
import java.util.Date
import authentikat.jwt.{JsonWebToken, JwtClaimsSet, JwtHeader}
import code.api.util.APIUtil._
import code.model.dataAccess.OBPUser
import code.model.{Consumer, Token, TokenType, User}
import net.liftweb.common._
import net.liftweb.http._
import net.liftweb.http.rest.RestHelper
import net.liftweb.json.Extraction
import net.liftweb.mapper.By
import net.liftweb.util.{Props, Helpers}
import net.liftweb.util.Helpers._
import scala.compat.Platform
import code.api.util.ErrorMessages
/**
* This object provides the API calls necessary to
* authenticate users using JSON Web Tokens (http://jwt.io).
*/
object JSONFactory {
case class TokenJSON( token : String )
def stringOrNull(text: String) =
if (text == null || text.isEmpty)
null
else
text
def stringOptionOrNull(text: Option[String]) =
text match {
case Some(t) => stringOrNull(t)
case _ => null
}
def createTokenJSON(token: String): TokenJSON = {
new TokenJSON(
stringOrNull(token)
)
}
}
object DirectLogin extends RestHelper with Loggable {
// Our version of serve
def dlServe(handler : PartialFunction[Req, JsonResponse]) : Unit = {
val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = {
new PartialFunction[Req, () => Box[LiftResponse]] {
def apply(r : Req) = {
handler(r)
}
def isDefinedAt(r : Req) = handler.isDefinedAt(r)
}
}
super.serve(obpHandler)
}
dlServe
{
//Handling get request for a token
case Req("my" :: "logins" :: "direct" :: Nil,_ , PostRequest|GetRequest) => {
//Extract the directLogin parameters from the header and test if the request is valid
var (httpCode, message, directLoginParameters) = validator("authorizationToken", getHttpMethod)
//Test if the request is valid
if(httpCode==200)
{
val claims = Map("" -> "")
val (token,secret) = generateTokenAndSecret(claims)
//Save the token that we have generated
if(saveAuthorizationToken(directLoginParameters,token, secret))
message = token
else
message = "invalid"
}
val tokenJSON = JSONFactory.createTokenJSON(message)
successJsonResponse(Extraction.decompose(tokenJSON))
}
}
def getHttpMethod = S.request match {
case Full(s) => s.post_? match {
case true => "POST"
case _ => "GET"
}
case _ => "ERROR"
}
//Check if the request (access token or request token) is valid and return a tuple
def validator(requestType : String, httpMethod : String) : (Int, String, Map[String,String]) = {
//return a Map containing the directLogin parameters : prameter -> value
def getAllParameters: Map[String, String] = {
def toMap(parametersList: String) = {
//transform the string "directLogin_prameter="value""
//to a tuple (directLogin_parameter,Decoded(value))
def dynamicListExtract(input: String) = {
val directLoginPossibleParameters =
List(
"consumer_key",
"token",
"username",
"password"
)
if (input contains "=") {
val split = input.split("=", 2)
val parameterValue = split(1).replace("\"", "")
//add only OAuth parameters and not empty
if (directLoginPossibleParameters.contains(split(0)) && !parameterValue.isEmpty)
Some(split(0), parameterValue) // return key , value
else
None
}
else
None
}
//we delete the "DirectLogin" prefix and all the white spaces that may exist in the string
val cleanedParameterList = parametersList.stripPrefix("DirectLogin").replaceAll("\\s", "")
val params = Map(cleanedParameterList.split(",").flatMap(dynamicListExtract _): _*)
params
}
S.request match {
case Full(a) => a.header("Authorization") match {
case Full(header) => {
if (header.contains("DirectLogin"))
toMap(header)
else
Map("error" -> "header incorrect")
}
case _ => Map("error" -> "missing header")
}
case _ => Map("error" -> "request incorrect")
}
}
def registeredApplication(consumerKey: String): Boolean = {
Consumer.find(By(Consumer.key, consumerKey)) match {
case Full(application) => application.isActive
case _ => false
}
}
def validAccessToken(tokenKey: String) = {
Token.find(By(Token.key, tokenKey), By(Token.tokenType, TokenType.Access)) match {
case Full(token) => token.isValid
case _ => false
}
}
//@return the missing parameters depending of the request type
def missingDirectLoginParameters(parameters: Map[String, String], requestType: String): Set[String] = {
requestType match {
case "authorizationToken" =>
("username" :: "password" :: "consumer_key" :: List()).toSet diff parameters.keySet
case "protectedResource" =>
("token" :: List()).toSet diff parameters.keySet
case _ =>
parameters.keySet
}
}
var message = ""
var httpCode: Int = 500
val parameters = getAllParameters
//are all the necessary directLogin parameters present?
val missingParams = missingDirectLoginParameters(parameters, requestType)
if (missingParams.nonEmpty) {
message = ErrorMessages.DirectLoginMissingParameters + missingParams.mkString(", ")
httpCode = 400
}
else if (
requestType == "protectedResource" &&
!validAccessToken(parameters.get("token").getOrElse(""))
) {
message = ErrorMessages.DirectLoginInvalidToken + parameters.get("token").getOrElse("")
httpCode = 401
}
//check if the application is registered and active
else if (
requestType == "authorizationToken" &&
Props.getBool("direct_login_consumer_key_mandatory", true) &&
!registeredApplication(parameters.get("consumer_key").getOrElse(""))) {
logger.error("application: " + parameters.get("consumer_key").getOrElse("") + " not found")
message = ErrorMessages.InvalidLoginCredentials
httpCode = 401
}
//checking if the signature is correct
//else if(! correctSignature(parameters, httpMethod))
//{
// message = "Invalid signature"
// httpCode = 401
//}
else
httpCode = 200
if(message.nonEmpty)
logger.error("error message : " + message)
(httpCode, message, parameters)
}
private def generateTokenAndSecret(claims: Map[String,String]) =
{
// generate random string
val secret_message = Helpers.randomString(40)
// jwt header
val header = JwtHeader("HS256")
// generate jwt token
val token_message = JsonWebToken(header, JwtClaimsSet(claims), secret_message)
(token_message, secret_message)
}
private def saveAuthorizationToken(directLoginParameters : Map[String, String], tokenKey : String, tokenSecret : String) =
{
import code.model.{Token, TokenType}
val token = Token.create
token.tokenType(TokenType.Access)
Consumer.find(By(Consumer.key, directLoginParameters.get("consumer_key").getOrElse(""))) match {
case Full(consumer) => token.consumerId(consumer.id)
case _ => None
}
val username = directLoginParameters.get("username").getOrElse("").toString
val password = directLoginParameters.get("password").getOrElse("") match {
case p: String => p
case _ => "error"
}
val userId = OBPUser.getUserId(username, password)
token.userForeignKey(userId)
token.key(tokenKey)
token.secret(tokenSecret)
val currentTime = Platform.currentTime
val tokenDuration : Long = Helpers.weeks(4)
token.duration(tokenDuration)
token.expirationDate(new Date(currentTime+tokenDuration))
token.insertDate(new Date(currentTime))
val tokenSaved = token.save()
tokenSaved
}
def getUser : Box[User] = {
val httpMethod = S.request match {
case Full(r) => r.request.method
case _ => "GET"
}
val (httpCode, message, directLoginParameters) = validator("protectedResource", httpMethod)
val user = getUser(200, directLoginParameters.get("token"))
if (user != Empty ) {
val res = Full(user.get)
res
} else {
ParamFailure(message, Empty, Empty, APIFailure(message, httpCode))
}
}
def getUser(httpCode : Int, tokenID : Box[String]) : Box[User] =
if(httpCode==200)
{
import code.model.Token
logger.info("DirectLogin header correct ")
Token.find(By(Token.key, tokenID.getOrElse(""))) match {
case Full(token) => {
logger.info("access token: "+ token + " found")
val user = token.user
//just a log
user match {
case Full(u) => logger.info("user " + u.emailAddress + " was found from the DirectLogin token")
case _ => logger.info("no user was found for the DirectLogin token")
}
user
}
case _ =>{
logger.warn("no token " + tokenID.getOrElse("") + " found")
Empty
}
}
}
else
Empty
}

View File

@ -250,7 +250,7 @@ object OAuthHandshake extends RestHelper with Loggable {
parameters
}
//prepare the base string
//prepare the base string (should we really have openOr here?)
var baseString = httpMethod+"&"+URLEncoder.encode(Props.get("hostname").openOr(S.hostAndPath) + S.uri,"UTF-8")+"&"
baseString+= generateOAuthParametersString(OAuthparameters)

File diff suppressed because it is too large Load Diff

View File

@ -32,25 +32,67 @@ Berlin 13359, Germany
package code.api.util
import code.api.util.APIUtil.ApiLink
import code.api.v1_2.ErrorMessage
import code.metrics.APIMetrics
import net.liftweb.common.{Full, Loggable}
import net.liftweb.http.{JsonResponse, S}
import code.model._
import net.liftweb.common.{Box, Full, Loggable}
import net.liftweb.http.js.JE.JsRaw
import net.liftweb.http.js.JsExp
import net.liftweb.http.{JsonResponse, Req, S}
import net.liftweb.json.Extraction
import net.liftweb.json.JsonAST.{JObject, JValue}
import net.liftweb.json._
import net.liftweb.json.JsonAST.JValue
import net.liftweb.util.Helpers._
import bootstrap.liftweb.Boot
import net.liftweb.util.Props
import scala.collection.JavaConversions.asScalaSet
import scala.collection.mutable.ArrayBuffer
import net.liftweb.http.CurrentReq
import code.api.Constant._
object ErrorMessages {
// Infrastructure / config messages
val HostnameNotSpecified = "OBP-00001: Hostname not specified. Could not get hostname from Props. Please edit your props file. Here are some example settings: hostname=http://127.0.0.1:8080 or hostname=https://www.example.com"
// General messages
val InvalidJsonFormat = "OBP-10001: Incorrect json format."
val InvalidNumber = "OBP-10002: Invalid Number. Could not convert value to a number."
val InvalidInitalBalance = "OBP-10003: Invalid Number. Initial balance must be a number, e.g 1000.00"
// Authentication / Authorisation messages
val UserNotLoggedIn = "OBP-20001: User not logged in. Authentication is required!"
val DirectLoginMissingParameters = "OBP-20002: These DirectLogin parameters are missing: "
val DirectLoginInvalidToken = "OBP-20003: This DirectLogin token is invalid or expired: "
val InvalidLoginCredentials = "OBP-20004: Invalid login credentials. Check username/password."
// Resource related messages
val BankNotFound = "OBP-30001: Bank not found. Please specify a valid value for BANK_ID."
val CustomerNotFound = "OBP-30002: Customer not found. Please specify a valid value for CUSTOMER_NUMBER."
val AccountNotFound = "OBP-30003: Account not found. Please specify a valid value for ACCOUNT_ID."
val CounterpartyNotFound = "OBP-30004: Counterparty not found."
val ViewNotFound = "OBP-30005: View not found for Account. Please specify a valid value for VIEW_ID"
}
object APIUtil extends Loggable {
@ -64,6 +106,16 @@ object APIUtil extends Loggable {
case _ => "GET"
}
def isThereDirectLoginHeader : Boolean = {
S.request match {
case Full(a) => a.header("Authorization") match {
case Full(parameters) => parameters.contains("DirectLogin")
case _ => false
}
case _ => false
}
}
def isThereAnOAuthHeader : Boolean = {
S.request match {
case Full(a) => a.header("Authorization") match {
@ -76,6 +128,7 @@ object APIUtil extends Loggable {
def logAPICall = {
if(Props.getBool("write_metrics", false)) {
// TODO This should use Elastic Search or Kafka not an RDBMS
APIMetrics.apiMetrics.vend.saveMetric(S.uriAndQueryString.getOrElse(""), (now: TimeSpan))
}
}
@ -132,7 +185,7 @@ object APIUtil extends Loggable {
import org.apache.http.protocol.HTTP.UTF_8
import scala.collection.Map
import scala.collection.immutable.{TreeMap, Map => IMap}
import scala.collection.immutable.{Map => IMap, TreeMap}
import scala.collection.mutable.Set
case class Consumer(key: String, secret: String)
@ -261,16 +314,243 @@ object APIUtil extends Loggable {
}
/*
Used to document API calls / resources. correct place for this?
Used to document API calls / resources.
TODO Can we extract apiVersion, apiFunction, requestVerb and requestUrl from partialFunction?
*/
// Used to tag Resource Docs
case class ResourceDocTag(tag: String)
val apiTagPayment = ResourceDocTag("Payments")
val apiTagApiInfo = ResourceDocTag("APIInfo")
val apiTagBanks = ResourceDocTag("Banks")
val apiTagAccounts = ResourceDocTag("Accounts")
val apiTagPublicData = ResourceDocTag("PublicData")
val apiTagPrivateData = ResourceDocTag("PrivateData")
val apiTagTransactions = ResourceDocTag("Transactions")
val apiTagMetaData = ResourceDocTag("Meta Data")
val apiTagViews = ResourceDocTag("Views")
val apiTagEntitlements = ResourceDocTag("Entitlements")
val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired")
val apiTagCounterparties = ResourceDocTag("Counterparties")
val apiTagKyc = ResourceDocTag("KYC")
val apiTagCustomer = ResourceDocTag("Customer")
// Used to document the API calls
case class ResourceDoc(
apiVersion: String, // TODO: Constrain to certain strings?
apiFunction: String, // The partial function that implements this resource. Could use it to link to the source code that implements the call
requestVerb: String, // GET, POST etc. TODO: Constrain to GET, POST etc.
requestUrl: String, // The URL (not including /obp/vX.X). Starts with / No trailing slash. TODO Constrain the string?
summary: String, // A summary of the call (originally taken from code comment) SHOULD be under 120 chars to be inline with Swagger
description: String, // Longer description (originally taken from github wiki)
exampleRequestBody: JValue, // An example of the body required (maybe empty)
successResponseBody: JValue, // A successful response body
errorResponseBodies: List[JValue]) // Possible error responses
partialFunction : PartialFunction[Req, Box[User] => Box[JsonResponse]],
apiVersion: String, // TODO: Constrain to certain strings?
apiFunction: String, // The partial function that implements this resource. Could use it to link to the source code that implements the call
requestVerb: String, // GET, POST etc. TODO: Constrain to GET, POST etc.
requestUrl: String, // The URL (not including /obp/vX.X). Starts with / No trailing slash. TODO Constrain the string?
summary: String, // A summary of the call (originally taken from code comment) SHOULD be under 120 chars to be inline with Swagger
description: String, // Longer description (originally taken from github wiki)
exampleRequestBody: JValue, // An example of the body required (maybe empty)
successResponseBody: JValue, // A successful response body
errorResponseBodies: List[JValue], // Possible error responses
isCore: Boolean,
isPSD2: Boolean,
tags: List[ResourceDocTag]
)
// Define relations between API end points. Used to create _links in the JSON and maybe later for API Explorer browsing
case class ApiRelation(
fromPF : PartialFunction[Req, Box[User] => Box[JsonResponse]],
toPF : PartialFunction[Req, Box[User] => Box[JsonResponse]],
rel : String
)
// Populated from Resource Doc and ApiRelation
case class InternalApiLink(
fromPF : PartialFunction[Req, Box[User] => Box[JsonResponse]],
toPF : PartialFunction[Req, Box[User] => Box[JsonResponse]],
rel : String,
requestUrl: String
)
// Used to pass context of current API call to the function that generates links for related Api calls.
case class DataContext(
user : Box[User],
bankId : Option[BankId],
accountId: Option[AccountId],
viewId: Option[ViewId],
counterpartyId: Option[CounterpartyId],
transactionId: Option[TransactionId]
)
case class CallerContext(
caller : PartialFunction[Req, Box[User] => Box[JsonResponse]]
)
case class CodeContext(
resourceDocsArrayBuffer : ArrayBuffer[ResourceDoc],
relationsArrayBuffer : ArrayBuffer[ApiRelation]
)
case class ApiLink(
rel: String,
href: String
)
case class LinksJSON(
_links: List[ApiLink]
)
case class ResultAndLinksJSON(
result : JValue,
_links: List[ApiLink]
)
def createResultAndLinksJSON(result : JValue, links : List[ApiLink] ) : ResultAndLinksJSON = {
new ResultAndLinksJSON(
result,
links
)
}
def authenticationRequiredMessage(authRequired: Boolean) : String =
authRequired match {
case true => "Authentication IS required"
case false => "Authentication is NOT required"
}
def apiVersionWithV(apiVersion : String) : String = {
// TODO Define a list of supported versions (put in Constant) and constrain the input
// Append v and replace _ with .
s"v${apiVersion.replaceAll("_",".")}"
}
def fullBaseUrl : String = {
val crv = CurrentReq.value
val apiPathZeroFromRequest = crv.path.partPath(0)
if (apiPathZeroFromRequest != ApiPathZero) throw new Exception("Configured ApiPathZero is not the same as the actual.")
val path = s"$HostName/$ApiPathZero"
path
}
// Modify URL replacing placeholders for Ids
def contextModifiedUrl(url: String, context: DataContext) = {
// Potentially replace BANK_ID
val url2: String = context.bankId match {
case Some(x) => url.replaceAll("BANK_ID", x.value)
case _ => url
}
val url3: String = context.accountId match {
// Take care *not* to change OTHER_ACCOUNT_ID HERE
case Some(x) => url2.replaceAll("/ACCOUNT_ID", s"/${x.value}").replaceAll("COUNTERPARTY_ID", x.value)
case _ => url2
}
val url4: String = context.viewId match {
case Some(x) => url3.replaceAll("VIEW_ID", {x.value})
case _ => url3
}
val url5: String = context.counterpartyId match {
// Change OTHER_ACCOUNT_ID or COUNTERPARTY_ID
case Some(x) => url4.replaceAll("OTHER_ACCOUNT_ID", x.value).replaceAll("COUNTERPARTY_ID", x.value)
case _ => url4
}
val url6: String = context.transactionId match {
case Some(x) => url5.replaceAll("TRANSACTION_ID", x.value)
case _ => url5
}
// Add host, port, prefix, version.
// not correct because call could be in other version
val fullUrl = s"$fullBaseUrl$url6"
fullUrl
}
def getApiLinkTemplates(callerContext: CallerContext,
codeContext: CodeContext
) : List[InternalApiLink] = {
// Relations of the API version where the caller is defined.
val relations = codeContext.relationsArrayBuffer.toList
// Resource Docs
// Note: This doesn't allow linking to calls in earlier versions of the API
// TODO: Fix me
val resourceDocs = codeContext.resourceDocsArrayBuffer
val pf = callerContext.caller
val internalApiLinks: List[InternalApiLink] = for {
relation <- relations.filter(r => r.fromPF == pf)
toResourceDoc <- resourceDocs.find(rd => rd.partialFunction == relation.toPF)
}
yield new InternalApiLink(
pf,
toResourceDoc.partialFunction,
relation.rel,
// Add the vVersion to the documented url
s"/${apiVersionWithV(toResourceDoc.apiVersion)}${toResourceDoc.requestUrl}"
)
internalApiLinks
}
// This is not currently including "templated" attribute
def halLinkFragment (link: ApiLink) : String = {
"\"" + link.rel +"\": { \"href\": \"" +link.href + "\" }"
}
// Since HAL links can't be represented via a case class, (they have dynamic attributes rather than a list) we need to generate them here.
def buildHalLinks(links: List[ApiLink]): JValue = {
val halLinksString = links match {
case head :: tail => tail.foldLeft("{"){(r: String, c: ApiLink) => ( r + " " + halLinkFragment(c) + " ," ) } + halLinkFragment(head) + "}"
case Nil => "{}"
}
parse(halLinksString)
}
// Returns API links (a list of them) that have placeholders (e.g. BANK_ID) replaced by values (e.g. ulster-bank)
def getApiLinks(callerContext: CallerContext, codeContext: CodeContext, dataContext: DataContext) : List[ApiLink] = {
val templates = getApiLinkTemplates(callerContext, codeContext)
// Replace place holders in the urls like BANK_ID with the current value e.g. 'ulster-bank' and return as ApiLinks for external consumption
val links = templates.map(i => ApiLink(i.rel,
contextModifiedUrl(i.requestUrl, dataContext) )
)
links
}
// Returns links formatted at objects.
def getHalLinks(callerContext: CallerContext, codeContext: CodeContext, dataContext: DataContext) : JValue = {
val links = getApiLinks(callerContext, codeContext, dataContext)
getHalLinksFromApiLinks(links)
}
def getHalLinksFromApiLinks(links: List[ApiLink]) : JValue = {
val halLinksJson = buildHalLinks(links)
halLinksJson
}
}

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,8 @@ import net.liftweb.json.JsonAST._
import net.liftweb.common.Loggable
import code.api.OBPRestHelper
// Added so we can add resource docs for this version of the API
object OBPAPI1_2_1 extends OBPRestHelper with APIMethods121 with Loggable {

View File

@ -30,6 +30,7 @@ trait APIMethods130 {
resourceDocs += ResourceDoc(
getCards,
apiVersion,
"getCards",
"GET",
@ -38,7 +39,10 @@ trait APIMethods130 {
"Returns data about all the physical cards a user has been issued. These could be debit cards, credit cards, etc.",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
true,
true,
Nil)
lazy val getCards : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "cards" :: Nil JsonGet _ => {
@ -58,6 +62,7 @@ trait APIMethods130 {
resourceDocs += ResourceDoc(
getCardsForBank,
apiVersion,
"getCardsForBank",
"GET",
@ -66,7 +71,10 @@ trait APIMethods130 {
"",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
false,
false,
Nil)
def getCardsForBank : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {

View File

@ -4,6 +4,9 @@ import code.api.OBPRestHelper
import code.api.v1_2_1.APIMethods121
import net.liftweb.common.Loggable
// Added so we can add resource docs for this version of the API
//has APIMethods121 as all api calls that went unchanged from 1.2.1 to 1.3.0 will use the old
//implementation
object OBPAPI1_3_0 extends OBPRestHelper with APIMethods130 with APIMethods121 with Loggable {

View File

@ -3,7 +3,9 @@ package code.api.v1_4_0
import java.text.SimpleDateFormat
import java.util.Date
import code.api.v1_4_0.JSONFactory1_4_0._
import code.bankconnectors.Connector
import code.metadata.comments.MappedComment
import code.transactionrequests.TransactionRequests.{TransactionRequestBody, TransactionRequestAccount}
import net.liftweb.common.{Failure, Loggable, Box, Full}
import net.liftweb.http.js.JE.JsRaw
@ -16,15 +18,21 @@ import net.liftweb.json.JsonDSL._
import net.liftweb.util.Props
import net.liftweb.json.JsonAST.JValue
import code.api.v1_2_1.{AmountOfMoneyJSON}
import scala.collection.immutable.Nil
// JObject creation
import collection.mutable.ArrayBuffer
import code.api.APIFailure
import code.api.v1_2_1.APIMethods121
import code.api.v1_3_0.APIMethods130
import code.api.v1_4_0.JSONFactory1_4_0._
import code.api.v1_2_1.{OBPAPI1_2_1, APIInfoJSON, HostedBy, APIMethods121}
import code.api.v1_3_0.{OBPAPI1_3_0, APIMethods130}
//import code.api.v2_0_0.{OBPAPI2_0_0, APIMethods200}
// So we can include resource docs from future versions
//import code.api.v1_4_0.JSONFactory1_4_0._
import code.atms.Atms
import code.branches.Branches
import code.crm.CrmEvent
@ -32,10 +40,15 @@ import code.customer.{MockCustomerFaceImage, CustomerMessages, Customer}
import code.model._
import code.products.Products
import code.api.util.APIUtil._
import code.api.util.ErrorMessages
import code.util.Helper._
import code.api.util.APIUtil.ResourceDoc
import java.text.SimpleDateFormat
import code.api.util.APIUtil.authenticationRequiredMessage
trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
//needs to be a RestHelper to get access to JsonGet, JsonPost, etc.
// We add previous APIMethods so we have access to the Resource Docs
@ -52,15 +65,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
val exampleDate = simpleDateFormat.parse(exampleDateString)
def getResourceDocsList : Option[List[ResourceDoc]] =
{
// Get the Resource Docs for this and previous versions of the API
val cumulativeResourceDocs = resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs
// Sort by endpoint, verb. Thus / is shown first then /accounts and /banks etc. Seems to read quite well like that.
Some(cumulativeResourceDocs.toList.sortBy(rd => (rd.requestUrl, rd.requestVerb)))
}
resourceDocs += ResourceDoc(
getCustomer,
apiVersion,
"getCustomer",
"GET",
@ -71,14 +77,17 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
|Authentication via OAuth is required.""",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
false,
false,
List(apiTagCustomer))
lazy val getCustomer : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "customer" :: Nil JsonGet _ => {
user => {
for {
u <- user ?~! "User must be logged in to retrieve Customer"
bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
u <- user ?~! ErrorMessages.UserNotLoggedIn
bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
info <- Customer.customerProvider.vend.getCustomer(bankId, u) ?~ "No customer information found for current user"
} yield {
val json = JSONFactory1_4_0.createCustomerJson(info)
@ -89,6 +98,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
}
resourceDocs += ResourceDoc(
getCustomerMessages,
apiVersion,
"getCustomerMessages",
"GET",
@ -99,14 +109,17 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
|Authentication via OAuth is required.""",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
false,
false,
List(apiTagCustomer))
lazy val getCustomerMessages : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "customer" :: "messages" :: Nil JsonGet _ => {
user => {
for {
u <- user ?~! "User must be logged in to retrieve customer messages"
bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
u <- user ?~! ErrorMessages.UserNotLoggedIn
bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
//au <- APIUser.find(By(APIUser.id, u.apiId))
//role <- au.isCustomerMessageAdmin ~> APIFailure("User does not have sufficient permissions", 401)
} yield {
@ -119,6 +132,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
}
resourceDocs += ResourceDoc(
addCustomerMessage,
apiVersion,
"addCustomerMessage",
"POST",
@ -128,16 +142,18 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
// We use Extraction.decompose to convert to json
Extraction.decompose(AddCustomerMessageJson("message to send", "from department", "from person")),
emptyObjectJson,
emptyObjectJson :: Nil
emptyObjectJson :: Nil,
false,
false,
List(apiTagCustomer)
)
lazy val addCustomerMessage : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "customer" :: customerNumber :: "messages" :: Nil JsonPost json -> _ => {
val test = Bank(bankId)
user => {
for {
postedData <- tryo{json.extract[AddCustomerMessageJson]} ?~! "Incorrect json format"
bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
customer <- Customer.customerProvider.vend.getUser(bankId, customerNumber) ?~! "Customer not found"
messageCreated <- booleanToBox(
CustomerMessages.customerMessageProvider.vend.addMessage(
@ -150,31 +166,41 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
}
}
val getBranchesIsPublic = Props.getBool("apiOptions.getBranchesIsPublic", true)
resourceDocs += ResourceDoc(
getBranches,
apiVersion,
"getBranches",
"GET",
"/banks/BANK_ID/branches",
"Get branches for the bank",
"""Returns information about branches for a single bank specified by BANK_ID including:
s"""Returns information about branches for a single bank specified by BANK_ID including:
|
|* Name
|* Address
|* Geo Location
|* License the data under this endpoint is released under
|
|Authentication via OAuth *may* be required.""",
|${authenticationRequiredMessage(!getBranchesIsPublic)}""",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil
emptyObjectJson :: Nil,
true,
false,
List(apiTagBanks)
)
lazy val getBranches : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "branches" :: Nil JsonGet _ => {
user => {
for {
u <- user ?~! "User must be logged in to retrieve Branches data"
bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
u <- if(getBranchesIsPublic)
Box(Some(1))
else
user ?~! "User must be logged in to retrieve Branches data"
bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
// Get branches from the active provider
branches <- Box(Branches.branchesProvider.vend.getBranches(bankId)) ~> APIFailure("No branches available. License may not be set.", 204)
} yield {
@ -187,23 +213,28 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
}
val getAtmsIsPublic = Props.getBool("apiOptions.getAtmsIsPublic", true)
resourceDocs += ResourceDoc(
getAtms,
apiVersion,
"getAtms",
"GET",
"/banks/BANK_ID/atms",
"Get ATMS for the bank",
"""Returns information about ATMs for a single bank specified by BANK_ID including:
s"""Returns information about ATMs for a single bank specified by BANK_ID including:
|
|* Address
|* Geo Location
|* License the data under this endpoint is released under
|
|Authentication via OAuth *may* be required.""",
|${authenticationRequiredMessage(!getAtmsIsPublic)}""",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil
emptyObjectJson :: Nil,
true,
false,
List(apiTagBanks)
)
lazy val getAtms : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
@ -211,8 +242,12 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
user => {
for {
// Get atms from the active provider
u <- user ?~! "User must be logged in to retrieve ATM data"
bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
u <- if(getAtmsIsPublic)
Box(Some(1))
else
user ?~! "User must be logged in to retrieve ATM data"
bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
atms <- Box(Atms.atmsProvider.vend.getAtms(bankId)) ~> APIFailure("No ATMs available. License may not be set.", 204)
} yield {
// Format the data as json
@ -225,13 +260,17 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
}
val getProductsIsPublic = Props.getBool("apiOptions.getProductsIsPublic", true)
resourceDocs += ResourceDoc(
getProducts,
apiVersion,
"getProducts",
"GET",
"/banks/BANK_ID/products",
"Get products offered by the bank",
"""Returns information about financial products offered by a bank specified by BANK_ID including:
s"""Returns information about financial products offered by a bank specified by BANK_ID including:
|
|* Name
|* Code
@ -241,10 +280,14 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
|* More info URL
|* Description
|* Terms and Conditions
|* License the data under this endpoint is released under""",
|* License the data under this endpoint is released under
|${authenticationRequiredMessage(!getProductsIsPublic)}""",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil
emptyObjectJson :: Nil,
true,
false,
List(apiTagBanks)
)
lazy val getProducts : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
@ -252,8 +295,11 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
user => {
for {
// Get products from the active provider
u <- user ?~! "User must be logged in to retrieve Products data"
bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
u <- if(getProductsIsPublic)
Box(Some(1))
else
user ?~! "User must be logged in to retrieve Products data"
bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
products <- Box(Products.productsProvider.vend.getProducts(bankId)) ~> APIFailure("No products available. License may not be set.", 204)
} yield {
// Format the data as json
@ -267,6 +313,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
resourceDocs += ResourceDoc(
getCrmEvents,
apiVersion,
"getCrmEvents",
"GET",
@ -275,7 +322,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
"",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil
emptyObjectJson :: Nil,
false,
false,
List(apiTagCustomer)
)
lazy val getCrmEvents : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
@ -284,7 +334,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
for {
// Get crm events from the active provider
u <- user ?~! "User must be logged in to retrieve CRM Event information"
bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
crmEvents <- Box(CrmEvent.crmEventProvider.vend.getCrmEvents(bankId)) ~> APIFailure("No CRM Events available.", 204)
} yield {
// Format the data as json
@ -295,40 +345,12 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
}
}
}
resourceDocs += ResourceDoc(
apiVersion,
"getResourceDocs",
"GET",
"/resource-docs/obp",
"Get Resource Documentation in OBP format.",
"Returns documentation about the resources on this server including example body for POST or PUT requests.",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil
)
// Provides resource documents so that live docs (currently on Sofi) can display API documentation
lazy val getResourceDocs : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "resource-docs" :: "obp" :: Nil JsonGet _ => {
user => {
for {
rd <- getResourceDocsList
} yield {
// Format the data as json
val json = JSONFactory1_4_0.createResourceDocsJson(rd)
// Return
successJsonResponse(Extraction.decompose(json))
}
}
}
}
/*
transaction requests (new payments since 1.4.0)
*/
resourceDocs += ResourceDoc(
getTransactionRequestTypes,
apiVersion,
"getTransactionRequestTypes",
"GET",
@ -337,7 +359,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
"",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
true,
true,
List(apiTagPayment))
lazy val getTransactionRequestTypes: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
@ -345,8 +370,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
user =>
if (Props.getBool("transactionRequests_enabled", false)) {
for {
u <- user ?~ "User not found"
fromBank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
u <- user ?~ ErrorMessages.UserNotLoggedIn
fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~ {"Unknown bank account"}
view <- tryo(fromAccount.permittedViews(user).find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId}
transactionRequestTypes <- Connector.connector.vend.getTransactionRequestTypes(u, fromAccount)
@ -361,6 +386,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
}
resourceDocs += ResourceDoc(
getTransactionRequests,
apiVersion,
"getTransactionRequests",
"GET",
@ -369,16 +395,19 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
"",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
true,
true,
List(apiTagPayment))
lazy val getTransactionRequests: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-requests" :: Nil JsonGet _ => {
user =>
if (Props.getBool("transactionRequests_enabled", false)) {
for {
u <- user ?~ "User not found"
fromBank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {"Unknown bank account"}
u <- user ?~ ErrorMessages.UserNotLoggedIn
fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound}
view <- tryo(fromAccount.permittedViews(user).find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId}
transactionRequests <- Connector.connector.vend.getTransactionRequests(u, fromAccount)
}
@ -397,12 +426,19 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
case class TransactionIdJson(transaction_id : String)
resourceDocs += ResourceDoc(
createTransactionRequest,
apiVersion,
"createTransactionRequest",
"POST",
"/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/TRANSACTION_REQUEST_TYPE/transaction-requests",
"Create Transaction Request.",
"",
"""Initiate a Payment via a Transaction Request.
|
|This is the preferred method to create a payment and supersedes makePayment in 1.2.1.
|
|See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow.
|
|In sandbox mode, if the amount is < 100 the transaction request will create a transaction without a challenge, else a challenge will need to be answered.""",
Extraction.decompose(TransactionRequestBodyJSON (
TransactionRequestAccountJSON("BANK_ID", "ACCOUNT_ID"),
AmountOfMoneyJSON("EUR", "100.53"),
@ -411,7 +447,10 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
)
),
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
true,
true,
List(apiTagPayment))
lazy val createTransactionRequest: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
@ -423,14 +462,14 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
* check if user has access using the view that is given (now it checks if user has access to owner view), will need some new permissions for transaction requests
* test: functionality, error messages if user not given or invalid, if any other value is not existing
*/
u <- user ?~ "User not found"
transBodyJson <- tryo{json.extract[TransactionRequestBodyJSON]} ?~ {"Invalid json format"}
u <- user ?~ ErrorMessages.UserNotLoggedIn
transBodyJson <- tryo{json.extract[TransactionRequestBodyJSON]} ?~ {ErrorMessages.InvalidJsonFormat}
transBody <- tryo{getTransactionRequestBodyFromJson(transBodyJson)}
fromBank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {"Unknown bank account"}
fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound}
toBankId <- tryo(BankId(transBodyJson.to.bank_id))
toAccountId <- tryo(AccountId(transBodyJson.to.account_id))
toAccount <- tryo{BankAccount(toBankId, toAccountId).get} ?~! {"Unknown counterparty account"}
toAccount <- tryo{BankAccount(toBankId, toAccountId).get} ?~! {ErrorMessages.CounterpartyNotFound}
accountsCurrencyEqual <- tryo(assert(fromAccount.currency == toAccount.currency)) ?~! {"Counterparty and holder accounts have differing currencies."}
transferCurrencyEqual <- tryo(assert(transBodyJson.value.currency == fromAccount.currency)) ?~! {"Request currency and holder account currency can't be different."}
rawAmt <- tryo {BigDecimal(transBodyJson.value.amount)} ?~! s"Amount ${transBodyJson.value.amount} not convertible to number"
@ -448,15 +487,19 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
resourceDocs += ResourceDoc(
answerTransactionRequestChallenge,
apiVersion,
"answerTransactionRequestChallenge",
"POST",
"/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/TRANSACTION_REQUEST_TYPE/transaction-requests/TRANSACTION_REQUEST_ID/challenge",
"Answer Transaction Request Challenge.",
"",
"In Sandbox mode, any string that can be converted to a possitive integer will be accepted as an answer.",
Extraction.decompose(ChallengeAnswerJSON("89123812", "123345")),
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
true,
true,
List(apiTagPayment))
lazy val answerTransactionRequestChallenge: PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" ::
@ -464,8 +507,8 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
user =>
if (Props.getBool("transactionRequests_enabled", false)) {
for {
u <- user ?~ "User not found"
fromBank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
u <- user ?~ ErrorMessages.UserNotLoggedIn
fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {"Unknown bank account"}
view <- tryo(fromAccount.permittedViews(user).find(_ == viewId)) ?~ {"Current user does not have access to the view " + viewId}
answerJson <- tryo{json.extract[ChallengeAnswerJSON]} ?~ {"Invalid json format"}
@ -487,20 +530,26 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
resourceDocs += ResourceDoc(
addCustomer,
apiVersion,
"addCustomer",
"POST",
"/banks/BANK_ID/customer",
"Add a customer.",
"""Add a customer linked to the currently authenticated user.
|This call is experimental and will require additional permissions/role in the future.
|The Customer resource stores the customer number, legal name, email, phone number, their date of birth, relationship status, education attained, a url for a profile image, KYC status etc.
|This call may require additional permissions/role in the future.
|For now the authenticated user can create at most one linked customer.
|OAuth authentication is required.
|""",
Extraction.decompose(CustomerJson("687687678", "Joe David Bloggs",
"+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate))),
"+44 07972 444 876", "person@example.com", CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate),
exampleDate, "Single", 1, List(exampleDate), "Bachelors Degree", "Employed", true, exampleDate)),
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
false,
false,
List(apiTagCustomer))
lazy val addCustomer : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
//updates a view on a bank account
@ -508,7 +557,7 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
user =>
for {
u <- user ?~! "User must be logged in to post Customer"
bank <- tryo(Bank(bankId).get) ?~! {"Unknown bank id"}
bank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound}
customer <- booleanToBox(Customer.customerProvider.vend.getCustomer(bankId, u).isEmpty) ?~ "Customer already exists for this user."
postedData <- tryo{json.extract[CustomerJson]} ?~! "Incorrect json format"
customer <- Customer.customerProvider.vend.addCustomer(bankId,
@ -517,7 +566,15 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
postedData.legal_name,
postedData.mobile_phone_number,
postedData.email,
MockCustomerFaceImage(postedData.face_image.date, postedData.face_image.url)) ?~! "Could not create customer"
MockCustomerFaceImage(postedData.face_image.date, postedData.face_image.url),
postedData.date_of_birth,
postedData.relationship_status,
postedData.dependants,
postedData.dob_of_dependants,
postedData.highest_education_attained,
postedData.employment_status,
postedData.kyc_status,
postedData.last_ok_date) ?~! "Could not create customer"
} yield {
val successJson = Extraction.decompose(customer)
successJsonResponse(successJson)
@ -529,10 +586,11 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
if (Props.devMode) {
resourceDocs += ResourceDoc(
dummy(apiVersion),
apiVersion,
"testResourceDoc",
"GET",
"/i-do-not-exist-i-will-404",
"/dummy",
"I am only a test resource Doc",
"""
|
@ -562,7 +620,26 @@ trait APIMethods140 extends Loggable with APIMethods130 with APIMethods121{
|_etc_...""",
emptyObjectJson,
emptyObjectJson,
emptyObjectJson :: Nil)
emptyObjectJson :: Nil,
false,
false,
Nil)
}
def dummy(apiVersion : String) : PartialFunction[Req, Box[User] => Box[JsonResponse]] = {
case "dummy" :: Nil JsonGet json => {
user =>
val apiDetails: JValue = {
val hostedBy = new HostedBy("TESOBE", "contact@tesobe.com", "+49 (0)30 8145 3994")
val apiInfoJSON = new APIInfoJSON(apiVersion, gitCommit, hostedBy)
Extraction.decompose(apiInfoJSON)
}
Full(successJsonResponse(apiDetails, 200))
}
}
}
}

View File

@ -17,13 +17,25 @@ import code.transactionrequests.TransactionRequests._
import net.liftweb.json.JsonAST.{JValue, JObject}
import org.pegdown.PegDownProcessor
// Use this so we don't duplicate it
import code.api.v1_2_1.{AmountOfMoneyJSON}
object JSONFactory1_4_0 {
case class CustomerJson(customer_number : String,
legal_name : String,
mobile_phone_number : String,
email : String,
face_image : CustomerFaceImageJson)
face_image : CustomerFaceImageJson,
date_of_birth: Date,
relationship_status: String,
dependants: Int,
dob_of_dependants: List[Date],
highest_education_attained: String,
employment_status: String,
kyc_status: Boolean,
last_ok_date: Date)
case class CustomerFaceImageJson(url : String, date : Date)
@ -77,7 +89,15 @@ object JSONFactory1_4_0 {
mobile_phone_number = cInfo.mobileNumber,
email = cInfo.email,
face_image = CustomerFaceImageJson(url = cInfo.faceImage.url,
date = cInfo.faceImage.date)
date = cInfo.faceImage.date),
date_of_birth = cInfo.dateOfBirth,
relationship_status = cInfo.relationshipStatus,
dependants = cInfo.dependents,
dob_of_dependants = cInfo.dobOfDependents,
highest_education_attained = cInfo.highestEducationAttained,
employment_status = cInfo.employmentStatus,
kyc_status = cInfo.kycStatus,
last_ok_date = cInfo.lastOkDate
)
}
@ -231,14 +251,18 @@ object JSONFactory1_4_0 {
description: String,
example_request_body: JValue,
success_response_body: JValue,
implemented_by: ImplementedByJson)
implemented_by: ImplementedByJson,
is_core: Boolean,
is_psd2: Boolean,
is_obwg: Boolean,
tags: List[String])
// Creates the json resource_docs
case class ResourceDocsJson (resource_docs : List[ResourceDocJson])
def createResourceDocJson(resourceDoc: ResourceDoc) : ResourceDocJson = {
def createResourceDocJson(rd: ResourceDoc) : ResourceDocJson = {
// There are multiple flavours of markdown. For instance, original markdown emphasises underscores (surrounds _ with (<em>))
// But we don't want to have to escape underscores (\_) in our documentation
@ -250,15 +274,19 @@ object JSONFactory1_4_0 {
val pegDownProcessor : PegDownProcessor = new PegDownProcessor
ResourceDocJson(
operation_id = s"${resourceDoc.apiVersion.toString}-${resourceDoc.apiFunction.toString}",
request_verb = resourceDoc.requestVerb,
request_url = resourceDoc.requestUrl,
summary = resourceDoc.summary,
operation_id = s"${rd.apiVersion.toString}-${rd.apiFunction.toString}",
request_verb = rd.requestVerb,
request_url = rd.requestUrl,
summary = rd.summary,
// Strip the margin character (|) and line breaks and convert from markdown to html
description = pegDownProcessor.markdownToHtml(resourceDoc.description.stripMargin).replaceAll("\n", ""),
example_request_body = resourceDoc.exampleRequestBody,
success_response_body = resourceDoc.successResponseBody,
implemented_by = ImplementedByJson(resourceDoc.apiVersion, resourceDoc.apiFunction)
description = pegDownProcessor.markdownToHtml(rd.description.stripMargin).replaceAll("\n", ""),
example_request_body = rd.exampleRequestBody,
success_response_body = rd.successResponseBody,
implemented_by = ImplementedByJson(rd.apiVersion, rd.apiFunction),
is_core = rd.isCore,
is_psd2 = rd.isPSD2,
is_obwg = rd.isCore, // For now, track isCore
tags = rd.tags.map(i => i.tag)
)
}
@ -309,10 +337,6 @@ object JSONFactory1_4_0 {
)
}
case class AmountOfMoneyJSON (
currency : String,
amount : String
)
case class TransactionRequestAccountJSON (
bank_id: String,
account_id : String

View File

@ -18,6 +18,7 @@ object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable {
Implementations1_2_1.privateAccountsAtOneBank,
Implementations1_2_1.publicAccountsAtOneBank,
Implementations1_2_1.accountById,
Implementations1_2_1.updateAccountLabel,
Implementations1_2_1.getViewsForBankAccount,
Implementations1_2_1.createViewForBankAccount,
Implementations1_2_1.updateViewForBankAccount,
@ -77,7 +78,7 @@ object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable {
Implementations1_2_1.updateWhereTagForViewOnTransaction,
Implementations1_2_1.deleteWhereTagForViewOnTransaction,
Implementations1_2_1.getCounterpartyForTransaction,
//note: removed 1.2.1 makePayment
Implementations1_2_1.makePayment, // Back for a while
// New in 1.3.0
Implementations1_3_0.getCards,
@ -92,7 +93,6 @@ object OBPAPI1_4_0 extends OBPRestHelper with APIMethods140 with Loggable {
Implementations1_4_0.getAtms,
Implementations1_4_0.getProducts,
Implementations1_4_0.getCrmEvents,
Implementations1_4_0.getResourceDocs,
Implementations1_4_0.createTransactionRequest,
Implementations1_4_0.getTransactionRequests,
Implementations1_4_0.getTransactionRequestTypes,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,438 @@
/**
Open Bank Project - API
Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Email: contact@tesobe.com
TESOBE / Music Pictures Ltd
Osloerstrasse 16/17
Berlin 13359, Germany
This product includes software developed at
TESOBE (http://www.tesobe.com/)
by
Simon Redfern : simon AT tesobe DOT com
Stefan Bethge : stefan AT tesobe DOT com
Everett Sochowski : everett AT tesobe DOT com
Ayoub Benali: ayoub AT tesobe DOT com
*/
package code.api.v2_0_0
import java.net.URL
import java.util.Date
import code.api.util.APIUtil.ApiLink
import code.kycdocuments.KycDocument
import code.kycmedias.KycMedia
import code.kycstatuses.KycStatus
import code.kycchecks.KycCheck
import code.socialmedia.SocialMedia
import net.liftweb.common.{Box, Full}
import code.model._
import net.liftweb.json.JsonAST.JValue
// Import explicitly and rename so its clear.
import code.api.v1_2_1.{AccountHolderJSON => AccountHolderJSON121, AmountOfMoneyJSON => AmountOfMoneyJSON121, UserJSON => UserJSON121, ViewJSON => ViewJSON121, ThisAccountJSON => ThisAccountJSON121, OtherAccountJSON => OtherAccountJSON121, TransactionDetailsJSON => TransactionDetailsJSON121, JSONFactory => JSONFactory121, MinimalBankJSON => MinimalBankJSON121}
// New in 2.0.0
class LinkJSON(
val href: URL,
val rel: String,
val method: String
)
class LinksJSON(
val _links: List[LinkJSON]
)
class ResultAndLinksJSON(
val result : JValue,
val links: LinksJSON
)
class BasicViewJSON(
val id: String,
val short_name: String,
val is_public: Boolean
)
case class BasicAccountsJSON(
accounts : List[BasicAccountJSON]
)
// Basic Account has basic View
case class BasicAccountJSON(
id : String,
label : String,
views_available : List[BasicViewJSON],
bank_id : String
)
// Json used in account creation
case class CreateAccountJSON(
`type` : String,
balance : AmountOfMoneyJSON121
)
// No view in core
case class CoreAccountJSON(
id : String,
label : String,
bank_id : String,
_links: JValue
)
case class KycDocumentJSON(
id: String,
customer_number: String,
`type`: String,
number: String,
issue_date: Date,
issue_place: String,
expiry_date: Date
)
case class KycDocumentsJSON(documents: List[KycDocumentJSON])
case class KycMediaJSON(
id: String,
customer_number: String,
`type`: String,
url: String,
date: Date,
relates_to_kyc_document_id: String,
relates_to_kyc_check_id: String
)
case class KycMediasJSON(medias: List[KycMediaJSON])
case class KycCheckJSON(
id: String,
customer_number: String,
date: Date,
how: String,
staff_user_id: String,
staff_name: String,
satisfied: Boolean,
comments: String
)
case class KycChecksJSON(checks: List[KycCheckJSON])
case class KycStatusJSON(
customer_number: String,
ok: Boolean,
date: Date
)
case class KycStatusesJSON(statuses: List[KycStatusJSON])
case class SocialMediaJSON(
customer_number: String,
`type`: String,
handle: String,
date_added: Date,
date_activated: Date
)
case class SocialMediasJSON(checks: List[SocialMediaJSON])
object JSONFactory200{
// New in 2.0.0
def createViewBasicJSON(view : View) : BasicViewJSON = {
val alias =
if(view.usePublicAliasIfOneExists)
"public"
else if(view.usePrivateAliasIfOneExists)
"private"
else
""
new BasicViewJSON(
id = view.viewId.value,
short_name = stringOrNull(view.name),
is_public = view.isPublic
)
}
def createBasicAccountJSON(account : BankAccount, basicViewsAvailable : List[BasicViewJSON] ) : BasicAccountJSON = {
new BasicAccountJSON(
account.accountId.value,
stringOrNull(account.label),
basicViewsAvailable,
account.bankId.value
)
}
// Contains only minimal info (could have more if owner) plus links
def createCoreAccountJSON(account : BankAccount, links: JValue ) : CoreAccountJSON = {
val coreAccountJson = new CoreAccountJSON(
account.accountId.value,
stringOrNull(account.label),
account.bankId.value,
links
)
coreAccountJson
}
case class ModeratedCoreAccountJSON(
id : String,
label : String,
number : String,
owners : List[UserJSON121],
`type` : String,
balance : AmountOfMoneyJSON121,
IBAN : String,
swift_bic: String,
bank_id : String
)
////
case class CoreTransactionsJSON(
transactions: List[CoreTransactionJSON]
)
case class CoreTransactionJSON(
id : String,
account : ThisAccountJSON121,
counterparty : CoreCounterpartyJSON,
details : CoreTransactionDetailsJSON
)
case class CoreAccountHolderJSON(
name : String
)
case class CoreCounterpartyJSON(
id : String,
holder : CoreAccountHolderJSON,
number : String,
kind : String,
IBAN : String,
swift_bic: String,
bank : MinimalBankJSON121
)
def createCoreTransactionsJSON(transactions: List[ModeratedTransaction]) : CoreTransactionsJSON = {
new CoreTransactionsJSON(transactions.map(createCoreTransactionJSON))
}
case class CoreTransactionDetailsJSON(
`type` : String,
description : String,
posted : Date,
completed : Date,
new_balance : AmountOfMoneyJSON121,
value : AmountOfMoneyJSON121
)
def createCoreTransactionDetailsJSON(transaction : ModeratedTransaction) : CoreTransactionDetailsJSON = {
new CoreTransactionDetailsJSON(
`type` = stringOptionOrNull(transaction.transactionType),
description = stringOptionOrNull(transaction.description),
posted = transaction.startDate.getOrElse(null),
completed = transaction.finishDate.getOrElse(null),
new_balance = JSONFactory121.createAmountOfMoneyJSON(transaction.currency, transaction.balance),
value= JSONFactory121.createAmountOfMoneyJSON(transaction.currency, transaction.amount.map(_.toString))
)
}
def createCoreTransactionJSON(transaction : ModeratedTransaction) : CoreTransactionJSON = {
new CoreTransactionJSON(
id = transaction.id.value,
account = transaction.bankAccount.map(JSONFactory121.createThisAccountJSON).getOrElse(null),
counterparty = transaction.otherBankAccount.map(createCoreCounterparty).getOrElse(null),
details = createCoreTransactionDetailsJSON(transaction)
)
}
case class CounterpartiesJSON(
counterparties : List[CoreCounterpartyJSON]
)
def createCoreCounterparty(bankAccount : ModeratedOtherBankAccount) : CoreCounterpartyJSON = {
new CoreCounterpartyJSON(
id = bankAccount.id,
number = stringOptionOrNull(bankAccount.number),
kind = stringOptionOrNull(bankAccount.kind),
IBAN = stringOptionOrNull(bankAccount.iban),
swift_bic = stringOptionOrNull(bankAccount.swift_bic),
bank = JSONFactory121.createMinimalBankJSON(bankAccount),
holder = createAccountHolderJSON(bankAccount.label.display, bankAccount.isAlias)
)
}
def createAccountHolderJSON(owner : User, isAlias : Boolean) : CoreAccountHolderJSON = {
// Note we are not using isAlias
new CoreAccountHolderJSON(
name = owner.name
)
}
def createAccountHolderJSON(name : String, isAlias : Boolean) : CoreAccountHolderJSON = {
// Note we are not using isAlias
new CoreAccountHolderJSON(
name = name
)
}
def createCoreBankAccountJSON(account : ModeratedBankAccount, viewsAvailable : List[ViewJSON121]) : ModeratedCoreAccountJSON = {
val bankName = account.bankName.getOrElse("")
new ModeratedCoreAccountJSON (
account.accountId.value,
JSONFactory121.stringOptionOrNull(account.label),
JSONFactory121.stringOptionOrNull(account.number),
JSONFactory121.createOwnersJSON(account.owners.getOrElse(Set()), bankName),
JSONFactory121.stringOptionOrNull(account.accountType),
JSONFactory121.createAmountOfMoneyJSON(account.currency.getOrElse(""), account.balance),
JSONFactory121.stringOptionOrNull(account.iban),
JSONFactory121.stringOptionOrNull(account.swift_bic),
stringOrNull(account.bankId.value)
)
}
def createKycDocumentJSON(kycDocument : KycDocument) : KycDocumentJSON = {
new KycDocumentJSON(
id = kycDocument.idKycDocument,
customer_number = kycDocument.customerNumber,
`type` = kycDocument.`type`,
number = kycDocument.number,
issue_date = kycDocument.issueDate,
issue_place = kycDocument.issuePlace,
expiry_date = kycDocument.expiryDate
)
}
def createKycDocumentsJSON(messages : List[KycDocument]) : KycDocumentsJSON = {
KycDocumentsJSON(messages.map(createKycDocumentJSON))
}
def createKycMediaJSON(kycMedia : KycMedia) : KycMediaJSON = {
new KycMediaJSON(
id = kycMedia.idKycMedia,
customer_number = kycMedia.customerNumber,
`type` = kycMedia.`type`,
url = kycMedia.url,
date = kycMedia.date,
relates_to_kyc_document_id = kycMedia.relatesToKycDocumentId,
relates_to_kyc_check_id = kycMedia.relatesToKycCheckId
)
}
def createKycMediasJSON(messages : List[KycMedia]) : KycMediasJSON = {
KycMediasJSON(messages.map(createKycMediaJSON))
}
def createKycCheckJSON(kycCheck : KycCheck) : KycCheckJSON = {
new KycCheckJSON(
id = kycCheck.idKycCheck,
customer_number = kycCheck.customerNumber,
date = kycCheck.date,
how = kycCheck.how,
staff_user_id = kycCheck.staffUserId,
staff_name = kycCheck.staffName,
satisfied = kycCheck.satisfied,
comments = kycCheck.comments
)
}
def createKycChecksJSON(messages : List[KycCheck]) : KycChecksJSON = {
KycChecksJSON(messages.map(createKycCheckJSON))
}
def createKycStatusJSON(kycStatus : KycStatus) : KycStatusJSON = {
new KycStatusJSON(
customer_number = kycStatus.customerNumber,
ok = kycStatus.ok,
date = kycStatus.date
)
}
def createKycStatusesJSON(messages : List[KycStatus]) : KycStatusesJSON = {
KycStatusesJSON(messages.map(createKycStatusJSON))
}
def createSocialMediaJSON(socialMedia : SocialMedia) : SocialMediaJSON = {
new SocialMediaJSON(
customer_number = socialMedia.customerNumber,
`type` = socialMedia.`type`,
handle = socialMedia.handle,
date_added = socialMedia.dateAdded,
date_activated = socialMedia.dateActivated
)
}
def createSocialMediasJSON(messages : List[SocialMedia]) : SocialMediasJSON = {
SocialMediasJSON(messages.map(createSocialMediaJSON))
}
// Copied from 1.2.1 (import just this def instead?)
def stringOrNull(text : String) =
if(text == null || text.isEmpty)
null
else
text
// Copied from 1.2.1 (import just this def instead?)
def stringOptionOrNull(text : Option[String]) =
text match {
case Some(t) => stringOrNull(t)
case _ => null
}
}

View File

@ -0,0 +1,175 @@
/**
Open Bank Project - API
Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Email: contact@tesobe.com
TESOBE / Music Pictures Ltd
Osloerstrasse 16/17
Berlin 13359, Germany
This product includes software developed at
TESOBE (http://www.tesobe.com/)
by
Simon Redfern : simon AT tesobe DOT com
Stefan Bethge : stefan AT tesobe DOT com
Everett Sochowski : everett AT tesobe DOT com
Ayoub Benali: ayoub AT tesobe DOT com
*/
package code.api.v2_0_0
import code.api.v1_3_0.APIMethods130
import code.api.v1_4_0.{APIMethods140}
import net.liftweb.json.Extraction
import net.liftweb.json.JsonAST._
import net.liftweb.common.Loggable
import code.api.OBPRestHelper
object OBPAPI2_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 with APIMethods200 with Loggable {
val VERSION = "2.0.0"
// Note: Since we pattern match on these routes, if two implementations match a given url the first will match
val routes = List(
Implementations1_2_1.root(VERSION),
Implementations1_2_1.allBanks,
Implementations1_2_1.bankById,
// Now in 2_0_0
// Implementations1_2_1.allAccountsAllBanks,
// Implementations1_2_1.privateAccountsAllBanks,
// Implementations1_2_1.publicAccountsAllBanks,
// Implementations1_2_1.allAccountsAtOneBank,
// Implementations1_2_1.privateAccountsAtOneBank,
// Implementations1_2_1.publicAccountsAtOneBank,
// Implementations1_2_1.accountById,
Implementations1_2_1.updateAccountLabel,
Implementations1_2_1.getViewsForBankAccount,
Implementations1_2_1.createViewForBankAccount,
Implementations1_2_1.updateViewForBankAccount,
Implementations1_2_1.deleteViewForBankAccount,
// Implementations1_2_1.getPermissionsForBankAccount,
// Implementations1_2_1.getPermissionForUserForBankAccount,
Implementations1_2_1.addPermissionForUserForBankAccountForMultipleViews,
Implementations1_2_1.addPermissionForUserForBankAccountForOneView,
Implementations1_2_1.removePermissionForUserForBankAccountForOneView,
Implementations1_2_1.removePermissionForUserForBankAccountForAllViews,
Implementations1_2_1.getCounterpartiesForBankAccount,
Implementations1_2_1.getCounterpartyByIdForBankAccount,
Implementations1_2_1.getCounterpartyMetadata,
Implementations1_2_1.getCounterpartyPublicAlias,
Implementations1_2_1.addCounterpartyPublicAlias,
Implementations1_2_1.updateCounterpartyPublicAlias,
Implementations1_2_1.deleteCounterpartyPublicAlias,
Implementations1_2_1.getCounterpartyPrivateAlias,
Implementations1_2_1.addCounterpartyPrivateAlias,
Implementations1_2_1.updateCounterpartyPrivateAlias,
Implementations1_2_1.deleteCounterpartyPrivateAlias,
Implementations1_2_1.addCounterpartyMoreInfo,
Implementations1_2_1.updateCounterpartyMoreInfo,
Implementations1_2_1.deleteCounterpartyMoreInfo,
Implementations1_2_1.addCounterpartyUrl,
Implementations1_2_1.updateCounterpartyUrl,
Implementations1_2_1.deleteCounterpartyUrl,
Implementations1_2_1.addCounterpartyImageUrl,
Implementations1_2_1.updateCounterpartyImageUrl,
Implementations1_2_1.deleteCounterpartyImageUrl,
Implementations1_2_1.addCounterpartyOpenCorporatesUrl,
Implementations1_2_1.updateCounterpartyOpenCorporatesUrl,
Implementations1_2_1.deleteCounterpartyOpenCorporatesUrl,
Implementations1_2_1.addCounterpartyCorporateLocation,
Implementations1_2_1.updateCounterpartyCorporateLocation,
Implementations1_2_1.deleteCounterpartyCorporateLocation,
Implementations1_2_1.addCounterpartyPhysicalLocation,
Implementations1_2_1.updateCounterpartyPhysicalLocation,
Implementations1_2_1.deleteCounterpartyPhysicalLocation,
Implementations1_2_1.getTransactionsForBankAccount,
Implementations1_2_1.getTransactionByIdForBankAccount,
Implementations1_2_1.getTransactionNarrative,
Implementations1_2_1.addTransactionNarrative,
Implementations1_2_1.updateTransactionNarrative,
Implementations1_2_1.deleteTransactionNarrative,
Implementations1_2_1.getCommentsForViewOnTransaction,
Implementations1_2_1.addCommentForViewOnTransaction,
Implementations1_2_1.deleteCommentForViewOnTransaction,
Implementations1_2_1.getTagsForViewOnTransaction,
Implementations1_2_1.addTagForViewOnTransaction,
Implementations1_2_1.deleteTagForViewOnTransaction,
Implementations1_2_1.getImagesForViewOnTransaction,
Implementations1_2_1.addImageForViewOnTransaction,
Implementations1_2_1.deleteImageForViewOnTransaction,
Implementations1_2_1.getWhereTagForViewOnTransaction,
Implementations1_2_1.addWhereTagForViewOnTransaction,
Implementations1_2_1.updateWhereTagForViewOnTransaction,
Implementations1_2_1.deleteWhereTagForViewOnTransaction,
Implementations1_2_1.getCounterpartyForTransaction,
Implementations1_2_1.makePayment,
// New in 1.3.0
Implementations1_3_0.getCards,
Implementations1_3_0.getCardsForBank,
// New in 1.4.0
Implementations1_4_0.getCustomer,
Implementations1_4_0.addCustomer,
Implementations1_4_0.getCustomerMessages,
Implementations1_4_0.addCustomerMessage,
Implementations1_4_0.getBranches,
Implementations1_4_0.getAtms,
Implementations1_4_0.getProducts,
Implementations1_4_0.getCrmEvents,
Implementations1_4_0.createTransactionRequest,
Implementations1_4_0.getTransactionRequests,
Implementations1_4_0.getTransactionRequestTypes,
Implementations1_4_0.answerTransactionRequestChallenge,
// Modified in 2.0.0 (less info about the views)
Implementations2_0_0.allAccountsAllBanks,
Implementations2_0_0.privateAccountsAllBanks,
Implementations2_0_0.publicAccountsAllBanks,
Implementations2_0_0.allAccountsAtOneBank,
Implementations2_0_0.privateAccountsAtOneBank,
Implementations2_0_0.publicAccountsAtOneBank,
// Modified in 2.0.0 (added sorting and better guards / error messages)
Implementations2_0_0.accountById,
Implementations2_0_0.getPermissionsForBankAccount,
Implementations2_0_0.getPermissionForUserForBankAccount,
// New in 2.0.0
Implementations2_0_0.getKycDocuments,
Implementations2_0_0.getKycMedia,
Implementations2_0_0.getKycStatuses,
Implementations2_0_0.getKycChecks,
Implementations2_0_0.getSocialMediaHandles,
Implementations2_0_0.addKycDocument,
Implementations2_0_0.addKycMedia,
Implementations2_0_0.addKycStatus,
Implementations2_0_0.addKycCheck,
Implementations2_0_0.addSocialMediaHandle,
Implementations2_0_0.getCoreAccountById,
Implementations2_0_0.getCoreTransactionsForBankAccount,
Implementations2_0_0.createAccount
//Implementations2_0_0.testLinks
)
routes.foreach(route => {
oauthServe(apiPrefix{route})
})
}

View File

@ -134,7 +134,7 @@ trait Connector {
//set initial status
//for sandbox / testing: depending on amount, we ask for challenge or not
val status =
if (transactionRequestType.value == "SANDBOX" && body.value.currency == "EUR" && BigDecimal(body.value.amount) < 100) {
if (transactionRequestType.value == TransactionRequests.CHALLENGE_SANDBOX_TAN && BigDecimal(body.value.amount) < 100) {
TransactionRequests.STATUS_COMPLETED
} else {
TransactionRequests.STATUS_INITIATED
@ -345,4 +345,4 @@ trait Connector {
def updateAccountLabel(bankId: BankId, accountId: AccountId, label: String): Boolean
}
}

View File

@ -1,305 +0,0 @@
package code.bankconnectors
/*
Open Bank Project - API
Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see http://www.gnu.org/licenses/.
Email: contact@tesobe.com
TESOBE / Music Pictures Ltd
Osloerstrasse 16/17
Berlin 13359, Germany
*/
import java.text.{SimpleDateFormat, DateFormat}
import java.util.{Date, UUID, TimeZone, Locale, Properties}
import code.model._
import code.util.Helper
import net.liftweb.common.{Loggable, Empty, Full, Box, Failure}
import net.liftweb.mapper._
import net.liftweb.util.Helpers._
import net.liftweb.util.{False, Props}
import scala.concurrent.ops._
import scala.concurrent.duration._
import kafka.utils.{ZkUtils, ZKStringSerializer}
import org.I0Itec.zkclient.ZkClient
import kafka.consumer.Consumer
import kafka.consumer._
import kafka.consumer.KafkaStream
import kafka.message._
import kafka.producer.{KeyedMessage, Producer, ProducerConfig}
import code.model.dataAccess.{Account, MappedBank}
import code.model.Transaction
object KafkaConnector extends Connector with Loggable {
//gets banks handled by this connector
override def getBanks: List[Bank] = {
// Generate random uuid to be used as request-respose match id
val reqId: String = UUID.randomUUID().toString
// Create Kafka producer, using list of brokers from Zookeeper
val producer: KafkaProducer = new KafkaProducer()
// Send request to Kafka, marked with reqId
// so we can fetch the corresponding response
val argList: Map[String, String] = Map()
producer.send(reqId, "getBanks", argList, "1")
// Request sent, now we wait for response with the same reqId
val consumer = new KafkaConsumer()
val rList = consumer.getResponse(reqId)
// Loop through list of responses and create entry for each
val res = { for ( r <- rList ) yield {
MappedBank.create
.permalink(r.getOrElse("permalink", ""))
.fullBankName(r.getOrElse("fullBankName", ""))
.shortBankName(r.getOrElse("shortBankName", ""))
.logoURL(r.getOrElse("logoURL", ""))
.websiteURL(r.getOrElse("websiteURL", ""))
}
}
// Return list of results
res
}
// Gets bank identified by id
override def getBank(bankId: code.model.BankId): Box[Bank] = {
// Generate random uuid to be used as request-respose match id
val reqId: String = UUID.randomUUID().toString
// Create Kafka producer, using list of brokers from Zookeeper
val producer: KafkaProducer = new KafkaProducer()
// Send request to Kafka, marked with reqId
// so we can fetch the corresponding response
val argList = Map( "bankId" -> bankId.toString )
producer.send(reqId, "getBank", argList, "1")
// Request sent, now we wait for response with the same reqId
val consumer = new KafkaConsumer()
// Create entry only for the first item on returned list
val r = consumer.getResponse(reqId).head
val res = MappedBank.create
.permalink(r.getOrElse("permalink", ""))
.fullBankName(r.getOrElse("fullBankName", ""))
.shortBankName(r.getOrElse("shortBankName", ""))
.logoURL(r.getOrElse("logoURL", ""))
.websiteURL(r.getOrElse("websiteURL", ""))
// Return result
Full(res)
}
// Gets bank account identified by bankid and accountid
override def getBankAccount(bankId : BankId, accountId : AccountId) : Box[BankAccount] = {
// Generate random uuid to be used as request-respose match id
val reqId: String = UUID.randomUUID().toString
// Create Kafka producer, using list of brokers from Zookeeper
val producer: KafkaProducer = new KafkaProducer()
// Send request to Kafka, marked with reqId
// so we can fetch the corresponding response
val argList = Map( "bankId" -> bankId.toString,
"accountId" -> accountId.toString )
producer.send(reqId, "getBankAccount", argList, "1")
// Request sent, now we wait for response with the same reqId
val consumer = new KafkaConsumer()
// Create entry only for the first item on returned list
val r = consumer.getResponse(reqId).head
val res = new BankAccount {
val accountId = AccountId(r.getOrElse("accountId", ""));
val bankId = BankId(r.getOrElse("bankId", ""));
val uuid = r.getOrElse("uuid", "");
val accountHolder = r.getOrElse("accountHolder", "");
val accountType = r.getOrElse("accountType", "");
val currency = r.getOrElse("currency", "");
val label = r.getOrElse("label", "");
val name = r.getOrElse("name", "");
val number = r.getOrElse("number", "");
val balance = BigDecimal(r.getOrElse("balance", "0.0"));
val swift_bic = Some(r.getOrElse("swift_bic", ""));
val iban = Some(r.getOrElse("iban", ""));
val lastUpdate = new SimpleDateFormat("EEE MMMM d HH:mm:ss z yyyy", Locale.ENGLISH).parse(r.getOrElse("lastUpdate", "Thu Jan 1 00:00:00 UTC 1970"))
}
// Return list of results
Full(res)
}
// Gets transaction identified by bankid, accountid and transactionId
def getTransaction(bankId: BankId, accountId: AccountId, transactionId: TransactionId): Box[Transaction] = {
// Generate random uuid to be used as request-respose match id
val reqId: String = UUID.randomUUID().toString
// Create Kafka producer, using list of brokers from Zookeeper
val producer: KafkaProducer = new KafkaProducer()
// Send request to Kafka, marked with reqId
// so we can fetch the corresponding response
val argList = Map( "bankId" -> bankId.toString,
"accountId" -> accountId.toString,
"transactionId" -> transactionId.toString )
producer.send(reqId, "getTransaction", argList, "1")
// Request sent, now we wait for response with the same reqId
val consumer = new KafkaConsumer()
// Create entry only for the first item on returned list
val r = consumer.getResponse(reqId).head
// helper for creating otherbankaccount
def createOtherBankAccount(alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = {
new OtherBankAccount(
id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""),
label = r.getOrElse("label", ""),
nationalIdentifier = r.getOrElse("nationalIdentifier ", ""),
swift_bic = Some(r.getOrElse("swift_bic", "")), //TODO: need to add this to the json/model
iban = Some(r.getOrElse("iban", "")),
number = r.getOrElse("number", ""),
bankName = r.getOrElse("bankName", ""),
kind = r.getOrElse("accountType", ""),
originalPartyBankId = new BankId(r.getOrElse("bankId", "")),
originalPartyAccountId = new AccountId(r.getOrElse("accountId", "")),
alreadyFoundMetadata = alreadyFoundMetadata
)
}
//creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init)
val dummyOtherBankAccount = createOtherBankAccount(None)
//and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object
//note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init
val otherAccount = createOtherBankAccount(Some(dummyOtherBankAccount.metadata))
Full(
new Transaction(
TransactionId(r.getOrElse("accountId", "")).value, // uuid:String
TransactionId(r.getOrElse("accountId", "")), // id:TransactionId
getBankAccount(BankId(r.getOrElse("bankId", "")), AccountId(r.getOrElse("accountId", ""))).openOr(null), // thisAccount:BankAccount
otherAccount, // otherAccount:OtherBankAccount
r.getOrElse("transactionType", ""), // transactionType:String
BigDecimal(r.getOrElse("amount", "0.0")), // val amount:BigDecimal
r.getOrElse("currency", ""), // currency:String
Some(r.getOrElse("description", "")), // description:Option[String]
new SimpleDateFormat("EEE MMMM d HH:mm:ss z yyyy", Locale.ENGLISH).parse(r.getOrElse("startDate", "Thu Jan 1 00:00:00 UTC 1970")), // startDate:Date
new SimpleDateFormat("EEE MMMM d HH:mm:ss z yyyy", Locale.ENGLISH).parse(r.getOrElse("finishDate", "Thu Jan 1 00:00:00 UTC 1970")), // finishDate:Date
BigDecimal(r.getOrElse("balance", "0.0")) // balance:BigDecimal
))
}
// Gets transactions identified by bankid, accountid and filtered by queryparams
def getTransactions(bankId: BankId, accountId: AccountId, queryParams: OBPQueryParam*): Box[List[Transaction]] = {
// Generate random uuid to be used as request-respose match id
val reqId: String = UUID.randomUUID().toString
// Create Kafka producer, using list of brokers from Zookeeper
val producer: KafkaProducer = new KafkaProducer()
// Send request to Kafka, marked with reqId
// so we can fetch the corresponding response
val argList = Map( "bankId" -> bankId.toString,
"accountId" -> accountId.toString,
"queryParams" -> queryParams.toString )
producer.send(reqId, "getTransactions", argList, "1")
// Request sent, now we wait for response with the same reqId
val consumer = new KafkaConsumer()
// Create entry only for the first item on returned list
val rList = consumer.getResponse(reqId)
// Loop through list of responses and create entry for each
val res = { for ( r <- rList ) yield {
// helper for creating otherbankaccount
def createOtherBankAccount(alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = {
// TODO Can we improve this? Remove duplication / have a more direct mapping of objects on the queue?
new OtherBankAccount(
id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""),
label = r.getOrElse("label", ""),
nationalIdentifier = r.getOrElse("nationalIdentifier ", ""),
swift_bic = Some(r.getOrElse("swift_bic", "")), //TODO: need to add this to the json/model
iban = Some(r.getOrElse("iban", "")),
number = r.getOrElse("number", ""),
bankName = r.getOrElse("bankName", ""),
kind = r.getOrElse("accountType", ""),
originalPartyBankId = new BankId(r.getOrElse("bankId", "")),
originalPartyAccountId = new AccountId(r.getOrElse("accountId", "")),
alreadyFoundMetadata = alreadyFoundMetadata
)
}
//creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init)
val dummyOtherBankAccount = createOtherBankAccount(None)
//and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object
//note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init
val otherAccount = createOtherBankAccount(Some(dummyOtherBankAccount.metadata))
new Transaction(
TransactionId(r.getOrElse("accountId", "")).value, // uuid:String
TransactionId(r.getOrElse("accountId", "")), // id:TransactionId
getBankAccount(BankId(r.getOrElse("bankId", "")), AccountId(r.getOrElse("accountId", ""))).openOr(null), // thisAccount:BankAccount
otherAccount, // otherAccount:OtherBankAccount
r.getOrElse("transactionType", ""), // transactionType:String
BigDecimal(r.getOrElse("amount", "0.0")), // val amount:BigDecimal
r.getOrElse("currency", ""), // currency:String
Some(r.getOrElse("description", "")), // description:Option[String]
new SimpleDateFormat("EEE MMMM d HH:mm:ss z yyyy", Locale.ENGLISH).parse(r.getOrElse("startDate", "Thu Jan 1 00:00:00 UTC 1970")), // startDate:Date
new SimpleDateFormat("EEE MMMM d HH:mm:ss z yyyy", Locale.ENGLISH).parse(r.getOrElse("finishDate", "Thu Jan 1 00:00:00 UTC 1970")), // finishDate:Date
BigDecimal(r.getOrElse("balance", "0.0")) // balance:BigDecimal
)
}
}
Full(res)
}
def getBankAccountType(bankId: code.model.BankId, accountId: code.model.AccountId): net.liftweb.common.Box[code.bankconnectors.KafkaConnector.AccountType] = ???
def accountExists(bankId: code.model.BankId,accountNumber: String): Boolean = ???
def addCashTransactionAndUpdateBalance(account: code.bankconnectors.KafkaConnector.AccountType,cashTransaction: code.tesobe.CashTransaction): Unit = ???
def createBankAndAccount(bankName: String,bankNationalIdentifier: String,accountNumber: String,accountHolderName: String): (code.model.Bank, code.model.BankAccount) = ???
def createImportedTransaction(transaction: code.management.ImporterAPI.ImporterTransaction): net.liftweb.common.Box[code.model.Transaction] = ???
def createSandboxBankAccount(bankId: code.model.BankId,accountId: code.model.AccountId,accountNumber: String,currency: String,initialBalance: BigDecimal,accountHolderName: String): net.liftweb.common.Box[code.model.BankAccount] = ???
protected def createTransactionRequestImpl(transactionRequestId: code.model.TransactionRequestId,transactionRequestType: code.model.TransactionRequestType,fromAccount: code.model.BankAccount,counterparty: code.model.BankAccount,body: code.transactionrequests.TransactionRequests.TransactionRequestBody,status: String): net.liftweb.common.Box[code.transactionrequests.TransactionRequests.TransactionRequest] = ???
def getAccountByUUID(uuid: String): net.liftweb.common.Box[code.bankconnectors.KafkaConnector.AccountType] = ???
def getAccountHolders(bankId: code.model.BankId,accountID: code.model.AccountId): Set[code.model.User] = ???
def getMatchingTransactionCount(bankNationalIdentifier: String,accountNumber: String,amount: String,completed: java.util.Date,otherAccountHolder: String): Int = ???
def getOtherBankAccount(bankId: code.model.BankId,accountID: code.model.AccountId,otherAccountID: String): net.liftweb.common.Box[code.model.OtherBankAccount] = ???
def getOtherBankAccounts(bankId: code.model.BankId,accountID: code.model.AccountId): List[code.model.OtherBankAccount] = ???
def getPhysicalCards(user: code.model.User): Set[code.model.PhysicalCard] = ???
def getPhysicalCardsForBank(bankId: code.model.BankId,user: code.model.User): Set[code.model.PhysicalCard] = ???
protected def getTransactionRequestImpl(transactionRequestId: code.model.TransactionRequestId): net.liftweb.common.Box[code.transactionrequests.TransactionRequests.TransactionRequest] = ???
protected def getTransactionRequestTypesImpl(fromAccount: code.model.BankAccount): net.liftweb.common.Box[List[code.model.TransactionRequestType]] = ???
protected def getTransactionRequestsImpl(fromAccount: code.model.BankAccount): net.liftweb.common.Box[List[code.transactionrequests.TransactionRequests.TransactionRequest]] = ???
protected def makePaymentImpl(fromAccount: code.bankconnectors.KafkaConnector.AccountType,toAccount: code.bankconnectors.KafkaConnector.AccountType,amt: BigDecimal,description: String): net.liftweb.common.Box[code.model.TransactionId] = ???
def removeAccount(bankId: code.model.BankId,accountId: code.model.AccountId): Boolean = ???
protected def saveTransactionRequestChallengeImpl(transactionRequestId: code.model.TransactionRequestId,challenge: code.transactionrequests.TransactionRequests.TransactionRequestChallenge): net.liftweb.common.Box[Boolean] = ???
protected def saveTransactionRequestStatusImpl(transactionRequestId: code.model.TransactionRequestId,status: String): net.liftweb.common.Box[Boolean] = ???
protected def saveTransactionRequestTransactionImpl(transactionRequestId: code.model.TransactionRequestId,transactionId: code.model.TransactionId): net.liftweb.common.Box[Boolean] = ???
def setAccountHolder(bankAccountUID: code.model.BankAccountUID,user: code.model.User): Unit = ???
def setBankAccountLastUpdated(bankNationalIdentifier: String,accountNumber: String,updateDate: java.util.Date): Boolean = ???
def updateAccountBalance(bankId: code.model.BankId,accountId: code.model.AccountId,newBalance: BigDecimal): Boolean = ???
def updateAccountLabel(bankId: code.model.BankId,accountId: code.model.AccountId,label: String): Boolean = ???
}

View File

@ -25,19 +25,13 @@ Berlin 13359, Germany
import java.util.{Properties, UUID}
import scala.concurrent.ops._
import scala.concurrent.duration._
import net.liftweb.util.Props
import kafka.utils.{ZkUtils, ZKStringSerializer}
import org.I0Itec.zkclient.ZkClient
import kafka.consumer.Consumer
import kafka.consumer._
import kafka.consumer.KafkaStream
import kafka.consumer.{Consumer, _}
import kafka.message._
import kafka.producer.{KeyedMessage, Producer, ProducerConfig}
import kafka.utils.{Json, ZKStringSerializer, ZkUtils}
import net.liftweb.json.DefaultFormats
import net.liftweb.util.Props
import org.I0Itec.zkclient.ZkClient
object ZooKeeperUtils {
// gets brokers tracked by zookeeper
@ -89,6 +83,7 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host")ope
val config = new ConsumerConfig(props)
config
}
def getResponse(reqId: String): List[Map[String, String]] = {
// create single stream for topic
val topicCountMap = Map(topic -> 1)
@ -107,12 +102,14 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host")ope
if (key == reqId) {
// disconnect from kafka
shutdown()
// split result if it contains multiple answers
val msgList = msg.split("\\},\\{")
// match '"<key>":"<value>"', with possible space after colon
val p = """"([a-zA-Z0-9_-]*?)":"(.*?)"""".r
val r = (for( m <- msgList) yield (for( p(k, v) <- p.findAllIn(m) ) yield (k -> v)).toMap[String, String]).toList
return r;
// Parse JSON message
val json = Json.parseFull(msg)
val r = json.get match {
case l: List[Map[String,String]] => l.asInstanceOf[List[Map[String, String]]]
case m: Map[String,String] => List(m.asInstanceOf[Map[String, String]])
case _ => List(Map("error" -> "incorrect JSON format"))
}
return r
}
}
}
@ -129,7 +126,7 @@ class KafkaConsumer(val zookeeper: String = Props.get("kafka.zookeeper_host")ope
}
}
import ZooKeeperUtils._
import code.bankconnectors.ZooKeeperUtils._
case class KafkaProducer(
topic: String = Props.get("kafka.request_topic").openOrThrowException("no kafka.request_topic set"),
@ -170,11 +167,23 @@ case class KafkaProducer(
}
}
//case class Argument(name: String, value: String)
case class Tweet(
username: String,
tweet: String,
date: String
)
implicit val formats = DefaultFormats
def send(key: String, request: String, arguments: Map[String, String], partition: String = null): Unit = {
// create string from named map of arguments
val args = (for ( (k,v) <- arguments ) yield { s""""$k":"$v",""" }).mkString.replaceAll(",$", "")
// create message using request and arguments strings
val message = s"$request:{$args}"
val reqArguments = arguments.map { args => Map(args._1 -> args._2) }
val reqCommand = Map(request -> reqArguments)
val message = Json.encode(reqCommand)
// translate strings to utf8 before sending to kafka
send(key.getBytes("UTF8"), message.getBytes("UTF8"), if (partition == null) null else partition.getBytes("UTF8"))
}
@ -189,4 +198,3 @@ case class KafkaProducer(
}
}
}

View File

@ -106,6 +106,7 @@ object LocalMappedConnector extends Connector with Loggable {
}
}
// Question: Why is this called getBankAccountType? Why not getBankAccount?
override def getBankAccountType(bankId: BankId, accountId: AccountId): Box[MappedBankAccount] = {
MappedBankAccount.find(
By(MappedBankAccount.bank, bankId.value),

View File

@ -19,7 +19,15 @@ trait CustomerProvider {
def getUser(bankId : BankId, customerId : String) : Box[User]
def addCustomer(bankId : BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage) : Box[Customer]
def addCustomer(bankId: BankId, user: User, number: String, legalName: String, mobileNumber: String, email: String, faceImage: CustomerFaceImage,
dateOfBirth: Date,
relationshipStatus: String,
dependents: Int,
dobOfDependents: List[Date],
highestEducationAttained: String,
employmentStatus: String,
kycStatus: Boolean,
lastOkDate: Date): Box[Customer]
}
@ -29,6 +37,14 @@ trait Customer {
def mobileNumber : String
def email : String
def faceImage : CustomerFaceImage
def dateOfBirth: Date
def relationshipStatus: String
def dependents: Int
def dobOfDependents: List[Date]
def highestEducationAttained: String
def employmentStatus: String
def kycStatus: Boolean
def lastOkDate: Date
}
trait CustomerFaceImage {

View File

@ -23,12 +23,25 @@ object MappedCustomerProvider extends CustomerProvider {
).flatMap(_.mUser.obj)
}
override def addCustomer(bankId: BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage) : Box[Customer] = {
override def addCustomer(bankId: BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage,
dateOfBirth: Date,
relationshipStatus: String,
dependents: Int,
dobOfDependents: List[Date],
highestEducationAttained: String,
employmentStatus: String,
kycStatus: Boolean,
lastOkDate: Date) : Box[Customer] = {
val createdCustomer = MappedCustomer.create
.mBank(bankId.value).mEmail(email).mFaceImageTime(faceImage.date)
.mFaceImageUrl(faceImage.url).mLegalName(legalName)
.mMobileNumber(mobileNumber).mNumber(number).mUser(user.apiId.value).saveMe()
.mMobileNumber(mobileNumber).mNumber(number).mUser(user.apiId.value)
.mDateOfBirth(dateOfBirth).mRelationshipStatus(relationshipStatus)
.mDependents(dependents)
.mHighestEducationAttained(highestEducationAttained)
.mEmploymentStatus(employmentStatus).mKycStatus(kycStatus)
.mLastOkDate(lastOkDate).saveMe()
Some(createdCustomer)
}
@ -48,6 +61,13 @@ class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with
object mEmail extends MappedEmail(this, 200)
object mFaceImageUrl extends DefaultStringField(this)
object mFaceImageTime extends MappedDateTime(this)
object mDateOfBirth extends MappedDateTime(this)
object mRelationshipStatus extends DefaultStringField(this)
object mDependents extends MappedInt(this)
object mHighestEducationAttained extends DefaultStringField(this)
object mEmploymentStatus extends DefaultStringField(this)
object mKycStatus extends MappedBoolean(this)
object mLastOkDate extends MappedDateTime(this)
override def number: String = mNumber.get
override def mobileNumber: String = mMobileNumber.get
@ -57,6 +77,14 @@ class MappedCustomer extends Customer with LongKeyedMapper[MappedCustomer] with
override def date: Date = mFaceImageTime.get
override def url: String = mFaceImageUrl.get
}
override def dateOfBirth: Date = mDateOfBirth.get
override def relationshipStatus: String = mRelationshipStatus.get
override def dependents: Int = mDependents
override def dobOfDependents: List[Date] = List(createdAt.get)
override def highestEducationAttained: String = mHighestEducationAttained.get
override def employmentStatus: String = mEmploymentStatus.get
override def kycStatus: Boolean = mKycStatus
override def lastOkDate: Date = mLastOkDate.get
}
object MappedCustomer extends MappedCustomer with LongKeyedMetaMapper[MappedCustomer] {

View File

@ -0,0 +1,32 @@
package code.kycchecks
import java.util.Date
import net.liftweb.util.SimpleInjector
object KycChecks extends SimpleInjector {
val kycCheckProvider = new Inject(buildOne _) {}
def buildOne: KycCheckProvider = MappedKycChecksProvider
}
trait KycCheckProvider {
def getKycChecks(customerNumber: String) : List[KycCheck]
def addKycChecks(id: String, customerNumber: String, date: Date, how: String, staffUserId: String, mStaffName: String, mSatisfied: Boolean, comments: String) : Boolean
}
trait KycCheck {
def idKycCheck : String
def customerNumber : String
def date : Date
def how : String
def staffUserId : String
def staffName : String
def satisfied: Boolean
def comments : String
}

View File

@ -0,0 +1,64 @@
package code.kycchecks
import java.util.Date
import code.model.{BankId, User}
import code.model.dataAccess.APIUser
import code.util.{DefaultStringField}
import net.liftweb.mapper._
object MappedKycChecksProvider extends KycCheckProvider {
override def getKycChecks(customerNumber: String): List[MappedKycCheck] = {
MappedKycCheck.findAll(
By(MappedKycCheck.mCustomerNumber, customerNumber),
OrderBy(MappedKycCheck.updatedAt, Descending))
}
override def addKycChecks(id: String, customerNumber: String, date: Date, how: String, staffUserId: String, mStaffName: String, mSatisfied: Boolean, comments: String): Boolean = {
MappedKycCheck.create
.mId(id)
.mCustomerNumber(customerNumber)
.mDate(date)
.mHow(how)
.mStaffUserId(staffUserId)
.mStaffName(mStaffName)
.mSatisfied(mSatisfied)
.mComments(comments)
.save()
}
}
class MappedKycCheck extends KycCheck
with LongKeyedMapper[MappedKycCheck] with IdPK with CreatedUpdated {
def getSingleton = MappedKycCheck
object user extends MappedLongForeignKey(this, APIUser)
object bank extends DefaultStringField(this)
object mId extends DefaultStringField(this)
object mCustomerNumber extends DefaultStringField(this)
object mDate extends MappedDateTime(this)
object mHow extends DefaultStringField(this)
object mStaffUserId extends DefaultStringField(this)
object mStaffName extends DefaultStringField(this)
object mSatisfied extends MappedBoolean(this)
object mComments extends DefaultStringField(this)
override def idKycCheck: String = mId.get
override def customerNumber: String = mCustomerNumber.get
override def date: Date = mDate.get
override def how: String = mHow.get
override def staffUserId: String = mStaffUserId.get
override def staffName: String = mStaffName.get
override def satisfied: Boolean = mSatisfied.get
override def comments: String = mComments.get
}
object MappedKycCheck extends MappedKycCheck with LongKeyedMetaMapper[MappedKycCheck] {
override def dbIndexes = UniqueIndex(mId) :: super.dbIndexes
}

View File

@ -0,0 +1,31 @@
package code.kycdocuments
import java.util.Date
import net.liftweb.util.SimpleInjector
object KycDocuments extends SimpleInjector {
val kycDocumentProvider = new Inject(buildOne _) {}
def buildOne: KycDocumentProvider = MappedKycDocumentsProvider
}
trait KycDocumentProvider {
def getKycDocuments(customerNumber: String) : List[KycDocument]
def addKycDocuments(id: String, customerNumber: String, `type`: String, number: String, issueDate: Date, issuePlace: String, expiryDate: Date) : Boolean
}
trait KycDocument {
def idKycDocument : String
def customerNumber : String
def `type` : String
def number : String
def issueDate : Date
def issuePlace : String
def expiryDate : Date
}

View File

@ -0,0 +1,60 @@
package code.kycdocuments
import java.util.Date
import code.model.{BankId, User}
import code.model.dataAccess.APIUser
import code.util.{DefaultStringField}
import net.liftweb.mapper._
object MappedKycDocumentsProvider extends KycDocumentProvider {
override def getKycDocuments(customerNumber: String): List[MappedKycDocument] = {
MappedKycDocument.findAll(
By(MappedKycDocument.mCustomerNumber, customerNumber),
OrderBy(MappedKycDocument.updatedAt, Descending))
}
override def addKycDocuments(id: String, customerNumber: String, `type`: String, number: String, issueDate: Date, issuePlace: String, expiryDate: Date): Boolean = {
MappedKycDocument.create
.mId(id)
.mCustomerNumber(customerNumber)
.mType(`type`)
.mNumber(number)
.mIssueDate(issueDate)
.mIssuePlace(issuePlace)
.mExpiryDate(expiryDate)
.save()
}
}
class MappedKycDocument extends KycDocument
with LongKeyedMapper[MappedKycDocument] with IdPK with CreatedUpdated {
def getSingleton = MappedKycDocument
object user extends MappedLongForeignKey(this, APIUser)
object bank extends DefaultStringField(this)
object mId extends DefaultStringField(this)
object mCustomerNumber extends DefaultStringField(this)
object mType extends DefaultStringField(this)
object mNumber extends DefaultStringField(this)
object mIssueDate extends MappedDateTime(this)
object mIssuePlace extends DefaultStringField(this)
object mExpiryDate extends MappedDateTime(this)
override def idKycDocument: String = mId.get
override def customerNumber: String = mCustomerNumber.get
override def `type`: String = mType.get
override def number: String = mNumber.get
override def issueDate: Date = mIssueDate.get
override def issuePlace: String = mIssuePlace.get
override def expiryDate: Date = mExpiryDate.get
}
object MappedKycDocument extends MappedKycDocument with LongKeyedMetaMapper[MappedKycDocument] {
override def dbIndexes = UniqueIndex(mId) :: super.dbIndexes
}

View File

@ -0,0 +1,33 @@
package code.kycmedias
import java.util.Date
import code.model.{BankId, User}
import net.liftweb.util.SimpleInjector
object KycMedias extends SimpleInjector {
val kycMediaProvider = new Inject(buildOne _) {}
def buildOne: KycMediaProvider = MappedKycMediasProvider
}
trait KycMediaProvider {
def getKycMedias(customerNumber: String) : List[KycMedia]
def addKycMedias(id: String, customerNumber: String, `type`: String, url: String, date: Date, relatesToKycDocumentId: String, relatesToKycCheckId: String) : Boolean
}
trait KycMedia {
def idKycMedia : String
def customerNumber : String
def `type` : String
def url : String
def date : Date
def relatesToKycDocumentId : String
def relatesToKycCheckId : String
}

View File

@ -0,0 +1,60 @@
package code.kycmedias
import java.util.Date
import code.model.{BankId, User}
import code.model.dataAccess.APIUser
import code.util.{DefaultStringField}
import net.liftweb.mapper._
object MappedKycMediasProvider extends KycMediaProvider {
override def getKycMedias(customerNumber: String): List[MappedKycMedia] = {
MappedKycMedia.findAll(
By(MappedKycMedia.mCustomerNumber, customerNumber),
OrderBy(MappedKycMedia.updatedAt, Descending))
}
override def addKycMedias(id: String, customerNumber: String, `type`: String, url: String, date: Date, relatesToKycDocumentId: String, relatesToKycCheckId: String): Boolean = {
MappedKycMedia.create
.mId(id)
.mCustomerNumber(customerNumber)
.mType(`type`)
.mUrl(url)
.mDate(date)
.mRelatesToKycDocumentId(relatesToKycDocumentId)
.mRelatesToKycCheckId(relatesToKycCheckId)
.save()
}
}
class MappedKycMedia extends KycMedia
with LongKeyedMapper[MappedKycMedia] with IdPK with CreatedUpdated {
def getSingleton = MappedKycMedia
object user extends MappedLongForeignKey(this, APIUser)
object bank extends DefaultStringField(this)
object mId extends DefaultStringField(this)
object mCustomerNumber extends DefaultStringField(this)
object mType extends DefaultStringField(this)
object mUrl extends DefaultStringField(this)
object mDate extends MappedDateTime(this)
object mRelatesToKycDocumentId extends DefaultStringField(this)
object mRelatesToKycCheckId extends DefaultStringField(this)
override def idKycMedia: String = mId.get
override def customerNumber: String = mCustomerNumber.get
override def `type`: String = mType.get
override def url: String = mUrl.get
override def date: Date = mDate.get
override def relatesToKycDocumentId: String = mRelatesToKycDocumentId.get
override def relatesToKycCheckId: String = mRelatesToKycCheckId.get
}
object MappedKycMedia extends MappedKycMedia with LongKeyedMetaMapper[MappedKycMedia] {
override def dbIndexes = UniqueIndex(mId) :: super.dbIndexes
}

View File

@ -0,0 +1,29 @@
package code.kycstatuses
import java.util.Date
import code.model.{BankId, User}
import net.liftweb.util.SimpleInjector
object KycStatuses extends SimpleInjector {
val kycStatusProvider = new Inject(buildOne _) {}
def buildOne: KycStatusProvider = MappedKycStatusesProvider
}
trait KycStatusProvider {
def getKycStatuses(customerNumber: String) : List[KycStatus]
def addKycStatus(customerNumber: String, ok: Boolean, date: Date) : Boolean
}
trait KycStatus {
def customerNumber : String
def ok : Boolean
def date : Date
}

View File

@ -0,0 +1,50 @@
package code.kycstatuses
import java.util.Date
import code.model.{BankId, User}
import code.model.dataAccess.APIUser
import code.util.{DefaultStringField}
import net.liftweb.mapper._
object MappedKycStatusesProvider extends KycStatusProvider {
override def getKycStatuses(customerNumber: String): List[MappedKycStatus] = {
MappedKycStatus.findAll(
By(MappedKycStatus.mCustomerNumber, customerNumber),
OrderBy(MappedKycStatus.updatedAt, Descending))
}
override def addKycStatus(customerNumber: String, ok: Boolean, date: Date): Boolean = {
MappedKycStatus.create
.mCustomerNumber(customerNumber)
.mOk(ok)
.mDate(date)
.save()
}
}
class MappedKycStatus extends KycStatus
with LongKeyedMapper[MappedKycStatus] with IdPK with CreatedUpdated {
def getSingleton = MappedKycStatus
object user extends MappedLongForeignKey(this, APIUser)
object bank extends DefaultStringField(this)
object mCustomerNumber extends DefaultStringField(this)
object mOk extends MappedBoolean(this)
object mDate extends MappedDateTime(this)
override def customerNumber: String = mCustomerNumber.get
override def ok: Boolean = mOk.get
override def date: Date = mDate.get
}
object MappedKycStatus extends MappedKycStatus with LongKeyedMetaMapper[MappedKycStatus] {
override def dbIndexes = UniqueIndex(mCustomerNumber) :: super.dbIndexes
}

View File

@ -184,7 +184,7 @@ object ImporterAPI extends RestHelper with Loggable {
savetransactions
else
errorJsonResponse("wrong secret", 401)
case _ => errorJsonResponse("importer_secret not set")
case _ => errorJsonResponse("importer_secret not set on the server.")
}
}
case _ => errorJsonResponse("secret missing")

View File

@ -104,6 +104,15 @@ object BankId {
def unapply(id : String) = Some(BankId(id))
}
// In preparation for use in Context (api links) To replace OtherAccountId
case class CounterpartyId(val value : String) {
override def toString = value
}
object CounterpartyId {
def unapply(id : String) = Some(CounterpartyId(id))
}
trait Bank {
def bankId: BankId
def shortName : String

View File

@ -142,15 +142,10 @@ object Consumer extends Consumer with LongKeyedMetaMapper[Consumer] with CRUDify
//overridden to display extra stats above the table
override def _showAllTemplate =
<lift:crud.all>
<div>
<p>
Total of {Consumer.count} applications from {recordsWithUniqueEmails.getOrElse("ERROR")} unique email addresses.
{recordsWithUniqueAppNames.getOrElse("ERROR")} unique app names.
</p>
<br/>
<br/>
<br/>
</div>
<p id="admin-consumer-summary">
Total of {Consumer.count} applications from {recordsWithUniqueEmails.getOrElse("ERROR")} unique email addresses. <br />
{recordsWithUniqueAppNames.getOrElse("ERROR")} unique app names.
</p>
<table id={showAllId} class={showAllClass}>
<thead>
<tr>

View File

@ -59,6 +59,7 @@ object Admin extends Admin with MetaMegaProtoUser[Admin]{
locale, timezone, password)
// comment this line out to require email validations
// TODO Get this from Props
override def skipEmailValidation = true
//Keep track of the referer on login

View File

@ -292,6 +292,74 @@ import net.liftweb.util.Helpers._
S.error("login", S.?("Invalid Username or Password"))
}
def getUserId(username: String, password: String): Long = {
findUserByUserName(username) match {
case Full(user) => {
if (user.validated_? &&
user.getProvider() == Props.get("hostname","") &&
user.testPassword(Full(password)))
{
user.id.toLong
}
else {
getExternalUser(username, password).get.id.toLong
}
}
case _ => 0
}
}
def getExternalUser(username: String, password: String):Box[OBPUser] = {
getUserViaKafka(username, password) match {
case Full(SandboxUserImport(extEmail, extPassword, extDisplayName)) => {
val preLoginState = capturePreLoginState()
info("external user authenticated. login redir: " + loginRedirect.get)
val redir = loginRedirect.get match {
case Full(url) =>
loginRedirect(Empty)
url
case _ =>
homePage
}
val dummyPassword = "nothingreallyjustdummypass"
val extProvider = Props.get("connector").openOrThrowException("no connector set")
val user = findUserByUserName(username) match {
// Check if the external user is already created locally
case Full(user) if user.validated_? &&
user.provider == extProvider => {
// Return existing user if found
info("external user already exists locally, using that one")
user
}
// If not found, create new user
case _ => {
// Create OBPUser using fetched data from Kafka
// assuming that user's email is always validated
info("external user does not exist locally, creating one")
val newUser = OBPUser.create
.firstName(extDisplayName)
.email(extEmail)
// No need to store password, so store dummy string instead
.password(dummyPassword)
.provider(extProvider)
.validated(true)
// Save the user in order to be able to log in
newUser.save()
// Return created user
newUser
}
}
Full(user)
}
case _ => {
Empty
}
}
}
//overridden to allow a redirection if login fails
override def login = {
if (S.post_?) {
@ -326,67 +394,22 @@ import net.liftweb.util.Helpers._
case _ => {
// If not found locally, try to authenticate user via Kafka, if enabled in props
if (Props.get("connector").openOrThrowException("no connector set") == "kafka") {
S.param("username").
flatMap(username => getUserViaKafka(username, S.param("password").openOr(""))) match {
case Full(SandboxUserImport(extEmail, extPassword, extDisplayName)) => {
val preLoginState = capturePreLoginState()
info("external user authenticated. login redir: " + loginRedirect.get)
val redir = loginRedirect.get match {
case Full(url) =>
loginRedirect(Empty)
url
case _ =>
homePage
}
val preLoginState = capturePreLoginState()
val user = getExternalUser(S.param("username").get, S.param("password").get)
val dummyPassword = "nothingreallyjustdummypass"
val extProvider = Props.get("connector").openOrThrowException("no connector set")
if (!user.isEmpty) {
logUserIn(user.get, () => {
S.notice(S.?("logged.in"))
val user = S.param("username").
flatMap(username => findUserByUserName(username)) match {
preLoginState()
// Check if the external user is already created locally
case Full(user) if user.validated_? &&
user.provider == extProvider => {
// Return existing user if found
info("external user already exists locally, using that one")
user
}
// If not found, create new user
case _ => {
// Create OBPUser using fetched data from Kafka
// assuming that user's email is always validated
info("external user does not exist locally, creating one")
val newUser = OBPUser.create
.firstName(extDisplayName)
.email(extEmail)
// No need to store password, so store dummy string instead
.password(dummyPassword)
.provider(extProvider)
.validated(true)
// Save the user in order to be able to log in
newUser.save()
// Return created user
newUser
}
}
logUserIn(user, () => {
S.notice(S.?("logged.in"))
preLoginState()
S.redirectTo(homePage)
})
}
case _ => {
userLoginFailed
}
S.redirectTo(homePage)
})
} else {
userLoginFailed
}
} else {
userLoginFailed
userLoginFailed
}
}
}

View File

@ -487,7 +487,6 @@ trait OBPDataImport extends Loggable {
* @return A full box if the import worked, or else a failure describing what went wrong
*/
def importData(data: SandboxDataImport) : Box[Unit] = {
logger.info(s"Hello from importData")
for {
banks <- createBanks(data)

View File

@ -33,9 +33,10 @@ Berlin 13359, Germany
package code.snippet
import code.model.dataAccess.OBPUser
import net.liftweb.common.Loggable
import scala.xml.NodeSeq
import net.liftweb.util.Helpers._
import net.liftweb.util.CssSel
import net.liftweb.util.{Props, CssSel}
import net.liftweb.http.S
import code.model.dataAccess.Admin
import net.liftweb.http.SHtml
@ -70,7 +71,7 @@ class Login {
}
}
}
def adminLogout : CssSel = {
if(Admin.loggedIn_?) {
val current = Admin.currentUser
@ -84,4 +85,19 @@ class Login {
}
}
// Used to display custom message to users when they login.
// For instance we can use it to display example login on a sandbox
def customiseLogin : CssSel = {
val specialLoginInstructions = scala.xml.Unparsed(Props.get("webui_login_page_special_instructions", ""))
// In case we use Extraction.decompose
implicit val formats = net.liftweb.json.DefaultFormats
"#login_special_instructions *" #> specialLoginInstructions
}
// End of class
}

View File

@ -1,32 +1,37 @@
/**
Open Bank Project
Open Bank Project - API
Copyright (C) 2011 - 2015, TESOBE Ltd.
Copyright 2011,2012 TESOBE / Music Pictures Ltd.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
http://www.apache.org/licenses/LICENSE-2.0
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This product includes software developed at
TESOBE (http://www.tesobe.com/)
by
Simon Redfern : simon AT tesobe DOT com
Stefan Bethge : stefan AT tesobe DOT com
Everett Sochowski : everett AT tesobe DOT com
Ayoub Benali: ayoub AT tesobe DOT com
Open Bank Project (http://www.openbankproject.com)
Copyright 2011,2012 TESOBE / Music Pictures Ltd
This product includes software developed at
TESOBE (http://www.tesobe.com/)
TESOBE Ltd.
Osloer Str. 16/17
Berlin 13359, Germany
Email: contact@tesobe.com
*/
by
Simon Redfern : simon AT tesobe DOT com
Everett Sochowski: everett AT tesobe DOT com
Ayoub Benali : ayoub AT tesobe Dot com
*/
package code.snippet
import code.util.Helper
@ -168,7 +173,7 @@ object OAuthAuthorisation {
}
//looks for expired tokens and nonces and delete them
//looks for expired tokens and nonces and deletes them
def dataBaseCleaner: Unit = {
import net.liftweb.util.Schedule
import net.liftweb.mapper.By_<

View File

@ -63,10 +63,27 @@ class WebUI extends Loggable{
".api-explorer-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_explorer_url", ""))
}
// Social Finance (Sofi)
def sofiLink: CssSel = {
".sofi-link a [href]" #> scala.xml.Unparsed(Props.get("webui_sofi_url", ""))
}
// Points to the documentation. Probably a sandbox specific link is good.
def apiDocumentationLink: CssSel = {
".api-documentation-link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_documentation_url", "https://github.com/OpenBankProject/OBP-API/wiki"))
}
// For example customers and credentials
// This relies on the page for sandbox documentation having an anchor called example-customer-logins
def exampleSandboxCredentialsLink: CssSel = {
".example_sandbox_credentials_link a [href]" #> scala.xml.Unparsed(Props.get("webui_api_documentation_url", "") + "#example-customer-logins")
}
// For link to OAuth Client SDKs
def sdksLink: CssSel = {
".sdks_link a [href]" #> scala.xml.Unparsed(Props.get("webui_sdks_url", "https://github.com/OpenBankProject/OBP-API/wiki/OAuth-Client-SDKS"))
}
def mainStyleSheet: CssSel = {
"#main_style_sheet [href]" #> scala.xml.Unparsed(Props.get("webui_main_style_sheet", "/media/css/website.css"))

View File

@ -0,0 +1,52 @@
package code.socialmedia
import java.util.Date
import code.model.dataAccess.APIUser
import code.util.{DefaultStringField}
import net.liftweb.mapper._
object MappedSocialMediasProvider extends SocialMediaHandleProvider {
override def getSocialMedias(customerNumber: String): List[MappedSocialMedia] = {
MappedSocialMedia.findAll(
By(MappedSocialMedia.mCustomerNumber, customerNumber),
OrderBy(MappedSocialMedia.updatedAt, Descending))
}
override def addSocialMedias(customerNumber: String, `type`: String, handle: String, dateAdded: Date, dateActivated: Date): Boolean = {
MappedSocialMedia.create
.mCustomerNumber(customerNumber)
.mType(`type`)
.mHandle(handle)
.mDateAdded(dateAdded)
.mDateActivated(dateActivated)
.save()
}
}
class MappedSocialMedia extends SocialMedia
with LongKeyedMapper[MappedSocialMedia] with IdPK with CreatedUpdated {
def getSingleton = MappedSocialMedia
object user extends MappedLongForeignKey(this, APIUser)
object bank extends DefaultStringField(this)
object mCustomerNumber extends DefaultStringField(this)
object mType extends DefaultStringField(this)
object mHandle extends DefaultStringField(this)
object mDateAdded extends MappedDateTime(this)
object mDateActivated extends MappedDateTime(this)
override def customerNumber: String = mCustomerNumber.get
override def `type`: String = mType.get
override def handle: String = mHandle.get
override def dateAdded: Date = mDateAdded.get
override def dateActivated: Date = mDateActivated.get
}
object MappedSocialMedia extends MappedSocialMedia with LongKeyedMetaMapper[MappedSocialMedia] {
override def dbIndexes = UniqueIndex(mCustomerNumber) :: super.dbIndexes
}

View File

@ -0,0 +1,34 @@
package code.socialmedia
import java.util.Date
import net.liftweb.util.SimpleInjector
// TODO Rename to SocialMediaHandle
object SocialMediaHandle extends SimpleInjector {
val socialMediaHandleProvider = new Inject(buildOne _) {}
def buildOne: SocialMediaHandleProvider = MappedSocialMediasProvider
}
// TODO Rename to SocialMediaHandlesProvider etc.
trait SocialMediaHandleProvider {
def getSocialMedias(customerNumber: String) : List[SocialMedia]
def addSocialMedias(customerNumber: String, `type`: String, handle: String, dateAdded: Date, dateActivated: Date) : Boolean
}
// TODO Rename to SocialMediaHandle
trait SocialMedia {
def customerNumber : String
def `type` : String
def handle : String
def dateAdded : Date
def dateActivated : Date
}

View File

@ -243,6 +243,8 @@ private object MapperViews extends Views with Loggable {
def getAllPublicAccounts() : List[BankAccount] = {
//TODO: do this more efficiently
// An account is considered public if it contains a public view
val bankAndAccountIds : List[(BankId, AccountId)] =
ViewImpl.findAll(By(ViewImpl.isPublic_, true)).map(v =>
(v.bankId, v.accountId)

View File

@ -37,18 +37,17 @@ Berlin 13359, Germany
</div>
</div>
<div id="main-links">
<a href="/consumer-registration">Get API keys</a>
<a class="api-explorer-link" data-lift="WebUI.apiExplorerLink" href="">Use API Explorer</a>
<a href="/consumer-registration">API keys</a>
<a class="api-explorer-link" data-lift="WebUI.apiExplorerLink" href="">API Explorer</a>
<a class="sofi-link" data-lift="WebUI.sofiLink" href="">Sofi</a>
</div>
<div id="main-sandbox">
<h1>Get Started</h1>
<p class="about-text">
<ol start="1">
<li>Register / Login as a developer <a href="/user_mgt/sign_up">here</a></li>
<li>Get one or more developer keys <a href="/consumer-registration">here</a></li>
<li>Read the documentation <a class="api-documentation-link lift:WebUI.apiDocumentationLink" href="">here</a></li>
<li>Explore the API <a class="api-explorer-link lift:WebUI.apiExplorerLink" href="">here</a></li>
<li>Explore the API <a class="api-explorer-link" data-lift="WebUI.apiExplorerLink" href="">here</a> using the example customer logins <a class="example_sandbox_credentials_link" data-lift="WebUI.exampleSandboxCredentialsLink" href="">here</a>.</li>
<li>Get <a href="/consumer-registration">API keys</a> and <a class="sdks_link" data-lift="WebUI.sdksLink" href="">SDKs</a>.</li>
</ol>
</p>

View File

@ -0,0 +1,3 @@
#header-decoration {
background-color: #646ca1;
}

View File

@ -0,0 +1,84 @@
h1 {
color: white;
font-family: "Nunito", "sans-serif";
font-size: 36px;
margin-bottom: 10px;
text-align: center;
text-transform: uppercase;
}
#main #main-about {
padding-top: 0 !important;
}
.top {
background-color: #53C4EF;
font-family: "Nunito", "sans-serif";
padding: 40px;
}
.intro {
font-family: "Nunito", "sans-serif";
margin: 0 20px;
text-align: center;
}
.content-big {
font-family: "Nunito", "sans-serif";
margin: 30px 50px;
text-align: center;
}
.buttons {
font-family: "Nunito", "sans-serif";
margin: 40px 0;
text-align: center;
}
.buttons a {
border: 2px solid white;
border-radius: 24px;
color: white;
margin-bottom: 10px;
font-family: "Nunito", "sans-serif";
font-size: x-large;
font-weight: bold;
padding: 10px 50px;
text-decoration: none;
text-transform: uppercase;
}
.content-small {
background-color: white;
font-family: "Nunito", "sans-serif";
margin: 20px 20px 0 20px;
padding: 30px;
text-align: center;
}
.newsletter {
background-color: black;
font-family: "Nunito", "sans-serif";
margin: 0 20px;
padding: 30px;
text-align: center;
}
.newsletter input {
border: 1px solid black;
border-top-left-radius: 24px;
border-bottom-left-radius: 24px;
font-family: "Nunito", "sans-serif";
height: auto;
padding: 10px 20px;
}
.newsletter button {
border: 1px solid black;
border-left: 0px;
border-top-right-radius: 24px;
border-bottom-right-radius: 24px;
background-color: #50b165;
color: white;
font-family: "Nunito", "sans-serif";
font-size: 17px;
margin-left: -6px;
padding: 10px 30px 10px 10px;
}

View File

@ -0,0 +1,29 @@
#main-about h1 {
color: white;
font-size: 35px;
margin-bottom: 15px;
margin-left: -5px;
}
#main-about p {
color: white;
font-weight: bold;
margin-top: 10px;
margin-bottom: 10px;
}
#main-about ul
{
list-style-type: initial;
}
#main-about li
{
color: white;
text-align: left;
margin-left: 100px;
}
#main-about img {
margin-top: 30px;
}

View File

@ -894,8 +894,21 @@ span#accountsMsg {
}
#admin-logout {
margin: 20px;
padding: 10px;
padding-left: 10px;
border: 2px solid;
border-color: #FF0000;
margin-top:-6px;
padding: 4px 0 3px 10px;
}
#admin-consumer-summary {
color: black;
margin: 20px 0;
}
#login_special_instructions {
text-align: left;
}

View File

@ -1,4 +1,10 @@
<div data-lift="Login.customiseLogin">
<form class="login" action="/user_mgt/login" method="post">
<div class="login_special_instructions" id="login_special_instructions">Special Instructions</div>
<div class="field username">
<input class="username" type="email" placeholder="Username" name="username" tabindex=1 autofocus />
</div>
@ -16,4 +22,6 @@
<a href="/user_mgt/sign_up" class="signup" tabindex=5>Register</a>
</div>
</div>
</form>
</form>
</div>

View File

@ -1,6 +1,6 @@
<!--
Open Bank Project - API
Copyright (C) 2011-2015, TESOBE / Music Pictures Ltd.
Copyright (C) 2011-2015, TESOBE Ltd.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Email: contact@tesobe.com
TESOBE / Music Pictures Ltd
TESOBE Ltd.
Osloerstrasse 16/17
Berlin 13359, Germany
@ -122,7 +122,8 @@ Berlin 13359, Germany
<a href="http://twitter.com/#!/OpenBankProject">Twitter</a> |
<a href="https://github.com/OpenBankProject/OBP-API/">Github</a> |
<a class="api-documentation-link lift:WebUI.apiDocumentationLink" href="">API Documentation</a> |
<a class="api-explorer-link lift:WebUI.apiExplorerLink" href="">API Explorer</a>
<a class="api-explorer-link lift:WebUI.apiExplorerLink" href="">API Explorer</a> |
<a class="sofi-link lift:WebUI.sofiLink" href="">Sofi</a>
</div>
</footer>
</div>

View File

@ -15,12 +15,17 @@ class CustomerTest extends V140ServerSetup with DefaultUsers {
val mockBankId = BankId("testBank1")
case class MockFaceImage(date : Date, url : String) extends CustomerFaceImage
case class MockCustomer(number : String, mobileNumber : String,
legalName : String, email : String,
faceImage : MockFaceImage) extends Customer
case class MockCustomer(number: String, mobileNumber: String,
legalName: String, email: String,
faceImage: MockFaceImage, dateOfBirth: Date,
relationshipStatus: String, dependents: Int,
dobOfDependents: List[Date], highestEducationAttained: String,
employmentStatus: String, kycStatus: Boolean, lastOkDate: Date) extends Customer
val format = new java.text.SimpleDateFormat("dd/MM/yyyy")
val mockCustomerFaceImage = MockFaceImage(new Date(1234000), "http://example.com/image1")
val mockCustomer = MockCustomer("123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage)
val mockCustomer = MockCustomer("123", "3939", "Bob", "bob@example.com", mockCustomerFaceImage, new Date(1234000), "Single", 3, List(format.parse("30/03/2012"), format.parse("30/03/2012"), format.parse("30/03/2014")), "Bachelors Degree", "Employed", true, new Date(1234000))
object MockedCustomerProvider extends CustomerProvider {
override def getCustomer(bankId: BankId, user: User): Box[Customer] = {
@ -29,7 +34,15 @@ class CustomerTest extends V140ServerSetup with DefaultUsers {
}
override def getUser(bankId: BankId, customerId: String): Box[User] = Empty
override def addCustomer(bankId : BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage) : Box[Customer] = Empty
override def addCustomer(bankId : BankId, user : User, number : String, legalName : String, mobileNumber : String, email : String, faceImage: CustomerFaceImage,
dateOfBirth: Date,
relationshipStatus: String,
dependents: Int,
dobOfDependents: List[Date],
highestEducationAttained: String,
employmentStatus: String,
kycStatus: Boolean,
lastOkDate: Date) : Box[Customer] = Empty
}
override def beforeAll() {
@ -92,8 +105,19 @@ class CustomerTest extends V140ServerSetup with DefaultUsers {
And("We should get the right information back")
val info = response.body.extract[CustomerJson]
val received = MockCustomer(info.customer_number, info.mobile_phone_number,
info.legal_name, info.email, MockFaceImage(info.face_image.date, info.face_image.url))
val received = MockCustomer(info.customer_number,
info.mobile_phone_number,
info.legal_name,
info.email,
MockFaceImage(info.face_image.date, info.face_image.url),
info.date_of_birth,
info.relationship_status,
info.dependants,
info.dob_of_dependants,
info.highest_education_attained,
info.employment_status,
info.kyc_status,
info.last_ok_date)
received should equal(mockCustomer)
}

View File

@ -48,7 +48,15 @@ class MappedCustomerMessagesTest extends V140ServerSetup with DefaultUsers {
legal_name = "Someone",
mobile_phone_number = "125245",
email = "hello@hullo.com",
face_image = CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate)
face_image = CustomerFaceImageJson("www.example.com/person/123/image.png", exampleDate),
date_of_birth = exampleDate,
relationship_status = "Single",
dependants = 1,
dob_of_dependants = List(exampleDate),
highest_education_attained = "Bachelors Degree",
employment_status = "Employed",
kyc_status = true,
last_ok_date = exampleDate
)
var response = makePostRequest(request, write(customerJson))

View File

@ -3,7 +3,7 @@ package code.api.v1_4_0
import code.api.DefaultUsers
import code.api.test.{APIResponse, ServerSetupWithTestData, ServerSetup}
import code.api.util.APIUtil.OAuth.{Token, Consumer}
import code.api.v1_2_1.{TransactionsJSON, TransactionJSON, MakePaymentJson}
import code.api.v1_2_1.{TransactionsJSON, TransactionJSON, MakePaymentJson, AmountOfMoneyJSON}
import code.api.v1_4_0.JSONFactory1_4_0._
import code.bankconnectors.Connector
import code.model.{TransactionRequestId, AccountId, BankAccount}
@ -74,12 +74,12 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers
//call createTransactionRequest
var request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value /
"owner" / "transaction-request-types" / "SANDBOX" / "transaction-requests").POST <@(user1)
"owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@(user1)
var response = makePostRequest(request, write(transactionRequestBody))
Then("we should get a 201 created code")
response.code should equal(201)
//created a transaction request, check some return values. As type is SANDBOX, we expect no challenge
//created a transaction request, check some return values. As type is SANDBOX_TAN, we expect no challenge
val transId: String = (response.body \ "id" \ "value") match {
case JString(i) => i
case _ => ""
@ -141,36 +141,24 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers
}
description should not equal ("")
//TODO: check that the balances have been properly decreased/increased (since we handle that logic for sandbox accounts at least)
//check that the balances have been properly decreased/increased (since we handle that logic for sandbox accounts at least)
//(do it here even though the payments test does test makePayment already)
/* val fromAccountTransAmt = transJson.details.value.amount
//the from account transaction should have a negative value
//since money left the account
And("the json we receive back should have a transaction amount equal to the amount specified to pay")
fromAccountTransAmt should equal((-amt).toString)
val fromAccountBalance = getFromAccount.balance
And("the from account should have a balance smaller by the amount specified to pay")
fromAccountBalance should equal((beforeFromBalance - amt))
val expectedNewFromBalance = beforeFromBalance - amt
And("the account sending the payment should have a new_balance amount equal to the previous balance minus the amount paid")
transJson.details.new_balance.amount should equal(expectedNewFromBalance.toString)
getFromAccount.balance should equal(expectedNewFromBalance)
val toAccountTransactionsReq = getTransactions(toAccount.bankId.value, toAccount.accountId.value, view, user1)
toAccountTransactionsReq.code should equal(200)
val toAccountTransactions = toAccountTransactionsReq.body.extract[TransactionsJSON]
val newestToAccountTransaction = toAccountTransactions.transactions(0)
//here amt should be positive (unlike in the transaction in the "from" account")
/*
And("the newest transaction for the account receiving the payment should have the proper amount")
newestToAccountTransaction.details.value.amount should equal(amt.toString)
*/
And("the account receiving the payment should have the proper balance")
val expectedNewToBalance = beforeToBalance + amt
newestToAccountTransaction.details.new_balance.amount should equal(expectedNewToBalance.toString)
getToAccount.balance should equal(expectedNewToBalance)
And("the account receiving the payment should have a new balance plus the amount paid")
val toAccountBalance = getToAccount.balance
toAccountBalance should equal(beforeToBalance + amt)
And("there should now be 2 new transactions in the database (one for the sender, one for the receiver")
transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2)
*/
}
}
@ -216,12 +204,12 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers
//call createTransactionRequest API method
var request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value /
"owner" / "transaction-request-types" / "SANDBOX" / "transaction-requests").POST <@ (user1)
"owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@ (user1)
var response = makePostRequest(request, write(transactionRequestBody))
Then("we should get a 201 created code")
response.code should equal(201)
//ok, created a transaction request, check some return values. As type is SANDBOX but over 100, we expect a challenge
//ok, created a transaction request, check some return values. As type is SANDBOX_TAN but over 100, we expect a challenge
val transId: String = (response.body \ "id" \ "value") match {
case JString(i) => i
case _ => ""
@ -272,7 +260,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers
//call answerTransactionRequestChallenge, give a false answer
var answerJson = ChallengeAnswerJSON(id = challenge_id, answer = "hello") //wrong answer, not a number
request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value /
"owner" / "transaction-request-types" / "sandbox" / "transaction-requests" / transId / "challenge").POST <@ (user1)
"owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests" / transId / "challenge").POST <@ (user1)
response = makePostRequest(request, write(answerJson))
Then("we should get a 400 bad request code")
response.code should equal(400)
@ -282,7 +270,7 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers
//call answerTransactionRequestChallenge again, give a good answer
answerJson = ChallengeAnswerJSON(id = challenge_id, answer = "12345") //wrong answer, not a number
request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value /
"owner" / "transaction-request-types" / "sandbox" / "transaction-requests" / transId / "challenge").POST <@ (user1)
"owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests" / transId / "challenge").POST <@ (user1)
response = makePostRequest(request, write(answerJson))
Then("we should get a 202 accepted code")
response.code should equal(202)
@ -318,6 +306,25 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers
challenge = (response.body \ "challenge").children
challenge.size should not equal(0)
//check that the balances have been properly decreased/increased (since we handle that logic for sandbox accounts at least)
//(do it here even though the payments test does test makePayment already)
val fromAccountBalance = getFromAccount.balance
And("the from account should have a balance smaller by the amount specified to pay")
fromAccountBalance should equal((beforeFromBalance - amt))
/*
And("the newest transaction for the account receiving the payment should have the proper amount")
newestToAccountTransaction.details.value.amount should equal(amt.toString)
*/
And("the account receiving the payment should have a new balance plus the amount paid")
val toAccountBalance = getToAccount.balance
toAccountBalance should equal(beforeToBalance + amt)
And("there should now be 2 new transactions in the database (one for the sender, one for the receiver")
transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2)
}
}

View File

@ -42,17 +42,25 @@ import code.api.ObpJson.BarebonesAccountsJson
case class CounterpartyJSONRecord(name: String, category: String, superCategory: String, logoUrl: String, homePageUrl: String, region: String)
case class UserJSONRecord(email: String, password: String, display_name: String)
// Import counterparty metadata
// Instructions for using this:
// Run a copy of the API somewhere (else)
// Set the paths for users and counterparties. (remove the outer [] from the json)
object ImportCounterpartyMetadata extends SendServerRequests {
def main(args : Array[String]) {
implicit val formats = DefaultFormats
//load json for counterpaties
var path = "/Users/stefan/Downloads/OBP_sandbox_counterparties_pretty.json"
var path = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/ulster_bank_2016/OBP_sandbox_counterparties_pretty_MOD.json"
var records = JsonParser.parse(Source.fromFile(path) mkString)
var counterparties = ListBuffer[CounterpartyJSONRecord]()
//collect counterparties records
for(r <- records.children){
//logger.info(s" extract counterparty records")
val rec = r.extract[CounterpartyJSONRecord]
//println (rec.name + "in region " + rec.region)
counterparties.append(rec)
@ -61,7 +69,7 @@ object ImportCounterpartyMetadata extends SendServerRequests {
println("Got " + counterparties.length + " counterparty records")
//load sandbox users from json
path = "/Users/stefan/Downloads/OBP_sandbox_pretty_load_002.json"
path = "/Users/simonredfern/Documents/OpenBankProject/DATA/API_sandbox/ulster_bank_2016/loaded_30_jan_UB_OBP_sandbox_pretty.json"
records = JsonParser.parse(Source.fromFile(path) mkString)
val users = (records \ "users").children
println("got " + users.length + " users")