diff --git a/.gitignore b/.gitignore
index 82eca336e352c9026decda294ff678968050edfc..2a0148e22111eff3ddd5f76ad5d7ba04082cd6be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
/target/
!.mvn/wrapper/maven-wrapper.jar
+*.log
### STS ###
.apt_generated
diff --git a/Dockerfile b/Dockerfile
index 4afd4e6e8e144946f5e2a5f68bd9f3c9a5d93df8..d58e26b942dd6255d2f4a45a9f9fa244d5b5aefe 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,24 +1,15 @@
FROM openjdk:10.0.2-13-slim
-# Define working directory and create it
+# Define working directory.
ENV WORKING_DIR /opt/nynja
-RUN mkdir -p "$WORKING_DIR"
+# Install curl for use with Kubernetes readiness probe.
+RUN mkdir -p "$WORKING_DIR" \
+ && apt-get update \
+ && apt-get install -y curl \
+ && rm -rf /var/lib/apt/lists/*
WORKDIR $WORKING_DIR
-# Default configuration properties - HTTP server port, gRPC server port, Cassandra - host, port and database
-ENV HTTP_SERVER_PORT=8080
-ENV GRPC_SERVER_PORT=6565
-ENV CASSANDRA_CONTACT_POINTS=localhost
-ENV CASSANDRA_PORT=9042
-ENV CASSANDRA_KEYSPACE=account
-
-# Note: In order to be able to connect and persist accounts in Cassandra DB schema and tables need to be created first. Please refer to the src/main/resources/db/account-service.cql
-
-# Expose Tomcat and gRPC server ports
-EXPOSE $HTTP_SERVER_PORT
-EXPOSE $GRPC_SERVER_PORT
-
# Copy the .jar file into the Docker image
COPY ./target/*.jar $WORKING_DIR/account-service.jar
-CMD ["java", "-jar", "$WORKING_DIR/account-service.jar", "--spring.profiles.active=production", "-XX:+UseContainerSupport", "-Djava.awt.headless=true", "-server", "-Xms128m", "-Xmx512m"]
+CMD ["java", "-jar", "account-service.jar", "--spring.profiles.active=production"]
diff --git a/Jenkinsfile b/Jenkinsfile
index b1a9335e55765f48e65265c00a003501dc068f20..7d491ce4542e23b29aa346d3116cce75aa25c313 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,83 +1,165 @@
-#!/usr/bin/env groovy
-
-@Library('nynja-common') _
-
-pipeline {
- environment {
- SLACK_CHANNEL = "#nynja-devops-feed"
- // Application namespace (k8s namespace, docker repository, ...)
- NAMESPACE = "blueprint"
- // Application name
- APP_NAME = "account-service"
- IMAGE_NAME = "eu.gcr.io/nynja-ci-201610/${NAMESPACE}/${APP_NAME}"
- IMAGE_TAG = "${BRANCH_NAME == 'master' ? 'latest' : 'latest-' + BRANCH_NAME}"
- IMAGE_BUILD_TAG = "$BRANCH_NAME-$BUILD_NUMBER"
- // The branch to be deployed in the dev cluster, following gitflow, it should normally be "develop" or "dev"
- DEV_BRANCH = "master"
- }
- agent {
- kubernetes(builders.simple("jdk", "openjdk:11-jdk"))
- }
- options {
- skipDefaultCheckout()
- buildDiscarder(logRotator(numToKeepStr: '15'))
- }
- stages {
- stage('Checkout') {
- steps {
- container('jdk') {
- script {
- def vars = checkout scm
- vars.each { k,v -> env.setProperty(k, v) }
- }
-
- slackSend channel: SLACK_CHANNEL, message: slackStartMsg()
- slackSend channel: SLACK_CHANNEL, message: "", attachments: slackBuildInfo()
- }
- }
- }
- stage('Build') {
- steps {
- container('jdk') {
- // Maven must be available in the path.
- // Tests must be enabled as soon as they pass locally.
- mvn clean install
- }
- }
- }
- stage('Build & Publish Docker Image') {
- when {
- branch env.DEV_BRANCH
- }
- steps {
- // The container used to build & publish the docker image must have the docker binary
- // either installed, either mounted, like in this blueprint
- container('jdk') {
- dockerBuildAndPushToRegistry "${NAMESPACE}/${APP_NAME}", [IMAGE_TAG,IMAGE_BUILD_TAG]
- }
- }
- }
- stage('Update Kubernetes deployment') {
- when {
- branch env.DEV_BRANCH
- }
- steps {
- // To deploy, the configuration from the ./k8s folder will be commit to a deployment repository
- // environment variables are being substituted in the process.
- // The namespace correspond to the target k8s namespace.
- // Git must be available in the container used, as configuration will be pushed.
- container('jdk') {
- deployToDevelopment(NAMESPACE)
- }
- }
- }
- }
- post {
- success {
- slackSend channel: SLACK_CHANNEL, message: slackEndMsg(), color: 'good'
- }
- failure {
- slackSend channel: SLACK_CHANNEL, message: slackEndMsg(), color: 'danger'
- }
- }
-}
\ No newline at end of file
+#!/usr/bin/env groovy
+
+@Library('nynja-common') _
+
+pipeline {
+ environment {
+ SLACK_CHANNEL = "#nynja-devops-feed"
+ NAMESPACE = "account"
+ APP_NAME = "account-service"
+ IMAGE_NAME = "eu.gcr.io/nynja-ci-201610/${NAMESPACE}/${APP_NAME}"
+ IMAGE_BUILD_TAG = "$BRANCH_NAME-$BUILD_NUMBER"
+ HELM_CHART_NAME = "account-service"
+ DEV_BRANCH = "dev"
+ }
+ agent {
+ kubernetes(builders.multi([
+ "mvn":"maven:3-jdk-10",
+ "helm":"lachlanevenson/k8s-helm:v2.9.1"
+ ]))
+ }
+ options {
+ skipDefaultCheckout()
+ buildDiscarder(logRotator(numToKeepStr: '15'))
+ }
+ stages {
+ stage('Checkout') {
+ steps {
+ container('mvn') {
+ script {
+ def vars = checkout scm
+ vars.each { k,v -> env.setProperty(k, v) }
+ }
+ slackSend channel: SLACK_CHANNEL, message: slackStartMsg()
+ slackSend channel: SLACK_CHANNEL, message: "", attachments: slackBuildInfo()
+ }
+ }
+ }
+ /*
+ stage('Build PR') {
+ when {
+ branch 'PR-*'
+ }
+ stages {
+ stage('Build') {
+ steps {
+ echo 'build & test'
+ dockerBuildAndPushToRegistry "${NAMESPACE}/${APP_NAME}", [IMAGE_BUILD_TAG]
+ }
+ }
+ stage('Deploy preview') {
+ steps {
+ echo 'build & test'
+ }
+ }
+ }
+ }
+ */
+ stage('Build Dev') {
+ when {
+ branch env.DEV_BRANCH
+ }
+ stages {
+ stage('Build') {
+ steps {
+ container('mvn') {
+ withCredentials([file(credentialsId: 'mavenSettings.xml', variable: 'FILE')]) {
+ sh 'mvn --settings $FILE clean install -Dmaven.test.skip=true'
+ }
+ dockerBuildAndPushToRegistry "${NAMESPACE}/${APP_NAME}", [IMAGE_BUILD_TAG]
+ }
+ }
+ }
+ stage("Helm chart") {
+ steps {
+ container('helm') {
+ helmBuildAndPushToRegistry HELM_CHART_NAME
+ }
+ }
+ }
+ stage('Deploy preview') {
+ steps {
+ deployHelmTo "dev", NAMESPACE
+ }
+ }
+ }
+ post {
+ success {
+ container('mvn') { slackSend channel: SLACK_CHANNEL, message: slackEndMsg(), color: 'good' }
+ }
+ failure {
+ container('mvn') { slackSend channel: SLACK_CHANNEL, message: slackEndMsg(), color: 'danger' }
+ }
+ }
+ }
+ stage('Build Release') {
+ when {
+ branch 'master'
+ }
+ stages {
+ stage("Build") {
+ steps {
+ container('mvn') {
+ withCredentials([file(credentialsId: 'mavenSettings.xml', variable: 'FILE')]) {
+ sh 'mvn --settings $FILE clean install # -Dmaven.test.skip=true'
+ }
+ dockerBuildAndPushToRegistry "${NAMESPACE}/${APP_NAME}", [IMAGE_BUILD_TAG]
+ }
+ }
+ }
+ stage("Helm chart") {
+ steps {
+ container('helm') {
+ helmBuildAndPushToRegistry HELM_CHART_NAME
+ }
+ }
+ }
+ stage("Approval: Deploy to staging ?") {
+ steps {
+ slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: build #$BUILD_NUMBER ready to deploy to `STAGING`, approval required: $BUILD_URL (24h)"
+
+ timeout(time: 24, unit: 'HOURS') { input 'Deploy to staging ?' }
+ }
+ post { failure { echo 'Deploy aborted for build #...' }}
+ }
+ stage("Deploy to staging") {
+ steps {
+ slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: deploying build #$BUILD_NUMBER to `STAGING`"
+ deployHelmTo "staging", NAMESPACE
+ }
+ }
+ stage("Approval: Deploy to production ?") {
+ steps {
+ slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: build #$BUILD_NUMBER ready to deploy to `PRODUCTION`, approval required: $BUILD_URL (24h)"
+
+ timeout(time: 7, unit: 'DAYS') { input 'Deploy to production ?' }
+ }
+ post { failure { echo 'Deploy aborted for build #...' }}
+ }
+ stage('Tagging release') {
+ steps {
+ container("mvn") {
+ // Updating the "latest tag"
+ dockerTagLatestAndPushToRegistry "${NAMESPACE}/${APP_NAME}", IMAGE_BUILD_TAG
+ }
+ }
+ }
+ /*
+ stage('Deploy release to canary') {
+ steps {
+ slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: deploying build #$BUILD_NUMBER to `PRODUCTION` (canary)"
+ echo "deploy to canary"
+ }
+ }
+ */
+ stage("Deploy to production") {
+ steps {
+ slackSend channel: SLACK_CHANNEL, message: "$APP_NAME: deploying build #$BUILD_NUMBER to `PRODUCTION`"
+
+ deployHelmTo "prod", NAMESPACE
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/README.md b/README.md
index df97891d9104de4ca91728f207a9696b34933293..a75e0ec4d2065ea36e3e35ad1612abfc0a64e12a 100644
--- a/README.md
+++ b/README.md
@@ -34,26 +34,47 @@ The generated artifacts are then referenced from the .proto repo as Maven depend
Unit tests
```
-# How to build
-1. Make sure there's Java 10 installed.
-2. Get proper Maven settings.xml file and place it under ~/.m2 directory. This is needed because of the proto artifactory repo.
-3. Run `mvn clean install`
-4. (Optional, developers only) If you're using Eclipse, make sure you are on Eclipse Photon version at least.
+# SetUp info for developers
-# How to run
-1. Make sure there's Cassandra reachable somewhere within your network.
-For developers: the 'dev' profile (src/main/resources/application-dev.yml) assumes Cassandra will be reachable on localhost:9042.
-For production: few environment variables should be set (see src/main/resources/application-production.yml).
+ For starting current service you must do next:
+ 1. Install JDK 10 for project and JDK 8 for Cassandra DB
+ 2. Install Cassandra
+ a. If you use a Windows machine, you must save in JAVA_HOME variable path to jdk8.
+ Install python 2.7.
+ Download 7z or other archive manager and extract Cassandra's files to a necessary directory
+ Delete from cassandra.bat file -XX:+UseParNewGC^ .
+ Start cassandra.bat
+ b. With Linux, we have the same behavior
+ 3. Install maven and add to .m2 folder settings.xml (ping your teamLead with this issue)
+ 4. Set to VM options dev profile " -Dspring.profiles.active=dev"
+ 5. Start application
+ If you're using Eclipse, make sure you are on Eclipse Photon version at least.
-2. Use your IDE to start the Spring Boot Application (remember to set the 'Dev' profile) or run the Java application as:
-`java -jar account-service.jar --spring.profiles.active=dev`
+# SetUp for DevOps
-# Build the Docker image
+## How to build
+ 1. Make sure there's Java 10 installed.
+ 2. Get proper Maven settings.xml file and place it under ~/.m2 directory. This is needed because of the proto artifactory repo.
+ 3. Run `mvn clean install`
+
+## How to run
+ Make sure there's Cassandra reachable somewhere within your network.
+ For production: few environment variables should be set (see src/main/resources/application-production.yml).
+
+
+## Build the Docker image
```
docker build -t account-service .
```
-# Run the containerized application
+## Run the containerized application
```
docker run --rm -ti -p 8080:8080 -p 6565:6565 account-service
```
+
+# Known issues
+
+ (NY_3728) When starting the microservice account-service we get several warning messages due to a release mismatch with the groovy logging jar.
+ One possible but not suitable solution is use a VM argument "--illegal-access=deny".
+ Another proper solution is to upgrade the jar which should be done at a later date.
+
diff --git a/charts/account-service/.helmignore b/charts/account-service/.helmignore
new file mode 100644
index 0000000000000000000000000000000000000000..f0c13194444163d1cba5c67d9e79231a62bc8f44
--- /dev/null
+++ b/charts/account-service/.helmignore
@@ -0,0 +1,21 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
diff --git a/charts/account-service/Chart.yaml b/charts/account-service/Chart.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..15646489e1bf3df41f8d059527e6eae14ea613dd
--- /dev/null
+++ b/charts/account-service/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+appVersion: "1.0"
+description: Deployment of the nynja account service.
+name: account-service
+version: 0.1.0
diff --git a/charts/account-service/templates/00-label.yaml b/charts/account-service/templates/00-label.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..dd5d17afc13ed355afb9d48f5d41bb161f685ccc
--- /dev/null
+++ b/charts/account-service/templates/00-label.yaml
@@ -0,0 +1,32 @@
+# This hook depends on helm creating the target namespace if it doesn't exist
+# before the hook is called. This is the case on Helm v2.9.1
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: enable-istio-injection-{{ .Release.Namespace }}
+ namespace: kube-system
+ labels:
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+ app.kubernetes.io/managed-by: {{.Release.Service | quote }}
+ app.kubernetes.io/instance: {{.Release.Name | quote }}
+ helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
+ annotations:
+ helm.sh/hook: pre-install
+ helm.sh/hook-delete-policy: hook-before-creation,hook-succeeded
+spec:
+ template:
+ spec:
+ containers:
+ - name: labeler
+ image: gcr.io/google_containers/hyperkube:v1.9.7
+ command:
+ - kubectl
+ - label
+ - --overwrite
+ - ns
+ - {{ .Release.Namespace }}
+ - istio-injection=enabled
+ restartPolicy: Never
+ # use tiller service account since it should have permissions to do namespace labeling
+ serviceAccountName: tiller
diff --git a/charts/account-service/templates/_helpers.tpl b/charts/account-service/templates/_helpers.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..1befa9784b266cb2340e11a5c76065abfb26c88d
--- /dev/null
+++ b/charts/account-service/templates/_helpers.tpl
@@ -0,0 +1,32 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "account-service.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "account-service.fullname" -}}
+{{- if .Values.fullnameOverride -}}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- if contains $name .Release.Name -}}
+{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "account-service.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
diff --git a/charts/account-service/templates/deployment.yaml b/charts/account-service/templates/deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e2293f5fe361b03c0269bb8b551565de4320f4a1
--- /dev/null
+++ b/charts/account-service/templates/deployment.yaml
@@ -0,0 +1,72 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: {{ template "account-service.fullname" . }}
+ labels:
+ app: {{ template "account-service.name" . }}
+ chart: {{ template "account-service.chart" . }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ selector:
+ matchLabels:
+ app: {{ template "account-service.name" . }}
+ release: {{ .Release.Name }}
+ template:
+ metadata:
+ annotations:
+ sidecar.istio.io/inject: "true"
+ labels:
+ app: {{ template "account-service.name" . }}
+ release: {{ .Release.Name }}
+ spec:
+ containers:
+ - name: {{ template "account-service.name" . }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - containerPort: {{ .Values.ports.containerPort.http }}
+ name: http
+ - containerPort: {{ .Values.ports.containerPort.grpc }}
+ name: grpc
+ readinessProbe:
+ exec:
+ command:
+ - /bin/sh
+ - -c
+ - curl --silent http://localhost:{{ .Values.ports.containerPort.http }}/actuator/health 2>&1 | grep UP || exit 1
+ successThreshold: 1
+ failureThreshold: 10
+ initialDelaySeconds: 60
+ periodSeconds: 5
+ timeoutSeconds: 5
+ livenessProbe:
+ httpGet:
+ path: /actuator/health
+ port: {{ .Values.ports.containerPort.http }}
+ successThreshold: 1
+ failureThreshold: 10
+ initialDelaySeconds: 60
+ periodSeconds: 5
+ timeoutSeconds: 5
+ env:
+ ## Container ports.
+ - name: HTTP_SERVER_PORT
+ value: {{ .Values.ports.containerPort.http | quote }}
+ - name: GRPC_SERVER_PORT
+ value: {{ .Values.ports.containerPort.grpc | quote }}
+ resources:
+{{ toYaml .Values.resources | indent 12 }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+{{ toYaml . | indent 8 }}
+ {{- end }}
+ {{- with .Values.affinity }}
+ affinity:
+{{ toYaml . | indent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+{{ toYaml . | indent 8 }}
+ {{- end }}
diff --git a/charts/account-service/templates/envoy-grpc-web.yaml b/charts/account-service/templates/envoy-grpc-web.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7462b223dd319c249e8d2e515e99095de723b225
--- /dev/null
+++ b/charts/account-service/templates/envoy-grpc-web.yaml
@@ -0,0 +1,21 @@
+apiVersion: networking.istio.io/v1alpha3
+kind: EnvoyFilter
+metadata:
+ name: {{ template "account-service.fullname" . }}-grpc-web
+ labels:
+ app: {{ template "account-service.name" . }}
+ chart: {{ template "account-service.chart" . }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+spec:
+ workloadLabels:
+ app: {{ template "account-service.name" . }}
+ filters:
+ - listenerMatch:
+ portNumber: {{ .Values.ports.containerPort.grpc }}
+ listenerType: SIDECAR_INBOUND
+ filterName: envoy.grpc_web
+ filterType: HTTP
+ filterConfig: {}
+ insertPosition:
+ index: FIRST
diff --git a/charts/account-service/templates/service.yaml b/charts/account-service/templates/service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5eef5b8199e5f312a6199cbc88d13fa62f93a4f6
--- /dev/null
+++ b/charts/account-service/templates/service.yaml
@@ -0,0 +1,22 @@
+kind: Service
+apiVersion: v1
+metadata:
+ name: {{ template "account-service.fullname" . }}
+ labels:
+ app: {{ template "account-service.name" . }}
+ chart: {{ template "account-service.chart" . }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+spec:
+ selector:
+ app: {{ template "account-service.name" . }}
+ release: {{ .Release.Name }}
+ ports:
+ - protocol: TCP
+ port: {{ .Values.ports.containerPort.http }}
+ targetPort: {{ .Values.ports.containerPort.http }}
+ name: http-account
+ - protocol: TCP
+ port: {{ .Values.ports.containerPort.grpc }}
+ targetPort: {{ .Values.ports.containerPort.grpc }}
+ name: grpc-account
diff --git a/charts/account-service/templates/virtualservice.yaml b/charts/account-service/templates/virtualservice.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d3301e848515bf6ea4982394a829599327d6f38f
--- /dev/null
+++ b/charts/account-service/templates/virtualservice.yaml
@@ -0,0 +1,42 @@
+apiVersion: networking.istio.io/v1alpha3
+kind: VirtualService
+metadata:
+ name: {{ template "account-service.fullname" . }}
+ labels:
+ app: {{ template "account-service.name" . }}
+ chart: {{ template "account-service.chart" . }}
+ release: {{ .Release.Name }}
+ heritage: {{ .Release.Service }}
+spec:
+ gateways:
+ {{- range .Values.gateway.selector }}
+ - {{ . }}
+ {{- end }}
+ hosts:
+ {{- range .Values.gateway.hosts }}
+ - {{ . }}
+ {{- end }}
+ http:
+ - match:
+ - uri:
+ prefix: /
+ route:
+ - destination:
+ host: {{ template "account-service.fullname" . }}
+ port:
+ number: {{ .Values.ports.containerPort.grpc }}
+ corsPolicy:
+ allowOrigin:
+ {{- range .Values.corsPolicy.allowOrigin }}
+ - {{ . }}
+ {{- end }}
+ allowMethods:
+ {{- range .Values.corsPolicy.allowMethods}}
+ - {{ . }}
+ {{- end }}
+ allowCredentials: {{ .Values.corsPolicy.allowCredentials }}
+ allowHeaders:
+ {{- range .Values.corsPolicy.allowHeaders }}
+ - {{ . }}
+ {{- end }}
+ maxAge: {{ .Values.corsPolicy.maxAge }}
diff --git a/charts/account-service/values.yaml b/charts/account-service/values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9428e11b241c85bd9f2417a6869ac99eba96dfe2
--- /dev/null
+++ b/charts/account-service/values.yaml
@@ -0,0 +1,38 @@
+replicaCount: 1
+
+image:
+ repository: eu.gcr.io/nynja-ci-201610/nynja-account/nynja-account-service
+ tag: stable
+ pullPolicy: IfNotPresent
+
+gateway:
+ selector:
+ - api-gateway.default.svc.cluster.local
+ hosts:
+
+resources:
+ limits:
+ cpu: 1
+ memory: 1Gi
+ requests:
+ cpu: 500m
+ memory: 512Mi
+
+ports:
+ containerPort:
+ http:
+ grpc:
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
+
+corsPolicy:
+ allowOrigin:
+ allowMethods:
+ allowCredentials:
+ allowHeaders:
+ maxAge:
+
diff --git a/pom.xml b/pom.xml
index 2f5577d9f6a245c126f622e5b6c86c22db4954b6..ad665e6ff21fe253fac125dbf21dce25e09a13cc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
UTF-8
UTF-8
- 1.10
+ 10
2.3.2
@@ -101,7 +101,7 @@
libs-snapshot-local.biz.nynja.protos
- blueprint-java-intracoldev
+ account-service-intracoldev
1.0-SNAPSHOT
@@ -111,18 +111,50 @@
+
+ com.googlecode.libphonenumber
+ libphonenumber
+ 8.9.12
+
+
com.fasterxml.jackson.core
jackson-databind
- 2.9.5
+ 2.9.5
com.fasterxml.jackson.core
jackson-core
- 2.9.5
+ 2.9.5
+
+
+
+ org.codehaus.groovy
+ groovy-all
+
+
+
+ io.micrometer
+ micrometer-core
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.8.1
+
+
+
+ com.sun.mail
+ javax.mail
+ 1.6.2
+
diff --git a/releases/dev/account-service.yaml b/releases/dev/account-service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8b4d6d31afc40ca620bcc7e2964f5eaaf9bcdc70
--- /dev/null
+++ b/releases/dev/account-service.yaml
@@ -0,0 +1,55 @@
+kind: HelmRelease
+metadata:
+ name: account-service
+ namespace: account
+spec:
+ chart:
+ name: account-service
+ values:
+ replicaCount: 1
+
+ image:
+ repository: ${IMAGE_NAME}
+ tag: ${IMAGE_BUILD_TAG}
+
+ gateway:
+ selector:
+ - api-gateway.default.svc.cluster.local
+ hosts:
+ - account.dev-eu.nynja.net
+
+ resources:
+ limits:
+ cpu: 1
+ memory: 1500Mi
+ requests:
+ cpu: 500m
+ memory: 1000Mi
+
+ ports:
+ containerPort:
+ http: 8080
+ grpc: 6565
+
+ # CORS policy
+ corsPolicy:
+ allowOrigin:
+ - http://localhost:3000
+ - https://localhost
+ - https://localhost/grpc/
+ - http://10.191.224.180:3000
+ - https://localhost:8080
+ - https://127.0.0.1:8080
+ - https://web.dev-eu.nynja.net
+ - https://web.staging.nynja.net
+ - https://web.nynja.net
+ allowMethods:
+ - POST
+ - GET
+ - OPTIONS
+ allowCredentials: false
+ allowHeaders:
+ - content-type
+ - x-grpc-web
+ maxAge: "600s"
+
diff --git a/releases/prod/account-service.yaml b/releases/prod/account-service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..cf66d1519f268531fc744b59825d38c5ef6f772a
--- /dev/null
+++ b/releases/prod/account-service.yaml
@@ -0,0 +1,17 @@
+kind: HelmRelease
+metadata:
+ name: account-service
+ namespace: account
+spec:
+ chart:
+ name: account-service
+ values:
+ replicaCount: 3
+ image:
+ repository: ${IMAGE_NAME}
+ tag: ${IMAGE_BUILD_TAG}
+ gateway:
+ selector:
+ - api-gateway.default.svc.cluster.local
+ hosts:
+ - account.nynja.net
diff --git a/releases/staging/account-service.yaml b/releases/staging/account-service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8a149a61960b085dd314a4d84ca307cc3c66d467
--- /dev/null
+++ b/releases/staging/account-service.yaml
@@ -0,0 +1,17 @@
+kind: HelmRelease
+metadata:
+ name: account-service
+ namespace: account
+spec:
+ chart:
+ name: account-service
+ values:
+ replicaCount: 2
+ image:
+ repository: ${IMAGE_NAME}
+ tag: ${IMAGE_BUILD_TAG}
+ gateway:
+ selector:
+ - api-gateway.default.svc.cluster.local
+ hosts:
+ - account.staging.nynja.net
diff --git a/setup/intracol-code-formatter.xml b/setup/intracol-code-formatter.xml
new file mode 100644
index 0000000000000000000000000000000000000000..06b04e2b9e824eaf5d96499005dc6a92b045cdd6
--- /dev/null
+++ b/setup/intracol-code-formatter.xml
@@ -0,0 +1,587 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/biz/nynja/account/grpc/Application.java b/src/main/java/biz/nynja/account/Application.java
similarity index 92%
rename from src/main/java/biz/nynja/account/grpc/Application.java
rename to src/main/java/biz/nynja/account/Application.java
index 79f40bb26b6ad5d357de400ddd16e26caef58460..f19c3d10434c5e8f31d924d56ebdc731fec98c02 100644
--- a/src/main/java/biz/nynja/account/grpc/Application.java
+++ b/src/main/java/biz/nynja/account/Application.java
@@ -1,7 +1,7 @@
/**
* Copyright (C) 2018 Nynja Inc. All rights reserved.
*/
-package biz.nynja.account.grpc;
+package biz.nynja.account;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
diff --git a/src/main/java/biz/nynja/account/StartupScriptsListener.java b/src/main/java/biz/nynja/account/StartupScriptsListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac797ab029f730ba455206115828361f22ceda77
--- /dev/null
+++ b/src/main/java/biz/nynja/account/StartupScriptsListener.java
@@ -0,0 +1,67 @@
+package biz.nynja.account;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import com.datastax.driver.core.Session;
+
+import biz.nynja.account.configuration.CassandraConfig;
+
+/**
+ * This acts as {@link CassandraConfig} startupScripts executor
+ * but activated after the spring has setup the needed tables though JPA
+ * @author dragomir.todorov
+ *
+ */
+@Component
+public class StartupScriptsListener {
+
+ private String keyspace;
+
+ @Autowired
+ private Session session;
+
+ @EventListener(ContextRefreshedEvent.class)
+ public void contextRefreshedEvent() {
+ keyspace = session.getLoggedKeyspace();
+
+ for (String script : getStartupScripts()) {
+ session.execute(script);
+ }
+ }
+
+ private List getStartupScripts() {
+ String scriptAccountViewByProfileId = "CREATE MATERIALIZED VIEW IF NOT EXISTS " + keyspace
+ + ".accountbyprofileid AS SELECT * FROM account " + "WHERE profileid IS NOT NULL "
+ + "PRIMARY KEY (profileid, accountid);";
+
+ String scriptAccountViewByAuthProvider = "CREATE MATERIALIZED VIEW IF NOT EXISTS " + keyspace
+ + ".accountbyauthenticationprovider AS SELECT * FROM account "
+ + "WHERE authenticationprovider IS NOT NULL " + "PRIMARY KEY (authenticationprovider, accountid);";
+
+ String scriptAccountViewByAccountName = "CREATE MATERIALIZED VIEW IF NOT EXISTS " + keyspace
+ + ".accountbyaccountname AS SELECT * FROM account " + "WHERE accountname IS NOT NULL "
+ + "PRIMARY KEY (accountname, accountid);";
+
+ String scriptAccountViewByUsername = "CREATE MATERIALIZED VIEW IF NOT EXISTS " + keyspace
+ + ".accountbyusername AS SELECT * FROM account " + "WHERE username IS NOT NULL "
+ + "PRIMARY KEY (username, accountid);";
+
+ String scriptPendingAccountViewByAuthenticationProvider = "CREATE MATERIALIZED VIEW IF NOT EXISTS " + keyspace
+ + ".pendingaccountbyauthenticationprovider AS SELECT * FROM pendingaccount " + "WHERE authenticationprovider IS NOT NULL "
+ + "PRIMARY KEY (authenticationprovider, accountid);";
+
+ String scriptAccountViewByQrCode = "CREATE MATERIALIZED VIEW IF NOT EXISTS " + keyspace
+ + ".accountbyqrcode AS SELECT * FROM account " + "WHERE qrcode IS NOT NULL "
+ + "PRIMARY KEY (qrcode, accountid);";
+
+ return Arrays.asList(scriptAccountViewByProfileId, scriptAccountViewByAuthProvider,
+ scriptAccountViewByAccountName, scriptAccountViewByUsername,
+ scriptPendingAccountViewByAuthenticationProvider, scriptAccountViewByQrCode);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/biz/nynja/account/codecs/AuthenticationProviderCodec.java b/src/main/java/biz/nynja/account/codecs/AuthenticationProviderCodec.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1260f786a8d5285ab5adffa3df7e95c6ec8f171
--- /dev/null
+++ b/src/main/java/biz/nynja/account/codecs/AuthenticationProviderCodec.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (C) 2018 Nynja Inc. All rights reserved.
+ */
+package biz.nynja.account.codecs;
+
+import java.nio.ByteBuffer;
+
+import com.datastax.driver.core.ProtocolVersion;
+import com.datastax.driver.core.TypeCodec;
+import com.datastax.driver.core.UDTValue;
+import com.datastax.driver.core.UserType;
+import com.datastax.driver.core.exceptions.InvalidTypeException;
+
+import biz.nynja.account.models.AuthenticationProvider;
+
+
+public class AuthenticationProviderCodec extends TypeCodec {
+
+ private final TypeCodec innerCodec;
+
+ private final UserType userType;
+
+ public AuthenticationProviderCodec(TypeCodec innerCodec, Class javaType) {
+ super(innerCodec.getCqlType(), javaType);
+ this.innerCodec = innerCodec;
+ this.userType = (UserType) innerCodec.getCqlType();
+ }
+
+ @Override
+ public ByteBuffer serialize(AuthenticationProvider value, ProtocolVersion protocolVersion)
+ throws InvalidTypeException {
+ return innerCodec.serialize(toUDTValue(value), protocolVersion);
+ }
+
+ @Override
+ public AuthenticationProvider deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion)
+ throws InvalidTypeException {
+ return toAuthenticationProvider(innerCodec.deserialize(bytes, protocolVersion));
+ }
+
+ @Override
+ public AuthenticationProvider parse(String value) throws InvalidTypeException {
+ return value == null || value.isEmpty() || value.equals("NULL") ? null
+ : toAuthenticationProvider(innerCodec.parse(value));
+ }
+
+ @Override
+ public String format(AuthenticationProvider value) throws InvalidTypeException {
+ return value == null ? null : innerCodec.format(toUDTValue(value));
+ }
+
+ protected AuthenticationProvider toAuthenticationProvider(UDTValue value) {
+ if (value == null) {
+ return null;
+ } else {
+ AuthenticationProvider authProvider = new AuthenticationProvider();
+ authProvider.setType(value.getString("type"));
+ authProvider.setValue(value.getString("value"));
+ return authProvider;
+ }
+ }
+
+ protected UDTValue toUDTValue(AuthenticationProvider value) {
+ return value == null ? null
+ : userType.newValue().setString("type", value.getType()).setString("value", value.getValue());
+ }
+}
diff --git a/src/main/java/biz/nynja/account/components/AccountServiceHelper.java b/src/main/java/biz/nynja/account/components/AccountServiceHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..c47fa789bc087d4b2cb4b4839c5007de28c3ad57
--- /dev/null
+++ b/src/main/java/biz/nynja/account/components/AccountServiceHelper.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (C) 2018 Nynja Inc. All rights reserved.
+ */
+package biz.nynja.account.components;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import biz.nynja.account.models.Account;
+import biz.nynja.account.models.AccountByAuthenticationProvider;
+import biz.nynja.account.repositories.AccountByAuthenticationProviderRepository;
+
+@Service
+public class AccountServiceHelper {
+ @Autowired
+ private AccountByAuthenticationProviderRepository accountByAuthenticationProviderRepository;
+
+ public Account getAccountByAuthenticationProviderHelper(String authenticationIdentifier,
+ String type) {
+
+ List accounts = accountByAuthenticationProviderRepository
+ .findAllByAuthenticationProvider(authenticationIdentifier);
+
+ if (accounts.isEmpty()) {
+ return null;
+ }
+
+ // We retrieve accounts by authentication provider identifier from DB where both authentication provider
+ // identifier and type uniquely identify an account.
+ // For this reason we need to filter results by authentication provider type.
+ for (AccountByAuthenticationProvider account : accounts) {
+ if (account.getAuthenticationProviderType().equals(type)) {
+ return account.toAccount();
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/main/java/biz/nynja/account/components/PendingAccountValidator.java b/src/main/java/biz/nynja/account/components/PendingAccountValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..09c4aae6be2b18492f4da27dd1f97cc96a426c7a
--- /dev/null
+++ b/src/main/java/biz/nynja/account/components/PendingAccountValidator.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (C) 2018 Nynja Inc. All rights reserved.
+ */
+package biz.nynja.account.components;
+
+import org.springframework.stereotype.Service;
+
+import biz.nynja.account.grpc.CompletePendingAccountCreationRequest;
+import biz.nynja.account.grpc.CreatePendingAccountRequest;
+import biz.nynja.account.grpc.ErrorResponse.Cause;
+
+@Service
+public class PendingAccountValidator {
+
+ private Validator validator;
+
+ public PendingAccountValidator(Validator validator) {
+ this.validator = validator;
+ }
+
+ public Cause validateCreatePendingAccountRequest(CreatePendingAccountRequest request) {
+ return validator.validateAuthProvider(request.getAuthenticationType(), request.getAuthenticationProvider());
+ }
+
+ public Cause validateCompletePendingAccountCreationRequest(CompletePendingAccountCreationRequest request) {
+ if (request.getAccountId() == null || request.getAccountId().trim().isEmpty()) {
+ return Cause.MISSING_ACCOUNT_ID;
+ }
+
+ if (request.getFirstName() != null && request.getFirstName().trim().isEmpty()) {
+ return Cause.MISSING_FIRST_NAME;
+ } else if (!validator.isFirstNameValid(request.getFirstName())) {
+ return Cause.INVALID_FIRST_NAME;
+ }
+
+ if (request.getLastName() != null && !request.getLastName().trim().isEmpty()
+ && !validator.isLastNameValid(request.getLastName())) {
+ return Cause.INVALID_LAST_NAME;
+ }
+
+ if (request.getUsername() != null && !request.getUsername().trim().isEmpty()
+ && !validator.isUsernameValid(request.getUsername())) {
+ return Cause.USERNAME_INVALID;
+ }
+
+ if (request.getAccountName() != null && !request.getAccountName().trim().isEmpty()
+ && !validator.isAccountNameValid(request.getAccountName())) {
+ return Cause.ACCOUNT_NAME_INVALID;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java b/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..91b9b32575dc5804591380f851ee8366510e02e4
--- /dev/null
+++ b/src/main/java/biz/nynja/account/components/PreparedStatementsCache.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (C) 2018 Nynja Inc. All rights reserved.
+ */
+package biz.nynja.account.components;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.PostConstruct;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.TypeCodec;
+import com.datastax.driver.core.UDTValue;
+import com.datastax.driver.core.UserType;
+
+import biz.nynja.account.codecs.AuthenticationProviderCodec;
+import biz.nynja.account.configuration.CassandraConfig;
+import biz.nynja.account.models.AuthenticationProvider;
+
+@Configuration
+public class PreparedStatementsCache {
+
+ private final CassandraConfig cassandraConfig;
+
+ private final Session session;
+
+ private static Map statementsCache;
+
+ @Autowired
+ public PreparedStatementsCache(Session session, CassandraConfig cassandraConfig) {
+ this.session = session;
+ this.cassandraConfig = cassandraConfig;
+ }
+
+ @PostConstruct
+ public void init() {
+ statementsCache = new ConcurrentHashMap<>();
+ registerAuthenticationProviderCodec();
+ }
+
+ private Map getPreparedStatementsCache() {
+ return statementsCache;
+ }
+
+ public PreparedStatement getPreparedStatement(String cql) {
+ if (getPreparedStatementsCache().containsKey(cql)) {
+ return getPreparedStatementsCache().get(cql);
+ } else {
+ PreparedStatement statement = session.prepare(cql);
+ getPreparedStatementsCache().put(cql, statement);
+ return statement;
+ }
+ }
+
+ private void registerAuthenticationProviderCodec() {
+ UserType authenticationProviderType = session.getCluster().getMetadata()
+ .getKeyspace(cassandraConfig.getConfiguredKeyspaceName()).getUserType("authenticationprovider");
+ TypeCodec authenticationProviderTypeCodec = session.getCluster().getConfiguration().getCodecRegistry()
+ .codecFor(authenticationProviderType);
+
+ AuthenticationProviderCodec addressCodec = new AuthenticationProviderCodec(authenticationProviderTypeCodec,
+ AuthenticationProvider.class);
+ session.getCluster().getConfiguration().getCodecRegistry().register(addressCodec);
+ }
+}
diff --git a/src/main/java/biz/nynja/account/components/StatementsPool.java b/src/main/java/biz/nynja/account/components/StatementsPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba8c9d66456df3ab3381b998c21acb7c3a1a562d
--- /dev/null
+++ b/src/main/java/biz/nynja/account/components/StatementsPool.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (C) 2018 Nynja Inc. All rights reserved.
+ */
+package biz.nynja.account.components;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.datastax.driver.core.BoundStatement;
+
+import biz.nynja.account.models.AuthenticationProvider;
+
+@Service
+public class StatementsPool {
+
+ private final PreparedStatementsCache preparedStatementsCache;
+
+ @Autowired
+ public StatementsPool(PreparedStatementsCache preparedStatementsCache) {
+ this.preparedStatementsCache = preparedStatementsCache;
+ }
+
+ public BoundStatement addAuthenticationProviderToProfile(UUID profileId, AuthenticationProvider authProvider) {
+ String cql = "UPDATE profile SET authenticationproviders = authenticationproviders + ? WHERE profileid = ? ;";
+ Set toBeAdded = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(authProvider)));
+ BoundStatement bound = preparedStatementsCache.getPreparedStatement(cql).bind(toBeAdded, profileId);
+ return bound;
+ }
+
+ public BoundStatement insertProfileByAuthenticationProvider(UUID profileId, String authPovider,
+ String authProviderType) {
+ String cql = "INSERT INTO profilebyauthenticationprovider (authenticationprovider, authenticationprovidertype, profileid) VALUES (?, ?, ?) ;";
+ BoundStatement bound = preparedStatementsCache.getPreparedStatement(cql).bind(authPovider, authProviderType,
+ profileId);
+ return bound;
+ }
+
+ public BoundStatement deleteAuthenicationProviderFromProfile(UUID profileId, AuthenticationProvider authProvider) {
+ String cql = "UPDATE profile SET authenticationproviders = authenticationproviders - ? WHERE profileid = ? ;";
+ Set toBeDeleted = Collections
+ .unmodifiableSet(new HashSet<>(Arrays.asList(authProvider)));
+ BoundStatement bound = preparedStatementsCache.getPreparedStatement(cql).bind(toBeDeleted, profileId);
+ return bound;
+ }
+
+ public BoundStatement deleteProfileByAuthenticationProvider(String authProvider, String authProviderType) {
+ String cql = "DELETE FROM profilebyauthenticationprovider where authenticationprovider = ? and authenticationprovidertype = ? ;";
+ BoundStatement bound = preparedStatementsCache.getPreparedStatement(cql).bind(authProvider, authProviderType);
+ return bound;
+ }
+
+ public BoundStatement deleteCreationProviderFromAccount(UUID accountId) {
+ String cql = "DELETE authenticationprovidertype, authenticationprovider FROM account WHERE accountid = ? ;";
+ BoundStatement bound = preparedStatementsCache.getPreparedStatement(cql).bind(accountId);
+ return bound;
+ }
+}
diff --git a/src/main/java/biz/nynja/account/components/Validator.java b/src/main/java/biz/nynja/account/components/Validator.java
new file mode 100644
index 0000000000000000000000000000000000000000..c48ff7dbb976c2806d2babd64097421c92f7f06c
--- /dev/null
+++ b/src/main/java/biz/nynja/account/components/Validator.java
@@ -0,0 +1,275 @@
+/**
+ * Copyright (C) 2018 Nynja Inc. All rights reserved.
+ */
+package biz.nynja.account.components;
+
+import java.time.DateTimeException;
+import java.time.LocalDate;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import biz.nynja.account.grpc.AddAuthenticationProviderRequest;
+import biz.nynja.account.grpc.AuthenticationType;
+import biz.nynja.account.grpc.ContactDetails;
+import biz.nynja.account.grpc.ContactType;
+import biz.nynja.account.grpc.Date;
+import biz.nynja.account.grpc.DeleteAuthenticationProviderRequest;
+import biz.nynja.account.grpc.ErrorResponse.Cause;
+import biz.nynja.account.grpc.UpdateAccountRequest;
+import biz.nynja.account.grpc.UpdateProfileRequest;
+import biz.nynja.account.phone.PhoneNumberValidator;
+
+/**
+ * Component which contains all validation methods.
+ */
+
+@Component
+public class Validator {
+
+ private static final Logger logger = LoggerFactory.getLogger(Validator.class);
+
+ private static final int MIN_ACCOUNT_NAME_LENGTH = 2;
+ private static final int MAX_ACCOUNT_NAME_LENGTH = 32;
+ private static final int MIN_FIRST_NAME_LENGTH = 2;
+ private static final int MAX_FIRST_NAME_LENGTH = 32;
+ private static final int MIN_LAST_NAME_LENGTH = 0;
+ private static final int MAX_LAST_NAME_LENGTH = 32;
+
+ private PhoneNumberValidator phoneValidator;
+
+ public Validator(PhoneNumberValidator phoneValidator) {
+ this.phoneValidator = phoneValidator;
+
+ }
+
+ public boolean isUsernameValid(String username) {
+
+ logger.debug("Checking username: {}", username);
+
+ final String USERNAME_PATTERN = "^[A-Za-z0-9_]{2,32}$";
+
+ Pattern pattern = Pattern.compile(USERNAME_PATTERN);
+ Matcher matcher = pattern.matcher(username);
+
+ boolean isValid = matcher.matches();
+ logger.debug("Username: {} is valid: {}", username, isValid);
+
+ return isValid;
+ }
+
+ public boolean isEmailValid(String email) {
+ boolean result = true;
+ logger.debug("Checking email: {}", email);
+
+ try {
+ InternetAddress emailAddr = new InternetAddress(email);
+ emailAddr.validate();
+ } catch (AddressException ex) {
+ result = false;
+ }
+ logger.debug("Email: {} is valid: {}", email, result);
+ return result;
+ }
+
+ public boolean isFirstNameValid(String firstName) {
+ logger.debug("Checking First Name: {}", firstName);
+
+ int len = firstName.length();
+ boolean isValid = MIN_FIRST_NAME_LENGTH <= len && len <= MAX_FIRST_NAME_LENGTH;
+
+ logger.debug("First Name: {} is valid: {}", firstName, isValid);
+ return isValid;
+ }
+
+ public boolean isLastNameValid(String lastName) {
+ logger.debug("Checking Last Name: {}", lastName);
+
+ int len = lastName.length();
+ boolean isValid = MIN_LAST_NAME_LENGTH <= len && len <= MAX_LAST_NAME_LENGTH;
+
+ logger.debug("Last Name: {} is valid: {}", lastName, isValid);
+ return isValid;
+ }
+
+ public boolean isValidUsername(String username) {
+ if (username == null) {
+ return false;
+ }
+ return username.matches("[a-zA-Z0-9_]{1,32}");
+ }
+
+ boolean isAccountNameValid(String accountName) {
+ logger.debug("Checking Account Name: {}", accountName);
+
+ int len = accountName.length();
+ boolean isValid = MIN_ACCOUNT_NAME_LENGTH <= len && len <= MAX_ACCOUNT_NAME_LENGTH;
+
+ logger.debug("First Name: {} is valid: {}", accountName, isValid);
+ return isValid;
+ }
+
+ public boolean isValidUuid(String id) {
+ return id.matches("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");
+ }
+
+ public Cause validateAuthProvider(AuthenticationType type, String authProvider) {
+ if (authProvider == null || authProvider.trim().isEmpty()) {
+ return Cause.MISSING_AUTH_PROVIDER_IDENTIFIER;
+ }
+ switch (type) {
+ case MISSING_TYPE:
+ return Cause.MISSING_AUTH_PROVIDER_TYPE;
+ case PHONE:
+ // We expect to receive phone number in the following format : ":"
+ String[] provider = authProvider.split(":");
+ if (provider == null || provider.length != 2) {
+ return Cause.PHONE_NUMBER_INVALID;
+ }
+ if (!phoneValidator.isPhoneNumberValid(provider[1], provider[0])) {
+ return Cause.PHONE_NUMBER_INVALID;
+ }
+ break;
+ case EMAIL:
+ if (!isEmailValid(authProvider)) {
+ return Cause.EMAIL_INVALID;
+ }
+ break;
+ default:
+ break;
+ }
+ return null;
+ }
+
+ public Optional> validateContactInfo(ContactType type, String contactInfoValue) {
+ if (contactInfoValue == null || contactInfoValue.trim().isEmpty()) {
+ return Optional
+ .of(new ImmutablePair<>(Cause.MISSING_CONTACT_INFO_IDENTIFIER, "Missing contact info identifier"));
+ }
+ switch (type) {
+ case MISSING_CONTACT_TYPE:
+ return Optional.of(new ImmutablePair<>(Cause.MISSING_CONTACT_INFO_TYPE, "Missing contact info type"));
+ case PHONE_CONTACT:
+ // We expect to receive phone number in the following format : ":"
+ String[] provider = contactInfoValue.split(":");
+ if (provider == null || provider.length != 2) {
+ return Optional.of(new ImmutablePair<>(Cause.PHONE_NUMBER_INVALID, "Invalid phone number"));
+ }
+ if (!phoneValidator.isPhoneNumberValid(provider[1], provider[0])) {
+ return Optional.of(new ImmutablePair<>(Cause.PHONE_NUMBER_INVALID, "Invalid phone number"));
+ }
+ break;
+ case EMAIL_CONTACT:
+ if (!isEmailValid(contactInfoValue)) {
+ return Optional.of(new ImmutablePair<>(Cause.EMAIL_INVALID, "Invalid email"));
+ }
+ break;
+ default:
+ break;
+ }
+ return Optional.empty();
+ }
+
+ public boolean validateBirthdayIsSet(Date birthday) {
+ return (birthday != null && birthday.getYear() != 0 && birthday.getMonth() != 0 && birthday.getDay() != 0);
+ }
+
+ public Cause validateUpdateAccountRequest(UpdateAccountRequest request) {
+
+ if (request.getUsername() != null && !request.getUsername().trim().isEmpty()
+ && !isUsernameValid(request.getUsername())) {
+ return Cause.USERNAME_INVALID;
+ }
+
+ if (request.getFirstName() != null && request.getFirstName().trim().isEmpty()) {
+ return Cause.MISSING_FIRST_NAME;
+ } else if (!isFirstNameValid(request.getFirstName())) {
+ return Cause.INVALID_FIRST_NAME;
+ }
+
+ if (request.getLastName() != null && !request.getLastName().trim().isEmpty()
+ && !isLastNameValid(request.getLastName())) {
+ return Cause.INVALID_LAST_NAME;
+ }
+
+ if (request.getAccountName() != null && !request.getAccountName().trim().isEmpty()
+ && !isAccountNameValid(request.getAccountName())) {
+ return Cause.ACCOUNT_NAME_INVALID;
+ }
+
+ if (validateBirthdayIsSet(request.getBirthday())) {
+ try {
+ LocalDate.of(request.getBirthday().getYear(), request.getBirthday().getMonth(),
+ request.getBirthday().getDay());
+ } catch (DateTimeException e) {
+ return Cause.INVALID_BIRTHDAY_DATE;
+ }
+ }
+
+ return null;
+ }
+
+ public Cause validateUpdateProfileRequest(UpdateProfileRequest request) {
+ if (!isValidUuid(request.getProfileId())) {
+ return Cause.INVALID_PROFILE_ID;
+ }
+ return null;
+ }
+
+ public Cause validateAddAuthenticationProviderRequest(AddAuthenticationProviderRequest request) {
+ if (!isValidUuid(request.getProfileId())) {
+ return Cause.INVALID_PROFILE_ID;
+ }
+ return validateAuthProvider(request.getAuthenticationProvider().getAuthenticationType(),
+ request.getAuthenticationProvider().getAuthenticationProvider());
+ }
+
+ public Optional> validateEditContactInfoRequest(String accountId,
+ ContactDetails oldContactInfoDetails, ContactDetails editedContactInfoDetails) {
+ Optional> validationResultOldContactInfo = validateContactInfoRequest(accountId,
+ oldContactInfoDetails);
+ if (validationResultOldContactInfo.isPresent()) {
+ return validationResultOldContactInfo;
+ }
+ return validateContactInfoRequest(accountId, editedContactInfoDetails);
+ }
+
+ public Optional> validateContactInfoRequest(String accountId,
+ ContactDetails contactDetails) {
+ if ((accountId == null) || (accountId.isEmpty())) {
+ return Optional.of(new ImmutablePair<>(Cause.MISSING_ACCOUNT_ID, "Missing account id"));
+ }
+ if (contactDetails.getTypeValue() == 0) {
+ return Optional.of(new ImmutablePair<>(Cause.MISSING_CONTACT_INFO_TYPE, "Missing contact info type"));
+
+ }
+ if (contactDetails.getValue() == null || contactDetails.getValue().isEmpty()) {
+ return Optional
+ .of(new ImmutablePair<>(Cause.MISSING_CONTACT_INFO_IDENTIFIER, "Missing contact info identifier"));
+ }
+ if (!isValidUuid(accountId)) {
+ return Optional.of(new ImmutablePair<>(Cause.INVALID_ACCOUNT_ID, "Invalid account id"));
+ }
+ return validateContactInfo(contactDetails.getType(), contactDetails.getValue());
+ }
+
+ public Cause validateDeleteAuthenticationProviderRequest(DeleteAuthenticationProviderRequest request) {
+ if (!isValidUuid(request.getProfileId())) {
+ return Cause.INVALID_PROFILE_ID;
+ }
+ Cause cause = validateAuthProvider(request.getAuthenticationProvider().getAuthenticationType(),
+ request.getAuthenticationProvider().getAuthenticationProvider());
+ if (cause != null) {
+ return cause;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/biz/nynja/account/configuration/CassandraConfig.java b/src/main/java/biz/nynja/account/configuration/CassandraConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..739d5063f39b8523254bf50a494cdee7e90b7e96
--- /dev/null
+++ b/src/main/java/biz/nynja/account/configuration/CassandraConfig.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (C) 2018 Nynja Inc. All rights reserved.
+ */
+package biz.nynja.account.configuration;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.cassandra.config.AbstractCassandraConfiguration;
+import org.springframework.data.cassandra.config.SchemaAction;
+import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification;
+import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories;
+
+import biz.nynja.account.StartupScriptsListener;
+
+@Configuration
+@EnableCassandraRepositories
+@ConditionalOnMissingClass("org.springframework.test.context.junit4.SpringRunner")
+public class CassandraConfig extends AbstractCassandraConfiguration {
+
+ @Value("${spring.data.cassandra.keyspace-name}")
+ private String keyspace;
+
+ @Override
+ protected String getKeyspaceName() {
+ return keyspace;
+ }
+
+ @Value("${spring.data.cassandra.contact-points}")
+ private String contactPoints;
+
+ @Override
+ protected String getContactPoints() {
+ return contactPoints;
+ }
+
+ @Value("${spring.data.cassandra.port}")
+ private int port;
+
+ @Override
+ protected int getPort() {
+ return port;
+ }
+
+ @Override
+ public SchemaAction getSchemaAction() {
+ return SchemaAction.CREATE_IF_NOT_EXISTS;
+ }
+
+ @Override
+ protected List getKeyspaceCreations() {
+ CreateKeyspaceSpecification specification = CreateKeyspaceSpecification.createKeyspace(getKeyspaceName())
+ .ifNotExists().withSimpleReplication();
+ return Arrays.asList(specification);
+ }
+
+ @Override
+ public String[] getEntityBasePackages() {
+ return new String[] { "biz.nynja.account.models" };
+ }
+
+ /**
+ * See {@link StartupScriptsListener} for scripts
+ * that require JPA annotated tables
+ */
+ @Override
+ protected List getStartupScripts() {
+ return super.getStartupScripts();
+ }
+
+ public String getConfiguredKeyspaceName() {
+ return getKeyspaceName();
+ }
+}
diff --git a/src/main/java/biz/nynja/account/grid/ag/AdminServiceImpl.java b/src/main/java/biz/nynja/account/grid/ag/AdminServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..535bd2cbc8425b4da8a8290f3e50cfd1a4c01a9a
--- /dev/null
+++ b/src/main/java/biz/nynja/account/grid/ag/AdminServiceImpl.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright (C) 2018 Nynja Inc. All rights reserved.
+ */
+package biz.nynja.account.grid.ag;
+
+import org.lognet.springboot.grpc.GRpcService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import biz.nynja.account.admin.grpc.AccountsAdminResponse;
+import biz.nynja.account.admin.grpc.AccountsCount;
+import biz.nynja.account.admin.grpc.AdminAccountServiceGrpc;
+import biz.nynja.account.admin.grpc.CreateAccountRequest;
+import biz.nynja.account.admin.grpc.EmptyRequest;
+import biz.nynja.account.admin.grpc.GetAllAccountsRequest;
+import biz.nynja.account.grpc.AccountResponse;
+import biz.nynja.account.grpc.CompletePendingAccountCreationRequest;
+import biz.nynja.account.grpc.CreatePendingAccountRequest;
+import biz.nynja.account.grpc.CreatePendingAccountResponse;
+import biz.nynja.account.repositories.AccountRepository;
+import biz.nynja.account.services.decomposition.AccountCreator;
+import io.grpc.stub.StreamObserver;
+
+/**
+ * gRPC Admin service implementation.
+ * The service extends the protobuf generated class and overrides the needed methods. It also saves/retrieves the admin
+ * information.
+ */
+@GRpcService
+public class AdminServiceImpl extends AdminAccountServiceGrpc.AdminAccountServiceImplBase {
+
+ private static final Logger logger = LoggerFactory.getLogger(AdminServiceImpl.class);
+
+ private AgGridService agGridService;
+ private AccountRepository accountRepository;
+ private final AccountCreator accountCreator;
+
+ public AdminServiceImpl(AgGridService agGridService, AccountRepository accountRepository,
+ AccountCreator accountCreator) {
+ this.agGridService = agGridService;
+ this.accountRepository = accountRepository;
+ this.accountCreator = accountCreator;
+ }
+
+ @Override
+ public void getAllAccounts(GetAllAccountsRequest request, StreamObserver responseObserver) {
+
+ AccountsAdminResponse response = agGridService.getData(request.getEndRow(), request.getStartRow());
+
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ return;
+ }
+
+ @Override
+ public void getCountOfAllAccounts(EmptyRequest request, StreamObserver responseObserver) {
+ long count = accountRepository.count();
+ responseObserver.onNext(AccountsCount.newBuilder().setCount(Math.toIntExact(count)).build());
+ responseObserver.onCompleted();
+ }
+
+ @Override
+ public void createAccount(CreateAccountRequest request, StreamObserver responseObserver) {
+
+ logger.info("Creating account from admin console...");
+ logger.debug("Creating account from admin console: {} ...", request);
+
+ CreatePendingAccountRequest pendingAccountRequest = CreatePendingAccountRequest.newBuilder()
+ .setAuthenticationProvider(request.getAuthenticationProvider())
+ .setAuthenticationType(request.getAuthenticationType()).build();
+ CreatePendingAccountResponse pendingAccountResponse = accountCreator
+ .retrieveCreatePendingAccountResponse(pendingAccountRequest);
+
+ if (pendingAccountResponse.hasError()) {
+ responseObserver.onNext(AccountResponse.newBuilder().setError(pendingAccountResponse.getError()).build());
+ responseObserver.onCompleted();
+ return;
+ }
+
+ CompletePendingAccountCreationRequest completePendingAccount = CompletePendingAccountCreationRequest
+ .newBuilder().setAccountId(pendingAccountResponse.getPendingAccountDetails().getAccountId())
+ .setAvatar(request.getAvatar()).setAccountMark(request.getAccountMark())
+ .setAccountName(request.getAccountName()).setFirstName(request.getFirstName())
+ .setLastName(request.getLastName()).setUsername(request.getUsername())
+ .addAllRoles(request.getRolesList()).setAccessStatus(request.getAccessStatus())
+ .setQrCode(request.getQrCode()).build();
+
+ AccountResponse response = accountCreator.retrieveCompletePendingAccountResponse(completePendingAccount);
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ }
+}
diff --git a/src/main/java/biz/nynja/account/grid/ag/AgGridController.java b/src/main/java/biz/nynja/account/grid/ag/AgGridController.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7a14db4b288859231f58a369ba06ef9ebb51f55
--- /dev/null
+++ b/src/main/java/biz/nynja/account/grid/ag/AgGridController.java
@@ -0,0 +1,39 @@
+package biz.nynja.account.grid.ag;
+
+import java.util.HashMap;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import biz.nynja.account.grid.ag.request.GetRowsRequest;
+import biz.nynja.account.repositories.AccountRepository;
+
+@RestController
+public class AgGridController {
+
+ private AgGridService agGridService;
+
+ private AccountRepository accountRepository;
+
+ @Autowired
+ public AgGridController(AgGridService accountDao, AccountRepository accountRepository) {
+ this.agGridService = accountDao;
+ this.accountRepository = accountRepository;
+ }
+
+ /* @RequestMapping(method = RequestMethod.POST, value = "/getRows")
+ public ResponseEntity getRows(@RequestBody GetRowsRequest request) {
+ return ResponseEntity.ok(agGridService.getData(request));
+ }*/
+
+ @RequestMapping(method = RequestMethod.GET, value = "/getRowsCount")
+ public ResponseEntity> getCountOfRows() {
+ HashMap map = new HashMap<>();
+ map.put("lastRow", accountRepository.count());
+ return ResponseEntity.ok(map);
+ }
+}
diff --git a/src/main/java/biz/nynja/account/grid/ag/AgGridService.java b/src/main/java/biz/nynja/account/grid/ag/AgGridService.java
new file mode 100644
index 0000000000000000000000000000000000000000..19134273e8fe8548fcdf1525b284760d8c355562
--- /dev/null
+++ b/src/main/java/biz/nynja/account/grid/ag/AgGridService.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (C) 2018 Nynja Inc. All rights reserved.
+ */
+package biz.nynja.account.grid.ag;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.cassandra.core.query.CassandraPageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.stereotype.Service;
+
+import biz.nynja.account.admin.grpc.AccountAdminResponse;
+import biz.nynja.account.admin.grpc.AccountsAdminResponse;
+import biz.nynja.account.grpc.AccountDetails;
+import biz.nynja.account.models.Account;
+import biz.nynja.account.repositories.AccountRepository;
+
+@Service
+public class AgGridService {
+
+ private AccountRepository accountRepository;
+
+ private Slice accounts = null;
+
+ @Autowired
+ public AgGridService(AccountRepository accountRepository) {
+ this.accountRepository = accountRepository;
+ }
+
+ public AccountsAdminResponse getData(int endRow, int startRow) {
+
+ Map> pivotValues = new HashMap>();
+
+ // Sort sort = new Sort(new Sort.Order(Direction.ASC, "type"));
+
+ Pageable pageable = CassandraPageRequest.of(0, endRow);
+
+ accounts = accountRepository.findAll(pageable);
+ List rows = new ArrayList<>();
+ accounts.getContent().subList(startRow - 1, accounts.getNumberOfElements()).forEach(account -> {
+ AccountDetails accountDetails = account.toProto();
+ rows.add(accountDetails);
+ });
+
+ // create response with our results
+
+ AccountsAdminResponse response = AccountsAdminResponse.newBuilder()
+ .setAccountsResponse(AccountAdminResponse.newBuilder().addAllAccountDetails(rows).build()).build();
+ return response;
+ }
+
+}
diff --git a/src/main/java/biz/nynja/account/grid/ag/GetRowsResponse.java b/src/main/java/biz/nynja/account/grid/ag/GetRowsResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..164390213ee4509d6420d0a319df666d664d9c3d
--- /dev/null
+++ b/src/main/java/biz/nynja/account/grid/ag/GetRowsResponse.java
@@ -0,0 +1,44 @@
+package biz.nynja.account.grid.ag;
+
+import java.util.List;
+import java.util.Map;
+
+public class GetRowsResponse {
+
+ private List