├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── deploy ├── akka-cluster-member-role.yaml ├── cassandra-values.yaml ├── cicd │ ├── jenkins │ │ ├── Dockerfile │ │ ├── jenkins.yaml │ │ └── plugins.txt │ ├── registry.yaml │ └── socat │ │ ├── Dockerfile │ │ └── entrypoint.sh ├── gateway-sa-role-binding.yaml ├── gateway-sa.yaml ├── gateway │ ├── gateway-deployment.yaml │ └── gateway-service.yaml ├── kafka-persistent-single.yaml ├── projectmanager-sa-role-binding.yaml ├── projectmanager-sa.yaml ├── projectmanager │ ├── projectmanager-deployment.yaml │ └── projectmanager-service.yaml └── tasktick-ingress.yaml ├── gateway-api └── src │ └── main │ └── scala │ └── io │ └── surfkit │ └── gateway │ └── api │ └── GatewayService.scala ├── gateway-impl └── src │ ├── main │ ├── resources │ │ ├── application.conf │ │ ├── application.prod.conf │ │ └── www │ │ │ ├── bundle.js │ │ │ ├── bundle.js.map │ │ │ ├── imgs │ │ │ ├── bg0.png │ │ │ ├── bg1.png │ │ │ ├── bg2.png │ │ │ ├── bg3.png │ │ │ ├── bg4.png │ │ │ ├── bg5.png │ │ │ ├── bg6.png │ │ │ ├── bg7.png │ │ │ ├── bg8.png │ │ │ ├── bg9.png │ │ │ ├── bga.png │ │ │ ├── bgb.png │ │ │ ├── bgc.png │ │ │ ├── bgd.png │ │ │ ├── bge.png │ │ │ ├── bgf.png │ │ │ ├── logo.png │ │ │ └── typebus-logo.png │ │ │ ├── index.html │ │ │ └── js │ │ │ └── fontawesome-all.min.js │ └── scala │ │ └── io │ │ └── surfkit │ │ └── gateway │ │ └── impl │ │ ├── GatewayLoader.scala │ │ ├── GatewayServiceImpl.scala │ │ ├── UserEntity.scala │ │ └── util │ │ ├── JwtTokenUtil.scala │ │ └── SecurePasswordHashing.scala │ └── test │ ├── resources │ └── logback.xml │ └── scala │ └── io │ └── surfkit │ └── gateway │ └── impl │ ├── GatewayServiceSpec.scala │ └── UserEntitySpec.scala ├── project ├── build.properties └── plugins.sbt ├── projectmanager-api └── src │ └── main │ └── scala │ └── io │ └── surfkit │ └── projectmanager │ └── api │ └── ProjectManagerService.scala ├── projectmanager-impl └── src │ ├── main │ ├── resources │ │ ├── application.conf │ │ └── application.prod.conf │ └── scala │ │ └── io │ │ └── surfkit │ │ └── projectmanager │ │ └── impl │ │ ├── ProjectEntity.scala │ │ ├── ProjectManagerLoader.scala │ │ └── ProjectManagerServiceImpl.scala │ └── test │ ├── resources │ └── logback.xml │ └── scala │ └── io │ └── surfkit │ └── projectmanager │ └── impl │ ├── ProjectEntitySpec.scala │ └── ProjectManagerServiceSpec.scala ├── sbt ├── sbt-dist ├── bin │ ├── sbt │ ├── sbt-launch-lib.bash │ ├── sbt-launch.jar │ └── sbt.bat └── conf │ ├── sbtconfig.txt │ └── sbtopts ├── sbt.bat ├── screen-shot.png └── tasktick-pwa ├── .gitignore ├── build ├── server │ └── main.js └── shared │ └── foo.js ├── gulpfile.js ├── package.json ├── src ├── client │ ├── App.tsx │ ├── components │ │ ├── ProjectCard.tsx │ │ ├── TablePager.tsx │ │ ├── TaskCard.tsx │ │ └── TraceTable.tsx │ ├── index.tsx │ ├── pages │ │ ├── Account.tsx │ │ ├── Projects.tsx │ │ ├── SignIn.tsx │ │ └── Users.tsx │ ├── serviceWorker.ts │ ├── socket │ │ └── WebSocket.tsx │ ├── stores │ │ ├── data.tsx │ │ └── index.tsx │ └── withRoot.tsx └── server │ └── main.ts ├── tsconfig.json └── www ├── bundle.js ├── bundle.js.map ├── imgs ├── bg0.png ├── bg1.png ├── bg2.png ├── bg3.png ├── bg4.png ├── bg5.png ├── bg6.png ├── bg7.png ├── bg8.png ├── bg9.png ├── bga.png ├── bgb.png ├── bgc.png ├── bgd.png ├── bge.png ├── bgf.png ├── logo.png └── typebus-logo.png ├── index.html └── js └── fontawesome-all.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/target 3 | target 4 | tmp 5 | .history 6 | dist 7 | /.idea 8 | /*.iml 9 | /out 10 | /.idea_modules 11 | /.classpath 12 | /.project 13 | /RUNNING_PID 14 | /.settings 15 | *.log 16 | **/secrets.conf 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | use this file except in compliance with the License. You may obtain a copy of 5 | the License at 6 | 7 | [http://www.apache.org/licenses/LICENSE-2.0] 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | License for the specific language governing permissions and limitations under 13 | the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TaskTick 2 | 3 | An example Task Manager project that has been created using Lagom. 4 | TaskTick is fantastic ;) 5 | 6 | ## Blog Post 7 | https://medium.com/@coreyauger/rapid-and-highly-scalable-development-using-scala-and-lagom-17a9205da42b 8 | 9 | ## Overview 10 | This project goes with my blog post and serves to demo how fast and easy it is to produce a modern progressive web application with Lagom. 11 | 12 | Some of the features include: 13 | 14 | * React FrontEnd (PWA) that connects to the Lagom backend using a WebSocket connection. 15 | * JWT authentication and Auth management: 16 | * This includes login and registration 17 | * Restricting access to routes based on the jwt auth token. 18 | * Publish Events to kafka stream (for a future Notification or other services) 19 | * Handle OAuth flow to allow for users to connect their github accounts. 20 | * Event Sourced Project and User Entities 21 | 22 | Screen Shot 23 | ![screen-shot](screen-shot.png) 24 | 25 | ## How to Run 26 | 27 | clone the repository 28 | 29 | `git clone git@github.com:coreyauger/tasktick.git` 30 | 31 | enter the new directory 32 | 33 | `cd tasktick` 34 | 35 | compile and run the react front end 36 | 37 | ``` 38 | cd tasktick-pwa 39 | npm i 40 | gulp watch 41 | ``` 42 | 43 | change the refrence to where the compiled `www` directory location is in your `application.conf` 44 | 45 | ```yaml 46 | www{ 47 | base-url = "/Users/coreyauger/projects/tasktick/tasktick-pwa/www" 48 | } 49 | ``` 50 | 51 | enter sbt and use the `runAll` 52 | 53 | ``` 54 | sbt> runAll 55 | ``` 56 | 57 | direct your browser at: 58 | 59 | `http://localhost:9000/p/signin` 60 | 61 | 62 | play! 63 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization in ThisBuild := "io.surfkit" 2 | version in ThisBuild := "1.0-SNAPSHOT" 3 | 4 | // the Scala version that will be used for cross-compiled libraries 5 | scalaVersion in ThisBuild := "2.12.4" 6 | 7 | val macwire = "com.softwaremill.macwire" %% "macros" % "2.3.0" % "provided" 8 | val scalaTest = "org.scalatest" %% "scalatest" % "3.0.4" % Test 9 | val jwt = "com.pauldijou" %% "jwt-play-json" % "2.1.0" 10 | 11 | val akkaMgmtVersion = "0.20.0" 12 | val akkaManagement = "com.lightbend.akka.management" %% "akka-management" % akkaMgmtVersion 13 | val akkaMgmtHttp = "com.lightbend.akka.management" %% "akka-management-cluster-http" % akkaMgmtVersion 14 | val akkaClusterBootstrap = "com.lightbend.akka.management" %% "akka-management-cluster-bootstrap" % akkaMgmtVersion 15 | val akkaServiceDiscovery = "com.lightbend.akka.discovery" %% "akka-discovery-dns" % akkaMgmtVersion 16 | val akkaDiscoveryK8s = "com.lightbend.akka.discovery" %% "akka-discovery-kubernetes-api" % akkaMgmtVersion 17 | val akkaDiscoveryConfig = "com.lightbend.akka.discovery" %% "akka-discovery-config" % akkaMgmtVersion 18 | 19 | val akkaManagementDeps = Seq(akkaManagement, akkaMgmtHttp, akkaClusterBootstrap, akkaServiceDiscovery, akkaDiscoveryK8s, akkaDiscoveryConfig) 20 | 21 | lazy val `tasktick` = (project in file(".")) 22 | .aggregate(`gateway-api`, `gateway-impl`, `projectmanager-api`, `projectmanager-impl`) 23 | 24 | 25 | lazy val `projectmanager-api` = (project in file("projectmanager-api")) 26 | .settings( 27 | libraryDependencies ++= Seq( 28 | lagomScaladslApi 29 | ) 30 | ) 31 | 32 | lazy val `projectmanager-impl` = (project in file("projectmanager-impl")) 33 | .enablePlugins(LagomScala) 34 | .settings( 35 | libraryDependencies ++= Seq( 36 | lagomScaladslPersistenceCassandra, 37 | lagomScaladslKafkaBroker, 38 | lagomScaladslTestKit, 39 | guice, 40 | macwire, 41 | scalaTest 42 | ) ++ akkaManagementDeps 43 | ) 44 | .settings(lagomForkedTestSettings: _*) 45 | .settings( 46 | dockerAlias := dockerAlias.value.withRegistryHost(Option("127.0.0.1:30400")) 47 | ) 48 | .dependsOn(`projectmanager-api`) 49 | 50 | 51 | lazy val `gateway-api` = (project in file("gateway-api")) 52 | .settings( 53 | libraryDependencies ++= Seq( 54 | lagomScaladslApi 55 | ) 56 | ).dependsOn(`projectmanager-api`) 57 | 58 | lazy val `gateway-impl` = (project in file("gateway-impl")) 59 | .enablePlugins(LagomScala) 60 | .settings( 61 | libraryDependencies ++= Seq( 62 | lagomScaladslPersistenceCassandra, 63 | lagomScaladslKafkaBroker, 64 | lagomScaladslTestKit, 65 | jwt, 66 | macwire, 67 | scalaTest 68 | ) ++ akkaManagementDeps 69 | ) 70 | .settings(lagomForkedTestSettings: _*) 71 | .settings( 72 | dockerAlias := dockerAlias.value.withRegistryHost(Option("127.0.0.1:30400")) 73 | ) 74 | .dependsOn(`gateway-api`) 75 | .dependsOn(`projectmanager-api`) 76 | 77 | dockerBaseImage := "openjdk:8-jre-slim" -------------------------------------------------------------------------------- /deploy/akka-cluster-member-role.yaml: -------------------------------------------------------------------------------- 1 | kind: Role 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: akka-cluster-member 5 | rules: 6 | - apiGroups: [""] # "" indicates the core API group 7 | resources: ["pods"] 8 | verbs: ["get", "watch", "list"] 9 | -------------------------------------------------------------------------------- /deploy/cassandra-values.yaml: -------------------------------------------------------------------------------- 1 | ## Cassandra image version 2 | ## ref: https://hub.docker.com/r/library/cassandra/ 3 | image: 4 | repo: "cassandra" 5 | tag: "3" 6 | pullPolicy: IfNotPresent 7 | ## Specify ImagePullSecrets for Pods 8 | ## ref: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod 9 | # pullSecrets: myregistrykey 10 | 11 | ## Specify a service type 12 | ## ref: http://kubernetes.io/docs/user-guide/services/ 13 | service: 14 | type: ClusterIP 15 | 16 | ## Persist data to a persistent volume 17 | persistence: 18 | enabled: false 19 | ## cassandra data Persistent Volume Storage Class 20 | ## If defined, storageClassName: 21 | ## If set to "-", storageClassName: "", which disables dynamic provisioning 22 | ## If undefined (the default) or set to null, no storageClassName spec is 23 | ## set, choosing the default provisioner. (gp2 on AWS, standard on 24 | ## GKE, AWS & OpenStack) 25 | ## 26 | storageClass: "standard" 27 | accessMode: ReadWriteOnce 28 | size: 5Gi 29 | 30 | ## Configure resource requests and limits 31 | ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ 32 | ## Minimum memory for development is 4GB and 2 CPU cores 33 | ## Minimum memory for production is 8GB and 4 CPU cores 34 | ## ref: http://docs.datastax.com/en/archived/cassandra/2.0/cassandra/architecture/architecturePlanningHardware_c.html 35 | resources: 36 | # requests: 37 | # memory: 256Mi 38 | # cpu: 100m 39 | # limits: 40 | # memory: 1Gi 41 | # cpu: 2 42 | 43 | ## Change cassandra configuration parameters below: 44 | ## ref: http://docs.datastax.com/en/cassandra/3.0/cassandra/configuration/configCassandra_yaml.html 45 | ## Recommended max heap size is 1/2 of system memory 46 | ## Recommeneed heap new size is 1/4 of max heap size 47 | ## ref: http://docs.datastax.com/en/cassandra/3.0/cassandra/operations/opsTuneJVM.html 48 | config: 49 | cluster_name: cassandra 50 | cluster_size: 1 51 | seed_size: 1 52 | num_tokens: 0 53 | # If you want Cassandra to use this datacenter and rack name, 54 | # you need to set endpoint_snitch to GossipingPropertyFileSnitch. 55 | # Otherwise, these values are ignored and datacenter1 and rack1 56 | # are used. 57 | dc_name: DC1 58 | rack_name: RAC1 59 | endpoint_snitch: SimpleSnitch 60 | max_heap_size: 512M 61 | heap_new_size: 256M 62 | start_rpc: false 63 | ports: 64 | cql: 9042 65 | thrift: 9160 66 | # If a JVM Agent is in place 67 | # agent: 61621 68 | 69 | ## Liveness and Readiness probe values. 70 | ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/ 71 | livenessProbe: 72 | initialDelaySeconds: 90 73 | periodSeconds: 30 74 | timeoutSeconds: 5 75 | successThreshold: 1 76 | failureThreshold: 3 77 | readinessProbe: 78 | initialDelaySeconds: 30 79 | periodSeconds: 30 80 | timeoutSeconds: 5 81 | successThreshold: 1 82 | failureThreshold: 3 83 | 84 | ## Configure node selector. Edit code below for adding selector to pods 85 | ## ref: https://kubernetes.io/docs/user-guide/node-selection/ 86 | # selector: 87 | # nodeSelector: 88 | # cloud.google.com/gke-nodepool: pool-db 89 | 90 | ## Additional pod labels 91 | ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ 92 | podLabels: {} 93 | 94 | ## Additional pod-level settings 95 | podSettings: 96 | # Change this to give pods more time to properly leave the cluster when not using persistent storage. 97 | terminationGracePeriodSeconds: 30 98 | 99 | podManagementPolicy: OrderedReady 100 | updateStrategy: 101 | type: OnDelete 102 | 103 | ## Pod Security Context 104 | securityContext: 105 | enabled: false 106 | fsGroup: 999 107 | runAsUser: 999 108 | 109 | ## Affinity for pod assignment 110 | ## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity 111 | affinity: {} 112 | 113 | commandOverrides: 114 | - "bash" 115 | 116 | argsOverrides: 117 | - "/etc/cassandra/cassandra.sh" 118 | 119 | configOverrides: 120 | cassandra.sh: | 121 | chown -R cassandra:cassandra /var/lib/cassandra 122 | chmod 777 /var/lib/cassandra 123 | chmod 777 /etc/cassandra 124 | 125 | # Custom Cassandra configuration for small footprint 126 | # This was inspired from: https://github.com/lightbend/reactive-sandbox/blob/master/docker/rs-exec 127 | # Note: We opted not to use Reactive Sandbox for the course, because it is too minimal for the course. 128 | 129 | ip="$(grep "$HOSTNAME" /etc/hosts|awk '{print $1}')" 130 | 131 | sed -i -e "s/num_tokens/\#num_tokens/" /etc/cassandra/cassandra.yaml 132 | sed -i -e "s/^rpc_address.*/rpc_address: $ip/" /etc/cassandra/cassandra.yaml 133 | sed -i -e "s/^listen_address.*/listen_address: $ip/" /etc/cassandra/cassandra.yaml 134 | sed -i -e 's/- seeds: "127.0.0.1"/- seeds: "'"$ip"'"/' /etc/cassandra/cassandra.yaml 135 | 136 | cat <> /etc/cassandra/cassandra.yaml 137 | initial_token: 0 138 | rpc_server_type: hsha 139 | concurrent_reads: 2 140 | concurrent_writes: 2 141 | concurrent_compactors: 1 142 | compaction_throughput_mb_per_sec: 0 143 | key_cache_size_in_mb: 0 144 | rpc_min_threads: 1 145 | rpc_max_threads: 1 146 | EOT 147 | 148 | find /var/lib/cassandra /var/log/cassandra -exec chown cassandra:cassandra '{}' \; 149 | gosu cassandra cassandra -f 150 | -------------------------------------------------------------------------------- /deploy/cicd/jenkins/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jenkins/jenkins:2.136 2 | USER root 3 | 4 | #Pre-Install Jenkins Plugins 5 | COPY plugins.txt /usr/share/jenkins/ref/plugins.txt 6 | RUN /usr/local/bin/install-plugins.sh < /usr/share/jenkins/ref/plugins.txt 7 | 8 | #Installing Docker 9 | RUN apt-get update && apt-get install software-properties-common apt-transport-https ca-certificates -y; \ 10 | curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -;\ 11 | add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable";\ 12 | apt-get update && apt-get install docker-ce -y 13 | 14 | #Installing kubectl from Docker 15 | RUN curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -;\ 16 | touch /etc/apt/sources.list.d/kubernetes.list;\ 17 | echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list;\ 18 | apt-get update && apt-get install -y kubectl 19 | 20 | # Grant jenkins user group access to /var/run/docker.sock 21 | RUN addgroup --gid 1001 dsock 22 | RUN gpasswd -a jenkins dsock 23 | USER jenkins -------------------------------------------------------------------------------- /deploy/cicd/jenkins/jenkins.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolume 2 | apiVersion: v1 3 | metadata: 4 | name: jenkins 5 | labels: 6 | type: local 7 | spec: 8 | capacity: 9 | storage: 2Gi 10 | accessModes: 11 | - ReadWriteOnce 12 | hostPath: 13 | path: "/data/jenkins/" 14 | 15 | --- 16 | 17 | kind: PersistentVolumeClaim 18 | apiVersion: v1 19 | metadata: 20 | name: jenkins-claim 21 | spec: 22 | accessModes: 23 | - ReadWriteOnce 24 | resources: 25 | requests: 26 | storage: 2Gi 27 | --- 28 | 29 | apiVersion: v1 30 | kind: ServiceAccount 31 | metadata: 32 | name: jenkins 33 | namespace: default 34 | automountServiceAccountToken: true 35 | 36 | --- 37 | 38 | apiVersion: rbac.authorization.k8s.io/v1 39 | kind: ClusterRoleBinding 40 | metadata: 41 | name: Jenkins-cluster-admin 42 | roleRef: 43 | apiGroup: rbac.authorization.k8s.io 44 | kind: ClusterRole 45 | name: cluster-admin 46 | subjects: 47 | - kind: ServiceAccount 48 | name: jenkins 49 | namespace: default 50 | 51 | --- 52 | 53 | apiVersion: v1 54 | kind: ConfigMap 55 | metadata: 56 | creationTimestamp: null 57 | name: kubectl-jenkins-context 58 | data: 59 | kubectl-config-context.sh: |- 60 | #!/bin/bash -v 61 | kubectl config set-credentials jenkins --token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) 62 | kubectl config set-cluster minikube --server="https://192.168.99.100:8443" --certificate-authority="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 63 | kubectl config set-context jenkins-minikube --cluster=minikube --user=jenkins --namespace=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) 64 | kubectl config use-context jenkins-minikube 65 | chmod 755 ~/.kube/config 66 | --- 67 | 68 | apiVersion: v1 69 | kind: Service 70 | metadata: 71 | name: jenkins 72 | labels: 73 | app: jenkins 74 | spec: 75 | ports: 76 | - port: 80 77 | targetPort: 8080 78 | selector: 79 | app: jenkins 80 | tier: jenkins 81 | type: NodePort 82 | 83 | --- 84 | 85 | apiVersion: extensions/v1beta1 86 | kind: Deployment 87 | metadata: 88 | name: jenkins 89 | labels: 90 | app: jenkins 91 | spec: 92 | strategy: 93 | type: Recreate 94 | template: 95 | metadata: 96 | labels: 97 | app: jenkins 98 | tier: jenkins 99 | spec: 100 | serviceAccountName: jenkins 101 | initContainers: 102 | - image: lachlanevenson/k8s-kubectl:v1.11.2 103 | name: kubectl-config 104 | command: 105 | - "/bin/sh" 106 | args: 107 | - "/kubectl-config-context.sh" 108 | volumeMounts: 109 | - name: kubeconfig 110 | mountPath: "/root/.kube" 111 | - name: kubectl-jenkins-context 112 | mountPath: "/kubectl-config-context.sh" 113 | subPath: "kubectl-config-context.sh" 114 | containers: 115 | - image: 127.0.0.1:30400/jenkins:latest 116 | name: jenkins 117 | securityContext: 118 | privileged: true 119 | volumeMounts: 120 | - name: kubeconfig 121 | mountPath: /var/jenkins_home/.kube 122 | - name: docker 123 | mountPath: /var/run/docker.sock 124 | - name: jenkins-persistent-storage 125 | mountPath: /var/jenkins_home 126 | ports: 127 | - containerPort: 8080 128 | name: jenkins 129 | volumes: 130 | - name: kubectl-jenkins-context 131 | configMap: 132 | name: kubectl-jenkins-context 133 | items: 134 | - key: kubectl-config-context.sh 135 | path: kubectl-config-context.sh 136 | - name: kubeconfig 137 | emptyDir: {} 138 | - name: docker 139 | hostPath: 140 | path: /var/run/docker.sock 141 | - name: jenkins-persistent-storage 142 | persistentVolumeClaim: 143 | claimName: jenkins-claim 144 | -------------------------------------------------------------------------------- /deploy/cicd/jenkins/plugins.txt: -------------------------------------------------------------------------------- 1 | jdk-tool:1.1 2 | script-security:1.44 3 | command-launcher:1.2 4 | cloudbees-folder:6.5.1 5 | bouncycastle-api:2.16.3 6 | structs:1.14 7 | workflow-step-api:2.16 8 | scm-api:2.2.7 9 | workflow-api:2.29 10 | junit:1.24 11 | antisamy-markup-formatter:1.5 12 | token-macro:2.5 13 | build-timeout:1.19 14 | credentials:2.1.18 15 | ssh-credentials:1.14 16 | plain-credentials:1.4 17 | credentials-binding:1.16 18 | timestamper:1.8.10 19 | workflow-support:2.20 20 | durable-task:1.25 21 | workflow-durable-task-step:2.20 22 | matrix-project:1.13 23 | resource-disposer:0.12 24 | ws-cleanup:0.34 25 | ant:1.8 26 | gradle:1.29 27 | pipeline-milestone-step:1.3.1 28 | jquery-detached:1.2.1 29 | jackson2-api:2.8.11.3 30 | ace-editor:1.1 31 | workflow-scm-step:2.6 32 | workflow-cps:2.54 33 | pipeline-input-step:2.8 34 | pipeline-stage-step:2.3 35 | workflow-job:2.24 36 | pipeline-graph-analysis:1.7 37 | pipeline-rest-api:2.10 38 | handlebars:1.1.1 39 | momentjs:1.1.1 40 | pipeline-stage-view:2.10 41 | pipeline-build-step:2.7 42 | pipeline-model-api:1.3.1 43 | pipeline-model-extensions:1.3.1 44 | apache-httpcomponents-client-4-api:4.5.5-3.0 45 | jsch:0.1.54.2 46 | git-client:2.7.3 47 | git-server:1.7 48 | workflow-cps-global-lib:2.9 49 | display-url-api:2.2.0 50 | mailer:1.21 51 | branch-api:2.0.20 52 | workflow-multibranch:2.20 53 | authentication-tokens:1.3 54 | docker-commons:1.13 55 | workflow-basic-steps:2.9 56 | docker-workflow:1.17 57 | pipeline-stage-tags-metadata:1.3.1 58 | pipeline-model-declarative-agent:1.1.1 59 | pipeline-model-definition:1.3.1 60 | workflow-aggregator:2.5 61 | github-api:1.92 62 | git:3.9.1 63 | github:1.29.2 64 | github-branch-source:2.3.6 65 | pipeline-github-lib:1.0 66 | mapdb-api:1.0.9.0 67 | subversion:2.11.1 68 | ssh-slaves:1.26 69 | matrix-auth:2.3 70 | pam-auth:1.3 71 | ldap:1.20 72 | email-ext:2.63 73 | kubernetes-cd:0.2.3 74 | azure-commons:0.2.6 -------------------------------------------------------------------------------- /deploy/cicd/registry.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolume 2 | apiVersion: v1 3 | metadata: 4 | name: registry 5 | labels: 6 | type: local 7 | spec: 8 | capacity: 9 | storage: 4Gi 10 | accessModes: 11 | - ReadWriteOnce 12 | hostPath: 13 | path: "/data/registry/" 14 | 15 | --- 16 | kind: PersistentVolumeClaim 17 | apiVersion: v1 18 | metadata: 19 | name: registry-claim 20 | spec: 21 | accessModes: 22 | - ReadWriteOnce 23 | resources: 24 | requests: 25 | storage: 4Gi 26 | --- 27 | 28 | apiVersion: v1 29 | kind: Service 30 | metadata: 31 | name: registry 32 | labels: 33 | app: registry 34 | spec: 35 | ports: 36 | - port: 5000 37 | targetPort: 5000 38 | nodePort: 30400 39 | name: registry 40 | selector: 41 | app: registry 42 | tier: registry 43 | type: NodePort 44 | --- 45 | 46 | apiVersion: v1 47 | kind: Service 48 | metadata: 49 | name: registry-ui 50 | labels: 51 | app: registry 52 | spec: 53 | ports: 54 | - port: 8080 55 | targetPort: 8080 56 | name: registry 57 | selector: 58 | app: registry 59 | tier: registry 60 | type: NodePort 61 | --- 62 | 63 | apiVersion: extensions/v1beta1 64 | kind: Deployment 65 | metadata: 66 | name: registry 67 | labels: 68 | app: registry 69 | spec: 70 | strategy: 71 | type: Recreate 72 | template: 73 | metadata: 74 | labels: 75 | app: registry 76 | tier: registry 77 | spec: 78 | containers: 79 | - image: registry:2 80 | name: registry 81 | volumeMounts: 82 | - name: docker 83 | mountPath: /var/run/docker.sock 84 | - name: registry-persistent-storage 85 | mountPath: /var/lib/registry 86 | ports: 87 | - containerPort: 5000 88 | name: registry 89 | - name: registryui 90 | image: hyper/docker-registry-web:latest 91 | ports: 92 | - containerPort: 8080 93 | env: 94 | - name: REGISTRY_URL 95 | value: http://localhost:5000/v2 96 | - name: REGISTRY_NAME 97 | value: cluster-registry 98 | volumes: 99 | - name: docker 100 | hostPath: 101 | path: /var/run/docker.sock 102 | - name: registry-persistent-storage 103 | persistentVolumeClaim: 104 | claimName: registry-claim -------------------------------------------------------------------------------- /deploy/cicd/socat/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | #USER root 3 | #Installing socat 4 | RUN apk update && apk upgrade && apk add bash socat 5 | COPY entrypoint.sh entrypoint.sh 6 | ENTRYPOINT ["sh", "./entrypoint.sh"] 7 | -------------------------------------------------------------------------------- /deploy/cicd/socat/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | socat TCP4-LISTEN:5000,fork,reuseaddr TCP4:$REG_IP:$REG_PORT 4 | -------------------------------------------------------------------------------- /deploy/gateway-sa-role-binding.yaml: -------------------------------------------------------------------------------- 1 | kind: RoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: gateway-akka-cluster 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: akka-cluster-member 9 | subjects: 10 | - kind: ServiceAccount 11 | namespace: default 12 | name: gateway-sa 13 | -------------------------------------------------------------------------------- /deploy/gateway-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: gateway-sa 5 | -------------------------------------------------------------------------------- /deploy/gateway/gateway-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: gateway 5 | labels: 6 | app: gateway 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: gateway 12 | template: 13 | metadata: 14 | labels: 15 | app: gateway 16 | spec: 17 | serviceAccountName: gateway-sa 18 | containers: 19 | - name: gateway 20 | image: 127.0.0.1:30400/gateway-impl:1.0-SNAPSHOT 21 | ports: 22 | - name: http 23 | containerPort: 9000 24 | - name: akka-remote 25 | containerPort: 2552 26 | - name: akka-mgmt-http 27 | containerPort: 8558 28 | env: 29 | - name: "HTTP_BIND_ADDRESS" 30 | value: "0.0.0.0" 31 | - name: JAVA_OPTS 32 | value: "-Dconfig.resource=application.prod.conf -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" 33 | - name: CASSANDRA_CONTACT_POINT 34 | value: "cassandra.cassandra" 35 | - name: KAFKA_BROKERS_SERVICE_URL 36 | value: "tasktick-strimzi-kafka-bootstrap.kafka:9092" 37 | resources: 38 | requests: 39 | memory: "256Mi" 40 | cpu: "100m" 41 | limits: 42 | memory: "2Gi" 43 | readinessProbe: 44 | httpGet: 45 | path: /ready 46 | port: akka-mgmt-http 47 | initialDelaySeconds: 10 48 | periodSeconds: 5 49 | livenessProbe: 50 | httpGet: 51 | path: /alive 52 | port: akka-mgmt-http 53 | initialDelaySeconds: 90 54 | periodSeconds: 30 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /deploy/gateway/gateway-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: gateway 6 | name: gateway-svc 7 | spec: 8 | type: ClusterIP 9 | selector: 10 | app: gateway 11 | ports: 12 | - name: http-tasktick 13 | port: 80 14 | targetPort: http 15 | -------------------------------------------------------------------------------- /deploy/kafka-persistent-single.yaml: -------------------------------------------------------------------------------- 1 | #Source: https://github.com/strimzi/strimzi-kafka-operator/blob/master/examples/kafka/kafka-persistent-single.yaml 2 | apiVersion: kafka.strimzi.io/v1alpha1 3 | kind: Kafka 4 | metadata: 5 | name: tasktick-strimzi 6 | spec: 7 | kafka: 8 | replicas: 1 9 | listeners: 10 | plain: {} 11 | tls: {} 12 | config: 13 | offsets.topic.replication.factor: 1 14 | transaction.state.log.replication.factor: 1 15 | transaction.state.log.min.isr: 1 16 | storage: 17 | type: persistent-claim 18 | size: 1Gi 19 | deleteClaim: false 20 | readinessProbe: 21 | initialDelaySeconds: 15 22 | timeoutSeconds: 5 23 | livenessProbe: 24 | initialDelaySeconds: 60 25 | timeoutSeconds: 10 26 | zookeeper: 27 | replicas: 1 28 | storage: 29 | type: persistent-claim 30 | size: 1Gi 31 | deleteClaim: false 32 | entityOperator: 33 | topicOperator: {} 34 | userOperator: {} 35 | -------------------------------------------------------------------------------- /deploy/projectmanager-sa-role-binding.yaml: -------------------------------------------------------------------------------- 1 | kind: RoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: projectmanager-akka-cluster 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: akka-cluster-member 9 | subjects: 10 | - kind: ServiceAccount 11 | namespace: default 12 | name: projectmanager-sa 13 | -------------------------------------------------------------------------------- /deploy/projectmanager-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: projectmanager-sa 5 | -------------------------------------------------------------------------------- /deploy/projectmanager/projectmanager-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: projectmanager 5 | labels: 6 | app: projectmanager 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: projectmanager 12 | template: 13 | metadata: 14 | labels: 15 | app: projectmanager 16 | spec: 17 | serviceAccountName: projectmanager-sa 18 | containers: 19 | - name: projectmanager 20 | image: 127.0.0.1:30400/projectmanager-impl:1.0-SNAPSHOT 21 | ports: 22 | - name: http 23 | containerPort: 9000 24 | - name: akka-remote 25 | containerPort: 2552 26 | - name: akka-mgmt-http 27 | containerPort: 8558 28 | env: 29 | - name: "HTTP_BIND_ADDRESS" 30 | value: "0.0.0.0" 31 | - name: JAVA_OPTS 32 | value: "-Dconfig.resource=application.prod.conf -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" 33 | - name: CASSANDRA_CONTACT_POINT 34 | value: "cassandra.cassandra" 35 | - name: KAFKA_BROKERS_SERVICE_URL 36 | value: "tasktick-strimzi-kafka-bootstrap.kafka:9092" 37 | resources: 38 | requests: 39 | memory: "256Mi" 40 | cpu: "100m" 41 | limits: 42 | memory: "2Gi" 43 | readinessProbe: 44 | httpGet: 45 | path: /ready 46 | port: akka-mgmt-http 47 | initialDelaySeconds: 10 48 | periodSeconds: 5 49 | livenessProbe: 50 | httpGet: 51 | path: /alive 52 | port: akka-mgmt-http 53 | initialDelaySeconds: 90 54 | periodSeconds: 30 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /deploy/projectmanager/projectmanager-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: projectmanager 6 | name: projectmanager-svc 7 | spec: 8 | type: ClusterIP 9 | selector: 10 | app: projectmanager 11 | ports: 12 | - name: pm-http 13 | port: 9000 14 | targetPort: http 15 | -------------------------------------------------------------------------------- /deploy/tasktick-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: tasktick 5 | spec: 6 | rules: 7 | - host: tasktick.io 8 | http: 9 | paths: 10 | - path: / 11 | backend: 12 | serviceName: gateway-svc 13 | servicePort: 80 14 | -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | play.application.loader = io.surfkit.gateway.impl.GatewayLoader 4 | 5 | lagom.services { 6 | projects = "http://projectmanager-svc.default:9000" 7 | projects = ${?PROJECTMANAGER_SERVICE_URL} 8 | } 9 | 10 | 11 | ###################################### 12 | # Persistence (Cassandra) Configuration 13 | ###################################### 14 | 15 | //Use same keyspace for journal, snapshot-store and read-side. 16 | //Each service should be in a unique keyspace. 17 | gateway.cassandra.keyspace = gateway 18 | 19 | cassandra-journal{ 20 | keyspace = ${gateway.cassandra.keyspace} 21 | keyspace-autocreate = true 22 | tables-autocreate = true 23 | } 24 | cassandra-snapshot-store{ 25 | keyspace = ${gateway.cassandra.keyspace} 26 | keyspace-autocreate = true 27 | tables-autocreate = true 28 | } 29 | lagom.persistence.read-side.cassandra{ 30 | keyspace = ${gateway.cassandra.keyspace} 31 | keyspace-autocreate = true 32 | } 33 | 34 | 35 | # The properties below override Lagom default configuration with the recommended values for new projects. 36 | # 37 | # Lagom has not yet made these settings the defaults for backward-compatibility reasons. 38 | 39 | # Prefer 'ddata' over 'persistence' to share cluster sharding state for new projects. 40 | # See https://doc.akka.io/docs/akka/current/cluster-sharding.html#distributed-data-vs-persistence-mode 41 | akka.cluster.sharding.state-store-mode = ddata 42 | 43 | # Enable the serializer provided in Akka 2.5.8+ for akka.Done and other internal 44 | # messages to avoid the use of Java serialization. 45 | akka.actor.serialization-bindings { 46 | "akka.Done" = akka-misc 47 | "akka.actor.Address" = akka-misc 48 | "akka.remote.UniqueAddress" = akka-misc 49 | } 50 | 51 | 52 | # https://discuss.lightbend.com/t/no-configuration-setting-found-for-key-decode-max-size/2738/3 53 | akka.http.routing.decode-max-size = 8m 54 | 55 | cassandra-query-journal.refresh-interval = 1s 56 | 57 | lagom.persistence { 58 | 59 | # As a rule of thumb, the number of shards should be a factor ten greater 60 | # than the planned maximum number of cluster nodes. Less shards than number 61 | # of nodes will result in that some nodes will not host any shards. Too many 62 | # shards will result in less efficient management of the shards, e.g. 63 | # rebalancing overhead, and increased latency because the coordinator is 64 | # involved in the routing of the first message for each shard. The value 65 | # must be the same on all nodes in a running cluster. It can be changed 66 | # after stopping all nodes in the cluster. 67 | max-number-of-shards = 100 68 | 69 | # Persistent entities saves snapshots after this number of persistent 70 | # events. Snapshots are used to reduce recovery times. 71 | # It may be configured to "off" to disable snapshots. 72 | # Author note: snapshotting turned off 73 | snapshot-after = off 74 | 75 | # A persistent entity is passivated automatically if it does not receive 76 | # any messages during this timeout. Passivation is performed to reduce 77 | # memory consumption. Objects referenced by the entity can be garbage 78 | # collected after passivation. Next message will activate the entity 79 | # again, which will recover its state from persistent storage. Set to 0 80 | # to disable passivation - this should only be done when the number of 81 | # entities is bounded and their state, sharded across the cluster, will 82 | # fit in memory. 83 | # Author note: Set to one day - this may be a bit long for production. 84 | passivate-after-idle-timeout = 86400s 85 | 86 | # Specifies that entities run on cluster nodes with a specific role. 87 | # If the role is not specified (or empty) all nodes in the cluster are used. 88 | # The entities can still be accessed from other nodes. 89 | run-entities-on-role = "" 90 | 91 | # Default timeout for PersistentEntityRef.ask replies. 92 | # Author note: Made longer to support potentially slower Minikube environment 93 | ask-timeout = 60s 94 | 95 | dispatcher { 96 | type = Dispatcher 97 | executor = "thread-pool-executor" 98 | thread-pool-executor { 99 | fixed-pool-size = 16 100 | } 101 | throughput = 1 102 | } 103 | } 104 | 105 | lagom.persistence.read-side { 106 | 107 | # how long should we wait when retrieving the last known offset 108 | # LROK Author Note: Default is 5s, this has been made longer for the course. 109 | offset-timeout = 60s 110 | 111 | # Exponential backoff for failures in ReadSideProcessor 112 | failure-exponential-backoff { 113 | # minimum (initial) duration until processor is started again 114 | # after failure 115 | min = 3s 116 | 117 | # the exponential back-off is capped to this duration 118 | max = 30s 119 | 120 | # additional random delay is based on this factor 121 | random-factor = 0.2 122 | } 123 | 124 | # The amount of time that a node should wait for the global prepare callback to execute 125 | # LROK Author Note: Default is 20s, this has been made longer for the course. 126 | global-prepare-timeout = 60s 127 | 128 | # Specifies that the read side processors should run on cluster nodes with a specific role. 129 | # If the role is not specified (or empty) all nodes in the cluster are used. 130 | run-on-role = "" 131 | 132 | # The Akka dispatcher to use for read-side actors and tasks. 133 | use-dispatcher = "lagom.persistence.dispatcher" 134 | } 135 | #//#persistence-read-side 136 | 137 | ###################################### 138 | # Message Broker (Kafka) Configuration 139 | ###################################### 140 | 141 | lagom.broker.kafka { 142 | # The name of the Kafka service to look up out of the service locator. 143 | # If this is an empty string, then a service locator lookup will not be done, 144 | # and the brokers configuration will be used instead. 145 | service-name = "kafka_native" 146 | service-name = ${?KAFKA_SERVICE_NAME} 147 | 148 | # The URLs of the Kafka brokers. Separate each URL with a comma. 149 | # This will be ignored if the service-name configuration is non empty. 150 | brokers = ${lagom.broker.defaults.kafka.brokers} 151 | 152 | client { 153 | default { 154 | # Exponential backoff for failures 155 | failure-exponential-backoff { 156 | # minimum (initial) duration until processor is started again 157 | # after failure 158 | min = 3s 159 | 160 | # the exponential back-off is capped to this duration 161 | max = 30s 162 | 163 | # additional random delay is based on this factor 164 | random-factor = 0.2 165 | } 166 | } 167 | 168 | # configuration used by the Lagom Kafka producer 169 | producer = ${lagom.broker.kafka.client.default} 170 | producer.role = "" 171 | 172 | # configuration used by the Lagom Kafka consumer 173 | consumer { 174 | failure-exponential-backoff = ${lagom.broker.kafka.client.default.failure-exponential-backoff} 175 | 176 | # The number of offsets that will be buffered to allow the consumer flow to 177 | # do its own buffering. This should be set to a number that is at least as 178 | # large as the maximum amount of buffering that the consumer flow will do, 179 | # if the consumer buffer buffers more than this, the offset buffer will 180 | # backpressure and cause the stream to stop. 181 | offset-buffer = 100 182 | 183 | # Number of messages batched together by the consumer before the related messages' 184 | # offsets are committed to Kafka. 185 | # By increasing the batching-size you are trading speed with the risk of having 186 | # to re-process a larger number of messages if a failure occurs. 187 | # The value provided must be strictly greater than zero. 188 | batching-size = 20 189 | 190 | # Interval of time waited by the consumer before the currently batched messages' 191 | # offsets are committed to Kafka. 192 | # This parameter is useful to ensure that messages' offsets are always committed 193 | # within a fixed amount of time. 194 | # The value provided must be strictly greater than zero. 195 | batching-interval = 5 seconds 196 | } 197 | } 198 | } 199 | 200 | #Enable circuit breaker metrics 201 | lagom.spi.circuit-breaker-metrics-class = "cinnamon.lagom.CircuitBreakerInstrumentation" 202 | 203 | akka.discovery.method = akka.discovery.config 204 | 205 | # Ready health check returns 200 when cluster membership is in the following states. 206 | # Intended to be used to indicate this node is ready for user traffic so Up/WeaklyUp 207 | # Valid values: "Joining", "WeaklyUp", "Up", "Leaving", "Exiting", "Down", "Removed" 208 | akka.management.cluster.http.healthcheck.ready-states = ["Up"] 209 | 210 | #In case of unreachable nodes or network partition, Split Brain Resolver will 211 | #apply this strategy to repair the Akka cluster. 212 | #akka.cluster.downing-provider-class = "com.lightbend.akka.sbr.SplitBrainResolverProvider" 213 | #akka.cluster.split-brain-resolver.active-strategy=keep-majority 214 | 215 | # Necessary to ensure Lagom successfully exits the JVM on shutdown. 216 | lagom.cluster.exit-jvm-when-system-terminated = on 217 | 218 | 219 | jwt.token.auth.expirationInSeconds = 2592000 220 | jwt.token.refresh.expirationInSeconds = 25920000 221 | 222 | www{ 223 | base-url = "/Users/coreyauger/projects/tasktick/tasktick-pwa/www" 224 | } 225 | 226 | include "secrets.conf" -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/application.prod.conf: -------------------------------------------------------------------------------- 1 | include "application" 2 | 3 | http { 4 | address = ${?HTTP_BIND_ADDRESS} 5 | port = 9000 6 | } 7 | 8 | play.filters.hosts { 9 | # Requests that are not from one of these hosts will be rejected. 10 | allowed = [${?ALLOWED_HOST}] 11 | } 12 | 13 | cassandra.default { 14 | ## list the contact points here 15 | contact-points = [${?CASSANDRA_CONTACT_POINT}] 16 | ## override Lagom’s ServiceLocator-based ConfigSessionProvider 17 | session-provider = akka.persistence.cassandra.ConfigSessionProvider 18 | } 19 | 20 | cassandra-journal { 21 | contact-points = ${cassandra.default.contact-points} 22 | session-provider = ${cassandra.default.session-provider} 23 | } 24 | 25 | cassandra-snapshot-store { 26 | contact-points = ${cassandra.default.contact-points} 27 | session-provider = ${cassandra.default.session-provider} 28 | } 29 | 30 | lagom.persistence.read-side.cassandra { 31 | contact-points = ${cassandra.default.contact-points} 32 | session-provider = ${cassandra.default.session-provider} 33 | } 34 | 35 | 36 | lagom.broker.kafka { 37 | # If this is an empty string, then the Lagom service locator lookup will not be done, 38 | # and the brokers configuration will be used instead. 39 | service-name = "" 40 | 41 | # The URLs of the Kafka brokers. Separate each URL with a comma. 42 | # This will be ignored if the service-name configuration is non empty. 43 | brokers = ${?KAFKA_BROKERS_SERVICE_URL} 44 | 45 | } 46 | 47 | play.http.secret.key = ">woRLf28Px]=GBewvGhDLIY9RksTQx6@X?CnDCsm2rd]9aIPI6cnaHaIebLnzR?Q" 48 | 49 | #Akka Remote will also use the host ip for the bind-hostname. 50 | akka.remote.netty.tcp.hostname = ${?HOST_ADDRESS} 51 | 52 | #Bind Akka Http and Akka Management listening hosts (keeping default ports). 53 | akka.management.http.bind-hostname = ${?HTTP_BIND_ADDRESS} 54 | 55 | #Akka Management and Remote are given the host ip address for identification. 56 | akka.management.http.hostname = ${?HOST_ADDRESS} 57 | 58 | #MUST DISABLE STATIC SEED NODES to use Akka Cluster Bootstrap! 59 | akka.cluster.seed-nodes = [] 60 | 61 | akka.discovery { 62 | method = kubernetes-api 63 | kubernetes-api { 64 | pod-namespace = "default" 65 | pod-label-selector = "app=gateway" 66 | pod-port-name = "akka-mgmt-http" 67 | } 68 | 69 | } 70 | akka.management.cluster.bootstrap.contact-point-discovery.port-name = "akka-mgmt-http" 71 | 72 | #Wait until there are N contact points present before attempting initial cluster formation 73 | akka.management.cluster.bootstrap.contact-point-discovery.required-contact-point-nr = 1 74 | 75 | #Shutdown if we have not joined a cluster after one minute. 76 | akka.cluster.shutdown-after-unsuccessful-join-seed-nodes = 300s 77 | 78 | # Necessary to ensure Lagom successfully exits the JVM on shutdown. The second one should no longer be needed after Lagom 1.5.0 or later. 79 | akka.coordinated-shutdown.exit-jvm = on 80 | lagom.cluster.exit-jvm-when-system-terminated = on 81 | 82 | 83 | # TODO: this should be in a kubernetes "secrets" 84 | jwt.secret = "4jkdgf4JHF38/385kjghs#$#(-.gdgk4498Q(gjgh3/3jhgdf,.,24#%8953+'8GJA3gsjjd3598#%(/$.,-Kjg#%$#64jhgskghja" -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg0.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg1.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg2.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg3.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg4.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg5.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg6.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg7.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg8.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bg9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bg9.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bga.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bgb.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bgc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bgc.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bgd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bgd.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bge.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/bgf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/bgf.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/logo.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/imgs/typebus-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/gateway-impl/src/main/resources/www/imgs/typebus-logo.png -------------------------------------------------------------------------------- /gateway-impl/src/main/resources/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TaskTick 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /gateway-impl/src/main/scala/io/surfkit/gateway/impl/GatewayLoader.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.gateway.impl 2 | 3 | import com.lightbend.lagom.scaladsl.api.ServiceLocator 4 | import com.lightbend.lagom.scaladsl.api.ServiceLocator.NoServiceLocator 5 | import com.lightbend.lagom.scaladsl.persistence.cassandra.CassandraPersistenceComponents 6 | import com.lightbend.lagom.scaladsl.server._ 7 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 8 | import play.api.libs.ws.ahc.AhcWSComponents 9 | import io.surfkit.gateway.api.GatewayService 10 | import com.lightbend.lagom.scaladsl.broker.kafka.LagomKafkaComponents 11 | import com.lightbend.lagom.scaladsl.client.ConfigurationServiceLocatorComponents 12 | import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry 13 | import com.softwaremill.macwire._ 14 | import io.surfkit.projectmanager.api.ProjectManagerService 15 | 16 | class GatewayLoader extends LagomApplicationLoader { 17 | 18 | override def load(context: LagomApplicationContext): LagomApplication = 19 | new GatewayApplication(context) with ConfigurationServiceLocatorComponents 20 | //new GatewayApplication(context) { 21 | // override def serviceLocator: ServiceLocator = NoServiceLocator 22 | //} 23 | 24 | override def loadDevMode(context: LagomApplicationContext): LagomApplication = { 25 | println("\n\n**** IN DEV MODE *****\n\n") 26 | new GatewayApplication(context) with LagomDevModeComponents 27 | } 28 | 29 | override def describeService = Some(readDescriptor[GatewayService]) 30 | } 31 | 32 | abstract class GatewayApplication(context: LagomApplicationContext) 33 | extends LagomApplication(context) 34 | with CassandraPersistenceComponents 35 | with LagomKafkaComponents 36 | with AhcWSComponents { 37 | 38 | lazy val projectService = serviceClient.implement[ProjectManagerService] 39 | 40 | // Bind the service that this server provides 41 | override lazy val lagomServer: LagomServer = serverFor[GatewayService](wire[GatewayServiceImpl]) 42 | 43 | // Register the JSON serializer registry 44 | override lazy val jsonSerializerRegistry: JsonSerializerRegistry = UserSerializerRegistry 45 | 46 | // Register the gateway persistent entity 47 | persistentEntityRegistry.register(wire[UserEntity]) 48 | } 49 | -------------------------------------------------------------------------------- /gateway-impl/src/main/scala/io/surfkit/gateway/impl/UserEntity.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.gateway.impl 2 | 3 | import java.util.UUID 4 | import com.lightbend.lagom.scaladsl.persistence.{AggregateEvent, AggregateEventTag, PersistentEntity} 5 | import com.lightbend.lagom.scaladsl.persistence.PersistentEntity.ReplyType 6 | import com.lightbend.lagom.scaladsl.playjson.{JsonSerializer, JsonSerializerRegistry} 7 | import io.surfkit.gateway.api._ 8 | import io.surfkit.gateway.impl.util.{SecurePasswordHashing, Token} 9 | import play.api.libs.json.{Format, Json} 10 | 11 | import scala.collection.immutable.Seq 12 | 13 | 14 | class UserEntity extends PersistentEntity { 15 | 16 | override type Command = UserCommand[_] 17 | override type Event = UserEvent 18 | override type State = UserState 19 | 20 | /** 21 | * The initial state. This is used if there is no snapshotted state to be found. 22 | */ 23 | override def initialState: UserState = UserState(None, Seq.empty) 24 | 25 | /** 26 | * An entity can define different behaviours for different states, so the behaviour 27 | * is a function of the current state to a set of actions. 28 | */ 29 | override def behavior: Behavior = { 30 | case UserState(_, _) => Actions() 31 | 32 | // 33 | // JWT Ideneity Commands / Actions / Events.... 34 | /// 35 | .onCommand[CreateUser, GeneratedIdDone] { 36 | case (CreateUser(firstName, lastName, email, password), ctx, state) => 37 | state.user match { 38 | case None if email == entityId => 39 | val hashedPassword = SecurePasswordHashing.hashPassword(password) 40 | val userId = UUID.randomUUID() 41 | ctx.thenPersist( 42 | UserCreated( 43 | userId = userId, 44 | firstName = firstName, 45 | lastName = lastName, 46 | email = email, 47 | hashedPassword = hashedPassword 48 | ) 49 | ) { _ => 50 | ctx.reply(GeneratedIdDone(userId.toString)) 51 | } 52 | case _ => 53 | ctx.invalidCommand(s"Failed to create user where entityId is ${entityId}") 54 | ctx.done 55 | } 56 | } 57 | .onCommand[AddProjectRef, ProjectRefAdded] { 58 | case (AddProjectRef(ref), ctx, _) => 59 | ctx.thenPersist(ProjectRefAdded(ref))(ctx.reply) 60 | } 61 | .onReadOnlyCommand[GetUserState, UserStateDone] { 62 | case (GetUserState(), ctx, state) => 63 | state.user match { 64 | case None => 65 | ctx.invalidCommand(s"User registered with ${entityId} can't be found") 66 | case Some(user: User) => 67 | ctx.reply( 68 | UserStateDone( 69 | io.surfkit.gateway.api.User( 70 | id = user.id.toString, 71 | firstName = user.firstName, 72 | lastName = user.lastName, 73 | email = user.email, 74 | hashedPassword = user.hashedPassword 75 | ) 76 | ) 77 | ) 78 | } 79 | } 80 | .onReadOnlyCommand[GetUserProjects, ProjectRefList] { 81 | case (GetUserProjects(skip, take), ctx, state) => 82 | ctx.reply(ProjectRefList(state.projects)) 83 | } 84 | .onReadOnlyCommand[GetUserProject, ProjectRef] { 85 | case (GetUserProject(id), ctx, state) => 86 | state.projects.find(_.id == id) match{ 87 | case Some(ref) => ctx.reply(ref) 88 | case None => ctx.invalidCommand(s"Could not locate project ${id} for user ${entityId}") 89 | } 90 | } 91 | .onEvent { 92 | case (UserCreated(userId, firstName, lastName, email, hashedPassword), state) => 93 | state.copy(Some( 94 | User( 95 | id = userId, 96 | firstName = firstName, 97 | lastName = lastName, 98 | email = email, 99 | hashedPassword = hashedPassword 100 | )) 101 | ) 102 | case (ProjectRefAdded(ref), state) => 103 | state.copy(projects = state.projects :+ ref) 104 | } 105 | // 106 | // END : JWT Ideneity Commands / Actions / Events.... 107 | // 108 | } 109 | } 110 | 111 | /** 112 | * State 113 | */ 114 | 115 | case class UserState(user: Option[User], projects: Seq[ProjectRef]) 116 | object UserState { implicit val format: Format[UserState] = Json.format } 117 | 118 | case class User( 119 | id: UUID, 120 | firstName: String, 121 | lastName: String, 122 | email: String, 123 | hashedPassword: String 124 | ) 125 | object User { implicit val format: Format[User] = Json.format } 126 | 127 | 128 | /** 129 | * Events 130 | */ 131 | sealed trait UserEvent extends AggregateEvent[UserEvent] { 132 | def aggregateTag: AggregateEventTag[UserEvent] = UserEvent.Tag 133 | } 134 | object UserEvent { 135 | val Tag: AggregateEventTag[UserEvent] = AggregateEventTag[UserEvent] 136 | } 137 | 138 | case class ClientCreated(company: String) extends UserEvent 139 | object ClientCreated { implicit val format: Format[ClientCreated] = Json.format } 140 | 141 | case class UserCreated(userId: UUID, firstName: String, lastName: String, email: String, hashedPassword: String) extends UserEvent 142 | object UserCreated { implicit val format: Format[UserCreated] = Json.format } 143 | 144 | 145 | case class ProjectRefAdded(ref: ProjectRef) extends UserEvent 146 | object ProjectRefAdded { implicit val format: Format[ProjectRefAdded] = Json.format } 147 | 148 | /** 149 | * Commands 150 | */ 151 | sealed trait UserCommand[R] extends ReplyType[R] 152 | 153 | case class CreateUser( 154 | firstName: String, 155 | lastName: String, 156 | email: String, 157 | password: String 158 | ) extends UserCommand[GeneratedIdDone] 159 | object CreateUser { implicit val format: Format[CreateUser] = Json.format } 160 | 161 | case class GetUserState() extends UserCommand[UserStateDone] 162 | 163 | case class GetUserProjects(skip: Int, take: Int) extends UserCommand[ProjectRefList] 164 | object GetUserProjects { implicit val format: Format[GetUserProjects] = Json.format } 165 | 166 | case class AddProjectRef(ref: ProjectRef) extends UserCommand[ProjectRefAdded] 167 | object AddProjectRef { implicit val format: Format[AddProjectRef] = Json.format } 168 | 169 | case class GetUserProject(id: UUID) extends UserCommand[ProjectRef] 170 | object GetUserProject { implicit val format: Format[GetUserProject] = Json.format } 171 | 172 | 173 | /** 174 | * Serialization 175 | */ 176 | object UserSerializerRegistry extends JsonSerializerRegistry { 177 | override def serializers: Seq[JsonSerializer[_]] = Seq( 178 | JsonSerializer[GetUserProject], 179 | JsonSerializer[AddProjectRef], 180 | JsonSerializer[ProjectRefAdded], 181 | JsonSerializer[GetUserProjects], 182 | JsonSerializer[GeneratedIdDone], 183 | JsonSerializer[CreateUser], 184 | JsonSerializer[UserStateDone], 185 | JsonSerializer[ClientCreated], 186 | JsonSerializer[UserCreated], 187 | JsonSerializer[UserCreated], 188 | JsonSerializer[User], 189 | JsonSerializer[Token], 190 | JsonSerializer[UserState] 191 | ) 192 | } 193 | -------------------------------------------------------------------------------- /gateway-impl/src/main/scala/io/surfkit/gateway/impl/util/JwtTokenUtil.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.gateway.impl.util 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import io.surfkit.gateway.api.TokenContent 5 | import pdi.jwt.{JwtAlgorithm, JwtClaim, JwtJson} 6 | import play.api.libs.json.{Format, Json} 7 | 8 | object JwtTokenUtil { 9 | val secret = ConfigFactory.load().getString("jwt.secret") 10 | val authExpiration = ConfigFactory.load().getInt("jwt.token.auth.expirationInSeconds") 11 | val refreshExpiration = ConfigFactory.load().getInt("jwt.token.refresh.expirationInSeconds") 12 | val algorithm = JwtAlgorithm.HS512 13 | 14 | def generateTokens(content: TokenContent)(implicit format: Format[TokenContent]): Token = { 15 | val authClaim = JwtClaim(Json.toJson(content).toString()) 16 | .expiresIn(authExpiration) 17 | .issuedNow 18 | 19 | val refreshClaim = JwtClaim(Json.toJson(content.copy(isRefreshToken = true)).toString()) 20 | .expiresIn(refreshExpiration) 21 | .issuedNow 22 | 23 | val authToken = JwtJson.encode(authClaim, secret, algorithm) 24 | val refreshToken = JwtJson.encode(refreshClaim, secret, algorithm) 25 | 26 | Token( 27 | authToken = authToken, 28 | refreshToken = Some(refreshToken) 29 | ) 30 | } 31 | 32 | def generateAuthTokenOnly(content: TokenContent)(implicit format: Format[TokenContent]): Token = { 33 | val authClaim = JwtClaim(Json.toJson(content.copy(isRefreshToken = false)).toString()) 34 | .expiresIn(authExpiration) 35 | .issuedNow 36 | 37 | val authToken = JwtJson.encode(authClaim, secret, algorithm) 38 | 39 | Token( 40 | authToken = authToken, 41 | None 42 | ) 43 | } 44 | } 45 | 46 | case class Token(authToken: String, refreshToken: Option[String]) 47 | object Token { 48 | implicit val format: Format[Token] = Json.format 49 | } -------------------------------------------------------------------------------- /gateway-impl/src/main/scala/io/surfkit/gateway/impl/util/SecurePasswordHashing.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.gateway.impl.util 2 | 3 | import java.security._ 4 | 5 | import javax.crypto._ 6 | import javax.crypto.spec._ 7 | import java.util.Base64 8 | import java.nio.charset.StandardCharsets 9 | 10 | import scala.util.Try 11 | 12 | /** 13 | * Credits: Age Mooij 14 | * https://gist.github.com/agemooij 15 | */ 16 | object SecurePasswordHashing { 17 | private val RandomSource = new SecureRandom() 18 | private val HashPartSeparator = ":" 19 | private val DefaultNrOfPasswordHashIterations = 2000 20 | private val SizeOfPasswordSaltInBytes = 16 21 | private val SizeOfPasswordHashInBytes = 32 22 | 23 | // This Scala implementation of password hashing was inspired by: 24 | // https://crackstation.net/hashing-security.htm#javasourcecode 25 | def hashPassword(password: String): String = hashPassword(password, generateRandomBytes(SizeOfPasswordSaltInBytes)) 26 | def hashPassword(password: String, salt: Array[Byte]): String = hashPassword(password, salt, DefaultNrOfPasswordHashIterations) 27 | def hashPassword(password: String, salt: Array[Byte], nrOfIterations: Int): String = { 28 | val hash = pbkdf2(password, salt, nrOfIterations) 29 | //val salt64 = new String(toBase64UrlSafe(salt)) 30 | //val hash64 = new String(toBase64UrlSafe(hash)) 31 | val salt64 = Base64.getEncoder.encodeToString(salt) 32 | val hash64 = Base64.getEncoder.encodeToString(hash) 33 | 34 | s"${nrOfIterations}${HashPartSeparator}${hash64}${HashPartSeparator}${salt64}" 35 | } 36 | 37 | def validatePassword(password: String, hashedPassword: String): Boolean = { 38 | /** Compares two byte arrays in length-constant time to prevent timing attacks. */ 39 | def slowEquals(a: Array[Byte], b: Array[Byte]): Boolean = { 40 | var diff = a.length ^ b.length; 41 | for (i <- 0 until math.min(a.length, b.length)) diff |= a(i) ^ b(i) 42 | return diff == 0 43 | } 44 | 45 | val hashParts = hashedPassword.split(HashPartSeparator) 46 | 47 | if (hashParts.length != 3) return false 48 | if (!hashParts(0).forall(_.isDigit)) return false 49 | 50 | val nrOfIterations = hashParts(0).toInt // this will throw a NumberFormatException for non-Int numbers... 51 | //val hash = fromBase64UrlSafe(hashParts(1)) 52 | //val salt = fromBase64UrlSafe(hashParts(2)) 53 | val hash = Try(Base64.getDecoder.decode(hashParts(1))).toEither 54 | val salt = Try(Base64.getDecoder.decode(hashParts(2))).toEither 55 | 56 | if (hash.isLeft || salt.isLeft) return false 57 | if (hash.right.get.length == 0 || salt.right.get.length == 0) return false 58 | 59 | val calculatedHash = pbkdf2(password, salt.right.get, nrOfIterations) 60 | 61 | slowEquals(calculatedHash, hash.right.get) 62 | } 63 | 64 | private def pbkdf2(password: String, salt: Array[Byte], nrOfIterations: Int): Array[Byte] = { 65 | val keySpec = new PBEKeySpec(password.toCharArray(), salt, nrOfIterations, SizeOfPasswordHashInBytes * 8) 66 | val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") 67 | 68 | keyFactory.generateSecret(keySpec).getEncoded() 69 | } 70 | 71 | private def generateRandomBytes(length: Int): Array[Byte] = { 72 | val keyData = new Array[Byte](length) 73 | RandomSource.nextBytes(keyData) 74 | keyData 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /gateway-impl/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{ISO8601} %-5level %logger - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /gateway-impl/src/test/scala/io/surfkit/gateway/impl/GatewayServiceSpec.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.gateway.impl 2 | 3 | import java.util.UUID 4 | 5 | import com.lightbend.lagom.scaladsl.api.transport.{Forbidden, TransportException} 6 | import com.lightbend.lagom.scaladsl.persistence.PersistentEntity.InvalidCommandException 7 | import com.lightbend.lagom.scaladsl.server.LocalServiceLocator 8 | import com.lightbend.lagom.scaladsl.testkit.ServiceTest 9 | import org.scalatest.{AsyncWordSpec, BeforeAndAfterAll, Matchers} 10 | import io.surfkit.gateway.api._ 11 | import io.surfkit.projectmanager.api.CreateProject 12 | 13 | import scala.util.Try 14 | 15 | class GatewayServiceSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 16 | 17 | private val server = ServiceTest.startServer( 18 | ServiceTest.defaultSetup 19 | .withCassandra() 20 | ) { ctx => 21 | new GatewayApplication(ctx) with LocalServiceLocator 22 | } 23 | 24 | val client: GatewayService = server.serviceClient.implement[GatewayService] 25 | 26 | override protected def afterAll(): Unit = server.stop() 27 | 28 | val testUserEmail = "testing@example.com" 29 | val testUserPass = "ThisIs@TestPass" 30 | 31 | //def authedRequest 32 | 33 | "gateway service auth" should { 34 | 35 | "register a user" in { 36 | client.registerUser.invoke(RegisterUser( 37 | firstName = "fname", 38 | lastName = "lname", 39 | email = testUserEmail, 40 | password = testUserPass 41 | )).map { answer => 42 | assert( Try(UUID.fromString(answer.id)).isSuccess ) 43 | } 44 | } 45 | 46 | "login a user" in { 47 | for { 48 | answer <- client.loginUser.invoke(UserLogin(testUserEmail, testUserPass)) 49 | } yield { 50 | assert(!answer.authToken.isEmpty) 51 | } 52 | } 53 | 54 | "fail to login a user with wrong pass" in { 55 | (for { 56 | answer <- client.loginUser.invoke(UserLogin(testUserEmail, "ThisISWRONG!!!")) 57 | } yield { 58 | assert(!answer.authToken.isEmpty) 59 | }).recover{ 60 | case _:Forbidden => assert(true) 61 | case x => assert(false, s"Should have got another exception type instead of: ${x}") 62 | } 63 | } 64 | 65 | "fail to login a user that does not exist" in { 66 | (for { 67 | answer <- client.loginUser.invoke(UserLogin("not.registered@example.com", testUserPass)) 68 | } yield { 69 | assert(!answer.authToken.isEmpty) 70 | }).recover{ 71 | case _:InvalidCommandException => assert(true) 72 | case _:TransportException => assert(true) 73 | case x => assert(false, s"Should have got another exception type instead of: ${x}") 74 | } 75 | } 76 | 77 | "Get the identity from auth token"in { 78 | for { 79 | answer <- client.loginUser.invoke(UserLogin(testUserEmail, testUserPass)) 80 | user <- client.getUser.handleRequestHeader(x => 81 | x.withHeader("Authorization", s"Bearer ${answer.authToken}" ) 82 | ).invoke() 83 | } yield { 84 | println(s"user: ${user}") 85 | assert(user.user.email == testUserEmail) 86 | } 87 | } 88 | 89 | "Fail to get the identity from a bad auth token"in { 90 | (for { 91 | answer <- client.loginUser.invoke(UserLogin(testUserEmail, testUserPass)) 92 | user <- client.getUser.handleRequestHeader(x => 93 | x.withHeader("Authorization", s"Bearer ${answer.authToken}XXXX" ) 94 | ).invoke() 95 | } yield { 96 | println(s"user: ${user}") 97 | assert(user.user.email == testUserEmail) 98 | }).recover{ 99 | case _:Forbidden => assert(true) 100 | case x => assert(false, s"Should have got another exception type instead of: ${x}") 101 | } 102 | } 103 | 104 | "Allow an auth token to refresh"in { 105 | for { 106 | answer <- client.loginUser.invoke(UserLogin(testUserEmail, testUserPass)) 107 | newAuth <- client.refreshToken.handleRequestHeader(x => 108 | x.withHeader("Authorization", s"Bearer ${answer.refreshToken}" ) 109 | ).invoke() 110 | user <- client.getUser.handleRequestHeader(x => 111 | x.withHeader("Authorization", s"Bearer ${newAuth.authToken}" ) 112 | ).invoke() 113 | } yield { 114 | println(s"user: ${user}") 115 | assert(user.user.email == testUserEmail) 116 | } 117 | } 118 | 119 | } 120 | 121 | /* 122 | val testProjectName = "Test project" 123 | 124 | "gateway service projects" should { 125 | "Allow a user to create a project" in { 126 | (for { 127 | answer <- client.loginUser.invoke(UserLogin(testUserEmail, testUserPass)) 128 | user <- client.getIdentityState.handleRequestHeader(x => 129 | x.withHeader("Authorization", s"Bearer ${answer.authToken}" ) 130 | ).invoke() 131 | project <- client.createProject.handleRequestHeader(x => 132 | x.withHeader("Authorization", s"Bearer ${answer.authToken}" ) 133 | ).invoke(CreateProject( 134 | name = testProjectName, 135 | owner = UUID.fromString(user.user.id), 136 | team = UUID.randomUUID(), 137 | description = "testing", 138 | imageUrl = None 139 | )) 140 | } yield { 141 | println(s"project: ${project}") 142 | assert(project.name == testProjectName) 143 | }) 144 | } 145 | }*/ 146 | 147 | } 148 | -------------------------------------------------------------------------------- /gateway-impl/src/test/scala/io/surfkit/gateway/impl/UserEntitySpec.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.gateway.impl 2 | 3 | import java.util.UUID 4 | 5 | import akka.actor.ActorSystem 6 | import akka.testkit.TestKit 7 | import com.lightbend.lagom.scaladsl.persistence.PersistentEntity.InvalidCommandException 8 | import com.lightbend.lagom.scaladsl.testkit.PersistentEntityTestDriver 9 | import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry 10 | import io.surfkit.gateway.api.{GeneratedIdDone, UserStateDone} 11 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} 12 | 13 | import scala.util.Try 14 | 15 | class UserEntitySpec extends WordSpec with Matchers with BeforeAndAfterAll { 16 | 17 | private val system = ActorSystem("UserEntitySpec", 18 | JsonSerializerRegistry.actorSystemSetupFor(UserSerializerRegistry)) 19 | 20 | override protected def afterAll(): Unit = { 21 | TestKit.shutdownActorSystem(system) 22 | } 23 | val testEmail = "test.user@example.com" 24 | val testPass = "Thi5IsMyP@ss" 25 | 26 | /*private def withTestDriver(block: PersistentEntityTestDriver[UserCommand[_], UserEvent, UserState] => Unit): Unit = { 27 | val driver = new PersistentEntityTestDriver(system, new UserEntity, testEmail) 28 | block(driver) 29 | driver.getAllIssues should have size 0 30 | }*/ 31 | 32 | val driver = new PersistentEntityTestDriver(system, new UserEntity, testEmail) 33 | 34 | 35 | "user entity" should { 36 | 37 | "create a user" in { 38 | val outcome = driver.run(CreateUser( 39 | firstName = "fname", 40 | lastName = "lname", 41 | email = testEmail, 42 | password = testPass 43 | )) 44 | val entityId = outcome.replies.head.asInstanceOf[GeneratedIdDone] 45 | driver.getAllIssues should have size 0 46 | assert( Try(UUID.fromString(entityId.id)).isSuccess ) 47 | } 48 | 49 | "get a user" in { 50 | val outcome = driver.run(GetUserState()) 51 | val us = outcome.replies.head.asInstanceOf[UserStateDone] 52 | assert(us.user.email == testEmail && us.user.firstName == "fname" && us.user.lastName == "lname") 53 | } 54 | 55 | "fail to create another user" in { 56 | val outcome = driver.run(CreateUser( 57 | firstName = "xxx", 58 | lastName = "yyy", 59 | email = "some.other@example.com", 60 | password = testPass 61 | )) 62 | //println(s"outcome: ${outcome}") 63 | driver.getAllIssues should have size 1 64 | assert(outcome.replies.head.isInstanceOf[InvalidCommandException]) 65 | } 66 | 67 | "fail to find a user that does not exist" in { 68 | val driver = new PersistentEntityTestDriver(system, new UserEntity, "test@test.com") 69 | val outcome = driver.run(GetUserState()) 70 | //println(s"outcome: ${outcome}") 71 | driver.getAllIssues should have size 1 72 | assert(outcome.replies.head.isInstanceOf[InvalidCommandException]) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // The Lagom plugin 2 | addSbtPlugin("com.lightbend.lagom" % "lagom-sbt-plugin" % "1.4.11") 3 | // Needed for importing the project into Eclipse 4 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4") 5 | -------------------------------------------------------------------------------- /projectmanager-api/src/main/scala/io/surfkit/projectmanager/api/ProjectManagerService.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.projectmanager.api 2 | 3 | import java.time.Instant 4 | 5 | import akka.{Done, NotUsed} 6 | import com.lightbend.lagom.scaladsl.api.broker.Topic 7 | import com.lightbend.lagom.scaladsl.api.broker.kafka.{KafkaProperties, PartitionKeyStrategy} 8 | import com.lightbend.lagom.scaladsl.api.{Service, ServiceCall} 9 | import play.api.libs.json.{Format, Json, OFormat} 10 | import java.util.UUID 11 | 12 | import com.lightbend.lagom.scaladsl.api.transport.Method 13 | 14 | object ServiceManagerService { 15 | val TOPIC_NAME = "greetings" 16 | } 17 | 18 | /** 19 | * The ServiceManager service interface. 20 | *

21 | * This describes everything that Lagom needs to know about how to serve and 22 | * consume the ServiceManagerService. 23 | */ 24 | trait ProjectManagerService extends Service { 25 | 26 | def getProject(id: UUID): ServiceCall[NotUsed, Project] 27 | def createProject: ServiceCall[CreateProject, Project] 28 | def updateProject: ServiceCall[Project, Project] 29 | def addTask(project: UUID): ServiceCall[AddTask, Task] 30 | def updateTask(project: UUID): ServiceCall[UpdateTask, Task] 31 | def deleteTask(project: UUID, task: UUID): ServiceCall[NotUsed, Done] 32 | def addTaskNote(project: UUID, task: UUID): ServiceCall[AddNote, Note] 33 | def deleteTaskNote(project: UUID, task: UUID, note: UUID): ServiceCall[NotUsed, Done] 34 | 35 | 36 | /** 37 | * This gets published to Kafka. 38 | */ 39 | def projectsTopic(): Topic[PublishEvents] 40 | 41 | override final def descriptor = { 42 | import Service._ 43 | // @formatter:off 44 | named("projects") 45 | .withCalls( 46 | restCall(Method.GET, "/api/project/:id", getProject _), 47 | restCall(Method.PUT, "/api/project/add", createProject _), 48 | restCall(Method.POST, "/api/project/add", updateProject _), 49 | restCall(Method.PUT, "/api/project/:project/task", addTask _), 50 | restCall(Method.POST, "/api/project/:project/task", updateTask _), 51 | restCall(Method.DELETE, "/api/project/:project/task/:task", deleteTask _), 52 | restCall(Method.PUT, "/api/project/:project/task/:task/note", addTaskNote _), 53 | restCall(Method.DELETE, "/api/project/:project/task/:task/note/:note", deleteTaskNote _) 54 | ) 55 | .withTopics( 56 | topic(ServiceManagerService.TOPIC_NAME, projectsTopic) 57 | // Kafka partitions messages, messages within the same partition will 58 | // be delivered in order, to ensure that all messages for the same user 59 | // go to the same partition (and hence are delivered in order with respect 60 | // to that user), we configure a partition key strategy that extracts the 61 | // name as the partition key. 62 | .addProperty( 63 | KafkaProperties.partitionKeyStrategy, 64 | PartitionKeyStrategy[PublishEvents](_.id.toString) 65 | ) 66 | ) 67 | .withAutoAcl(true) 68 | // @formatter:on 69 | } 70 | } 71 | 72 | case class CreateProject(name: String, owner: UUID, team: UUID, description: String, imageUrl: Option[String] = None) 73 | object CreateProject{ implicit val format: Format[CreateProject] = Json.format[CreateProject] } 74 | 75 | case class UpdateProject(project: Project) 76 | object UpdateProject{implicit val format: Format[UpdateProject] = Json.format[UpdateProject]} 77 | 78 | case class AddTask(project: UUID, name: String, description: String, section: String, parent: Option[UUID] = None) 79 | object AddTask{implicit val format: Format[AddTask] = Json.format[AddTask] } 80 | 81 | case class UpdateTask(project: UUID, task: Task) 82 | object UpdateTask{implicit val format: Format[UpdateTask] = Json.format[UpdateTask]} 83 | 84 | case class AddNote(project: UUID, task: UUID, user: UUID, note: String) 85 | object AddNote{implicit val format: Format[AddNote] = Json.format[AddNote] } 86 | 87 | case class Task( 88 | id: UUID, 89 | project: UUID, 90 | name: String, 91 | description: String, 92 | done: Boolean, 93 | assigned: Option[UUID], 94 | startDate: Option[Instant], 95 | endDate: Option[Instant], 96 | lastUpdated: Instant, 97 | section: String, 98 | parent: Option[UUID] = None, 99 | notes: Seq[Note] = Seq.empty[Note] 100 | ) 101 | object Task {implicit val format: Format[Task] = Json.format } 102 | 103 | case class Note(id: UUID, task: UUID, user: UUID, note: String, date: Instant) 104 | object Note {implicit val format: Format[Note] = Json.format} 105 | 106 | case class Project(id: UUID, 107 | name: String, 108 | owner: UUID, 109 | team: UUID, 110 | description: String, 111 | imgUrl: Option[String], 112 | tasks: Set[Task]) 113 | object Project {implicit val format: Format[Project] = Json.format[Project]} 114 | 115 | // Published Events... 116 | 117 | sealed trait PublishEvents{ 118 | def id: UUID 119 | } 120 | object PublishEvents{ implicit val format: OFormat[PublishEvents] = Json.format[PublishEvents] } 121 | 122 | case class ProjectUpdated(id: UUID, project: Project) extends PublishEvents 123 | object ProjectUpdated { implicit val format: Format[ProjectUpdated] = Json.format[ProjectUpdated] } 124 | 125 | case class ProjectCreated(id: UUID, project: Project) extends PublishEvents 126 | object ProjectCreated { implicit val format: Format[ProjectCreated] = Json.format[ProjectCreated] } 127 | 128 | case class ProjectDeleted(id: UUID) extends PublishEvents 129 | object ProjectDeleted { implicit val format: Format[ProjectDeleted] = Json.format[ProjectDeleted] } 130 | 131 | case class TaskUpdated(id: UUID, task: Task) extends PublishEvents 132 | object TaskUpdated { implicit val format: Format[TaskUpdated] = Json.format[TaskUpdated] } 133 | 134 | case class TaskCreated(id: UUID, task: Task) extends PublishEvents 135 | object TaskCreated { implicit val format: Format[TaskCreated] = Json.format[TaskCreated] } 136 | 137 | case class TaskDeleted(id: UUID) extends PublishEvents 138 | object TaskDeleted { implicit val format: Format[TaskDeleted] = Json.format[TaskDeleted] } 139 | 140 | case class NoteAdded(id: UUID, note: Note) extends PublishEvents 141 | object NoteAdded { implicit val format: Format[NoteAdded] = Json.format[NoteAdded] } 142 | 143 | case class NoteDeleted(id: UUID) extends PublishEvents 144 | object NoteDeleted { implicit val format: Format[NoteDeleted] = Json.format[NoteDeleted] } -------------------------------------------------------------------------------- /projectmanager-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | play.application.loader = io.surfkit.projectmanager.impl.ProjectManagerLoader 4 | 5 | 6 | ###################################### 7 | # Persistence (Cassandra) Configuration 8 | ###################################### 9 | 10 | //Use same keyspace for journal, snapshot-store and read-side. 11 | //Each service should be in a unique keyspace. 12 | projectmanager.cassandra.keyspace = projectmanager 13 | 14 | //For simplicity, keyspaces and tables will be auto-created on startup. 15 | //This is often not desirable in production environments, thus these may be 16 | //set to false to require manual creation. In that case, if the keyspace or 17 | //tables do not exist, the service will log an error and fail to start. 18 | cassandra-journal{ 19 | keyspace = ${projectmanager.cassandra.keyspace} 20 | keyspace-autocreate = true 21 | tables-autocreate = true 22 | } 23 | cassandra-snapshot-store{ 24 | keyspace = ${projectmanager.cassandra.keyspace} 25 | keyspace-autocreate = true 26 | tables-autocreate = true 27 | } 28 | lagom.persistence.read-side.cassandra{ 29 | keyspace = ${projectmanager.cassandra.keyspace} 30 | keyspace-autocreate = true 31 | } 32 | 33 | 34 | # The properties below override Lagom default configuration with the recommended values for new projects. 35 | # 36 | # Lagom has not yet made these settings the defaults for backward-compatibility reasons. 37 | 38 | # Prefer 'ddata' over 'persistence' to share cluster sharding state for new projects. 39 | # See https://doc.akka.io/docs/akka/current/cluster-sharding.html#distributed-data-vs-persistence-mode 40 | akka.cluster.sharding.state-store-mode = ddata 41 | 42 | # Enable the serializer provided in Akka 2.5.8+ for akka.Done and other internal 43 | # messages to avoid the use of Java serialization. 44 | akka.actor.serialization-bindings { 45 | "akka.Done" = akka-misc 46 | "akka.actor.Address" = akka-misc 47 | "akka.remote.UniqueAddress" = akka-misc 48 | } 49 | 50 | # https://discuss.lightbend.com/t/no-configuration-setting-found-for-key-decode-max-size/2738/3 51 | akka.http.routing.decode-max-size = 8m 52 | 53 | cassandra-query-journal.refresh-interval = 1s 54 | 55 | lagom.persistence { 56 | 57 | # As a rule of thumb, the number of shards should be a factor ten greater 58 | # than the planned maximum number of cluster nodes. Less shards than number 59 | # of nodes will result in that some nodes will not host any shards. Too many 60 | # shards will result in less efficient management of the shards, e.g. 61 | # rebalancing overhead, and increased latency because the coordinator is 62 | # involved in the routing of the first message for each shard. The value 63 | # must be the same on all nodes in a running cluster. It can be changed 64 | # after stopping all nodes in the cluster. 65 | max-number-of-shards = 100 66 | 67 | # Persistent entities saves snapshots after this number of persistent 68 | # events. Snapshots are used to reduce recovery times. 69 | # It may be configured to "off" to disable snapshots. 70 | # Author note: snapshotting turned off 71 | snapshot-after = off 72 | 73 | # A persistent entity is passivated automatically if it does not receive 74 | # any messages during this timeout. Passivation is performed to reduce 75 | # memory consumption. Objects referenced by the entity can be garbage 76 | # collected after passivation. Next message will activate the entity 77 | # again, which will recover its state from persistent storage. Set to 0 78 | # to disable passivation - this should only be done when the number of 79 | # entities is bounded and their state, sharded across the cluster, will 80 | # fit in memory. 81 | # Author note: Set to one day - this may be a bit long for production. 82 | passivate-after-idle-timeout = 86400s 83 | 84 | # Specifies that entities run on cluster nodes with a specific role. 85 | # If the role is not specified (or empty) all nodes in the cluster are used. 86 | # The entities can still be accessed from other nodes. 87 | run-entities-on-role = "" 88 | 89 | # Default timeout for PersistentEntityRef.ask replies. 90 | # Author note: Made longer to support potentially slower Minikube environment 91 | ask-timeout = 60s 92 | 93 | dispatcher { 94 | type = Dispatcher 95 | executor = "thread-pool-executor" 96 | thread-pool-executor { 97 | fixed-pool-size = 16 98 | } 99 | throughput = 1 100 | } 101 | } 102 | 103 | lagom.persistence.read-side { 104 | 105 | # how long should we wait when retrieving the last known offset 106 | # LROK Author Note: Default is 5s, this has been made longer for the course. 107 | offset-timeout = 60s 108 | 109 | # Exponential backoff for failures in ReadSideProcessor 110 | failure-exponential-backoff { 111 | # minimum (initial) duration until processor is started again 112 | # after failure 113 | min = 3s 114 | 115 | # the exponential back-off is capped to this duration 116 | max = 30s 117 | 118 | # additional random delay is based on this factor 119 | random-factor = 0.2 120 | } 121 | 122 | # The amount of time that a node should wait for the global prepare callback to execute 123 | # LROK Author Note: Default is 20s, this has been made longer for the course. 124 | global-prepare-timeout = 60s 125 | 126 | # Specifies that the read side processors should run on cluster nodes with a specific role. 127 | # If the role is not specified (or empty) all nodes in the cluster are used. 128 | run-on-role = "" 129 | 130 | # The Akka dispatcher to use for read-side actors and tasks. 131 | use-dispatcher = "lagom.persistence.dispatcher" 132 | } 133 | #//#persistence-read-side 134 | 135 | ###################################### 136 | # Message Broker (Kafka) Configuration 137 | ###################################### 138 | 139 | lagom.broker.kafka { 140 | # The name of the Kafka service to look up out of the service locator. 141 | # If this is an empty string, then a service locator lookup will not be done, 142 | # and the brokers configuration will be used instead. 143 | service-name = "kafka_native" 144 | service-name = ${?KAFKA_SERVICE_NAME} 145 | 146 | # The URLs of the Kafka brokers. Separate each URL with a comma. 147 | # This will be ignored if the service-name configuration is non empty. 148 | brokers = ${lagom.broker.defaults.kafka.brokers} 149 | 150 | client { 151 | default { 152 | # Exponential backoff for failures 153 | failure-exponential-backoff { 154 | # minimum (initial) duration until processor is started again 155 | # after failure 156 | min = 3s 157 | 158 | # the exponential back-off is capped to this duration 159 | max = 30s 160 | 161 | # additional random delay is based on this factor 162 | random-factor = 0.2 163 | } 164 | } 165 | 166 | # configuration used by the Lagom Kafka producer 167 | producer = ${lagom.broker.kafka.client.default} 168 | producer.role = "" 169 | 170 | # configuration used by the Lagom Kafka consumer 171 | consumer { 172 | failure-exponential-backoff = ${lagom.broker.kafka.client.default.failure-exponential-backoff} 173 | 174 | # The number of offsets that will be buffered to allow the consumer flow to 175 | # do its own buffering. This should be set to a number that is at least as 176 | # large as the maximum amount of buffering that the consumer flow will do, 177 | # if the consumer buffer buffers more than this, the offset buffer will 178 | # backpressure and cause the stream to stop. 179 | offset-buffer = 100 180 | 181 | # Number of messages batched together by the consumer before the related messages' 182 | # offsets are committed to Kafka. 183 | # By increasing the batching-size you are trading speed with the risk of having 184 | # to re-process a larger number of messages if a failure occurs. 185 | # The value provided must be strictly greater than zero. 186 | batching-size = 20 187 | 188 | # Interval of time waited by the consumer before the currently batched messages' 189 | # offsets are committed to Kafka. 190 | # This parameter is useful to ensure that messages' offsets are always committed 191 | # within a fixed amount of time. 192 | # The value provided must be strictly greater than zero. 193 | batching-interval = 5 seconds 194 | } 195 | } 196 | } 197 | 198 | #Enable circuit breaker metrics 199 | lagom.spi.circuit-breaker-metrics-class = "cinnamon.lagom.CircuitBreakerInstrumentation" 200 | 201 | akka.discovery.method = akka.discovery.config 202 | 203 | # Ready health check returns 200 when cluster membership is in the following states. 204 | # Intended to be used to indicate this node is ready for user traffic so Up/WeaklyUp 205 | # Valid values: "Joining", "WeaklyUp", "Up", "Leaving", "Exiting", "Down", "Removed" 206 | akka.management.cluster.http.healthcheck.ready-states = ["Up"] 207 | 208 | #In case of unreachable nodes or network partition, Split Brain Resolver will 209 | #apply this strategy to repair the Akka cluster. 210 | #akka.cluster.downing-provider-class = "com.lightbend.akka.sbr.SplitBrainResolverProvider" 211 | #akka.cluster.split-brain-resolver.active-strategy=keep-majority 212 | 213 | # Necessary to ensure Lagom successfully exits the JVM on shutdown. 214 | lagom.cluster.exit-jvm-when-system-terminated = on 215 | -------------------------------------------------------------------------------- /projectmanager-impl/src/main/resources/application.prod.conf: -------------------------------------------------------------------------------- 1 | include "application" 2 | 3 | http { 4 | address = ${?HTTP_BIND_ADDRESS} 5 | port = 9000 6 | } 7 | 8 | play.filters.hosts { 9 | # Requests that are not from one of these hosts will be rejected. 10 | allowed = [${?ALLOWED_HOST}] 11 | } 12 | 13 | cassandra.default { 14 | ## list the contact points here 15 | contact-points = [${?CASSANDRA_CONTACT_POINT}] 16 | ## override Lagom’s ServiceLocator-based ConfigSessionProvider 17 | session-provider = akka.persistence.cassandra.ConfigSessionProvider 18 | } 19 | 20 | cassandra-journal { 21 | contact-points = ${cassandra.default.contact-points} 22 | session-provider = ${cassandra.default.session-provider} 23 | } 24 | 25 | cassandra-snapshot-store { 26 | contact-points = ${cassandra.default.contact-points} 27 | session-provider = ${cassandra.default.session-provider} 28 | } 29 | 30 | lagom.persistence.read-side.cassandra { 31 | contact-points = ${cassandra.default.contact-points} 32 | session-provider = ${cassandra.default.session-provider} 33 | } 34 | 35 | 36 | lagom.broker.kafka { 37 | # If this is an empty string, then the Lagom service locator lookup will not be done, 38 | # and the brokers configuration will be used instead. 39 | service-name = "" 40 | 41 | # The URLs of the Kafka brokers. Separate each URL with a comma. 42 | # This will be ignored if the service-name configuration is non empty. 43 | brokers = ${?KAFKA_BROKERS_SERVICE_URL} 44 | 45 | } 46 | 47 | play.http.secret.key = ">woRLf28Px]=GBewvGhDLIY9RksTQx6@X?CnDCsm2rd]9aIPI6cnaHaIebLnzR?Q" 48 | 49 | #Akka Remote will also use the host ip for the bind-hostname. 50 | akka.remote.netty.tcp.hostname = ${?HOST_ADDRESS} 51 | 52 | #Bind Akka Http and Akka Management listening hosts (keeping default ports). 53 | akka.management.http.bind-hostname = ${?HTTP_BIND_ADDRESS} 54 | 55 | #Akka Management and Remote are given the host ip address for identification. 56 | akka.management.http.hostname = ${?HOST_ADDRESS} 57 | 58 | #MUST DISABLE STATIC SEED NODES to use Akka Cluster Bootstrap! 59 | akka.cluster.seed-nodes = [] 60 | 61 | akka.discovery { 62 | method = kubernetes-api 63 | kubernetes-api { 64 | pod-namespace = "default" 65 | pod-label-selector = "app=projectmanager" 66 | pod-port-name = "akka-mgmt-http" 67 | } 68 | 69 | } 70 | akka.management.cluster.bootstrap.contact-point-discovery.port-name = "akka-mgmt-http" 71 | 72 | #Wait until there are N contact points present before attempting initial cluster formation 73 | akka.management.cluster.bootstrap.contact-point-discovery.required-contact-point-nr = 1 74 | 75 | #Shutdown if we have not joined a cluster after one minute. 76 | akka.cluster.shutdown-after-unsuccessful-join-seed-nodes = 300s 77 | 78 | # Necessary to ensure Lagom successfully exits the JVM on shutdown. The second one should no longer be needed after Lagom 1.5.0 or later. 79 | akka.coordinated-shutdown.exit-jvm = on 80 | lagom.cluster.exit-jvm-when-system-terminated = on 81 | -------------------------------------------------------------------------------- /projectmanager-impl/src/main/scala/io/surfkit/projectmanager/impl/ProjectManagerLoader.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.projectmanager.impl 2 | 3 | import com.lightbend.lagom.scaladsl.api.ServiceLocator 4 | import com.lightbend.lagom.scaladsl.api.ServiceLocator.NoServiceLocator 5 | import com.lightbend.lagom.scaladsl.persistence.cassandra.CassandraPersistenceComponents 6 | import com.lightbend.lagom.scaladsl.server._ 7 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 8 | import play.api.libs.ws.ahc.AhcWSComponents 9 | import io.surfkit.projectmanager.api.ProjectManagerService 10 | import com.lightbend.lagom.scaladsl.broker.kafka.LagomKafkaComponents 11 | import com.lightbend.lagom.scaladsl.client.ConfigurationServiceLocatorComponents 12 | import com.softwaremill.macwire._ 13 | 14 | class ProjectManagerLoader extends LagomApplicationLoader { 15 | 16 | override def load(context: LagomApplicationContext): LagomApplication = 17 | new ServiceManagerApplication(context) with ConfigurationServiceLocatorComponents 18 | /*new ServiceManagerApplication(context) { 19 | override def serviceLocator: ServiceLocator = NoServiceLocator 20 | }*/ 21 | 22 | override def loadDevMode(context: LagomApplicationContext): LagomApplication = 23 | new ServiceManagerApplication(context) with LagomDevModeComponents 24 | 25 | override def describeService = Some(readDescriptor[ProjectManagerService]) 26 | } 27 | 28 | abstract class ServiceManagerApplication(context: LagomApplicationContext) 29 | extends LagomApplication(context) 30 | with CassandraPersistenceComponents 31 | with LagomKafkaComponents 32 | with AhcWSComponents { 33 | 34 | // Bind the service that this server provides 35 | override lazy val lagomServer = serverFor[ProjectManagerService](wire[ProjectManagerServiceImpl]) 36 | 37 | // Register the JSON serializer registry 38 | override lazy val jsonSerializerRegistry = ServiceManagerSerializerRegistry 39 | 40 | // Register the ServiceManager persistent entity 41 | persistentEntityRegistry.register(wire[ProjectEntity]) 42 | } 43 | -------------------------------------------------------------------------------- /projectmanager-impl/src/main/scala/io/surfkit/projectmanager/impl/ProjectManagerServiceImpl.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.projectmanager.impl 2 | 3 | import io.surfkit.projectmanager.api 4 | import io.surfkit.projectmanager.api.ProjectManagerService 5 | import com.lightbend.lagom.scaladsl.api.ServiceCall 6 | import com.lightbend.lagom.scaladsl.api.broker.Topic 7 | import com.lightbend.lagom.scaladsl.broker.TopicProducer 8 | 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | import com.lightbend.lagom.scaladsl.persistence.{EventStreamElement, PersistentEntityRegistry} 11 | import java.util.UUID 12 | 13 | import akka.actor.ActorSystem 14 | import akka.management.AkkaManagement 15 | import akka.management.cluster.bootstrap.ClusterBootstrap 16 | import akka.{Done, NotUsed} 17 | import com.lightbend.lagom.scaladsl.server.LagomApplicationContext 18 | import play.api.Mode 19 | /** 20 | * Implementation of the ServiceManagerService. 21 | */ 22 | class ProjectManagerServiceImpl(persistentEntityRegistry: PersistentEntityRegistry, system: ActorSystem, context: LagomApplicationContext) extends ProjectManagerService { 23 | 24 | // Akka Management hosts the HTTP routes for debugging 25 | AkkaManagement.get(system).start() 26 | if (context.playContext.environment.mode != Mode.Dev) { 27 | // Starting the bootstrap process in production 28 | ClusterBootstrap.get(system).start() 29 | } 30 | 31 | override def getProject(id: UUID) = ServiceCall { _ => 32 | val ref = persistentEntityRegistry.refFor[ProjectEntity](id.toString) 33 | ref.ask(GetProject(id)).map(convertProject) 34 | } 35 | 36 | override def createProject = ServiceCall{ req: api.CreateProject => 37 | val newProjectId = UUID.randomUUID 38 | val ref = persistentEntityRegistry.refFor[ProjectEntity](newProjectId.toString) 39 | ref.ask(CreateProject( 40 | name = req.name, 41 | owner = req.owner, 42 | team = req.team, 43 | description = req.description, 44 | imageUrl = req.imageUrl 45 | )).map(x => convertProject(x.project)) 46 | } 47 | 48 | override def updateProject = ServiceCall{ req => 49 | val ref = persistentEntityRegistry.refFor[ProjectEntity](req.id.toString) 50 | ref.ask(UpdateProject( 51 | name = req.name, 52 | owner = req.owner, 53 | team = req.team, 54 | description = req.description, 55 | imageUrl = req.imgUrl 56 | )).map(x => convertProject(x.project)) 57 | } 58 | 59 | override def addTask(project: UUID) = ServiceCall{ req => 60 | val ref = persistentEntityRegistry.refFor[ProjectEntity](project.toString) 61 | ref.ask(AddTask( 62 | name = req.name, 63 | description = req.description, 64 | section = req.section, 65 | parent = req.parent 66 | )).map(x => convertTask(x.task)) 67 | } 68 | 69 | override def updateTask(project: UUID) = ServiceCall{ req => 70 | val ref = persistentEntityRegistry.refFor[ProjectEntity](project.toString) 71 | ref.ask(UpdateTask( 72 | id = req.task.id, 73 | name = req.task.name, 74 | description = req.task.description, 75 | parent = req.task.parent, 76 | done = req.task.done, 77 | assigned = req.task.assigned, 78 | startDate = req.task.startDate, 79 | endDate = req.task.endDate, 80 | section = req.task.section 81 | )).map(x => convertTask(x.task)) 82 | } 83 | 84 | override def deleteTask(project: UUID, task: UUID) = ServiceCall{ _ => 85 | val ref = persistentEntityRegistry.refFor[ProjectEntity](project.toString) 86 | ref.ask(DeleteTask(task)).map(_ => Done) 87 | } 88 | 89 | override def addTaskNote(project: UUID, task: UUID) = ServiceCall{ req => 90 | val ref = persistentEntityRegistry.refFor[ProjectEntity](project.toString) 91 | ref.ask(AddNote(task, req.user, req.note)).map(x => convertNote(x.note)) 92 | } 93 | 94 | override def deleteTaskNote(project: UUID, task: UUID, note: UUID) = ServiceCall{ _ => 95 | val ref = persistentEntityRegistry.refFor[ProjectEntity](project.toString) 96 | ref.ask(DeleteNote(task, note)).map(_ => Done) 97 | } 98 | 99 | private def convertNote(n: Note):api.Note = api.Note( 100 | id = n.id, 101 | task = n.task, 102 | user = n.user, 103 | note = n.note, 104 | date = n.date 105 | ) 106 | 107 | private def convertTask(t: Task): api.Task = api.Task( 108 | id = t.id, 109 | name = t.name, 110 | project = t.project, 111 | description = t.description, 112 | done = t.done, 113 | assigned = t.assigned, 114 | startDate = t.startDate, 115 | endDate = t.endDate, 116 | lastUpdated = t.lastUpdated, 117 | section = t.section, 118 | parent = t.parent, 119 | notes = t.notes.map(convertNote) 120 | ) 121 | private def convertProject(p: Project): api.Project = api.Project( 122 | id = p.id, 123 | name = p.name, 124 | owner = p.owner, 125 | team = p.team, 126 | description = p.description, 127 | imgUrl = p.imgUrl, 128 | tasks = p.tasks.map(y => convertTask(y) ) 129 | ) 130 | 131 | 132 | override def projectsTopic(): Topic[api.PublishEvents] = 133 | TopicProducer.singleStreamWithOffset { 134 | fromOffset => 135 | persistentEntityRegistry.eventStream(ProjectEvent.Tag, fromOffset) 136 | .map(ev => (convertEvent(ev), ev.offset)) 137 | } 138 | 139 | private def convertEvent(ev: EventStreamElement[ProjectEvent]): api.PublishEvents = { 140 | ev.event match { 141 | case ProjectUpdated(p) => api.ProjectUpdated(p.id, convertProject(p)) 142 | case ProjectCreated(p) => api.ProjectCreated(p.id, convertProject(p)) 143 | case ProjectDeleted(id) => api.ProjectDeleted(id) 144 | case TaskUpdated(p) => api.TaskUpdated(p.id, convertTask(p)) 145 | case TaskAdded(p) => api.TaskCreated(p.id, convertTask(p)) 146 | case TaskDeleted(id) => api.TaskDeleted(id) 147 | case NoteAdded(id, n) => api.NoteAdded(id, convertNote(n)) 148 | case NoteDeleted(id, noteId) => api.NoteDeleted(noteId) 149 | } 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /projectmanager-impl/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{ISO8601} %-5level %logger - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /projectmanager-impl/src/test/scala/io/surfkit/projectmanager/impl/ProjectEntitySpec.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.projectmanager.impl 2 | 3 | import java.time.Instant 4 | import java.util.UUID 5 | 6 | import akka.actor.ActorSystem 7 | import akka.testkit.TestKit 8 | import com.lightbend.lagom.scaladsl.testkit.PersistentEntityTestDriver 9 | import com.lightbend.lagom.scaladsl.playjson.JsonSerializerRegistry 10 | import com.lightbend.lagom.scaladsl.testkit.PersistentEntityTestDriver.Reply 11 | import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} 12 | 13 | class ProjectEntitySpec extends WordSpec with Matchers with BeforeAndAfterAll { 14 | 15 | private val system = ActorSystem("ServiceManagerEntitySpec", 16 | JsonSerializerRegistry.actorSystemSetupFor(ServiceManagerSerializerRegistry)) 17 | 18 | override protected def afterAll(): Unit = { 19 | TestKit.shutdownActorSystem(system) 20 | } 21 | 22 | import ProjectEntitySpec._ 23 | 24 | val testDriver = new PersistentEntityTestDriver(system, new ProjectEntity, entityId.toString) 25 | 26 | val taskTestDriver = new PersistentEntityTestDriver(system, new ProjectEntity, entityId2.toString) 27 | 28 | 29 | private def withTestDriver(block: PersistentEntityTestDriver[ProjectCommand[_], ProjectEvent, ProjectState] => Unit): Unit = { 30 | val driver = new PersistentEntityTestDriver(system, new ProjectEntity, entityId.toString) 31 | block(driver) 32 | driver.getAllIssues should have size 0 33 | } 34 | 35 | private def withTestDriver2(block: PersistentEntityTestDriver[ProjectCommand[_], ProjectEvent, ProjectState] => Unit): Unit = { 36 | val driver = new PersistentEntityTestDriver(system, new ProjectEntity, entityId2.toString) 37 | block(driver) 38 | driver.getAllIssues should have size 0 39 | } 40 | 41 | "Project entity" should { 42 | 43 | "should not create a project with no name" in { 44 | val outcome = testDriver.run(CreateProject( 45 | name = "", 46 | owner = tp.owner, 47 | team = tp.team, 48 | description = tp.description, 49 | imageUrl = tp.imgUrl 50 | )) 51 | outcome.sideEffects.head match{ 52 | case Reply(_: RuntimeException) => assert(true) 53 | case _ => assert(false, "Should return error") 54 | } 55 | testDriver.getAllIssues should have size 1 56 | } 57 | 58 | "create a new project" in { 59 | val outcome = testDriver.run(CreateProject( 60 | name = tp.name, 61 | owner = tp.owner, 62 | team = tp.team, 63 | description = tp.description, 64 | imageUrl = tp.imgUrl 65 | )) 66 | val ev = outcome.events.head.asInstanceOf[ProjectCreated] 67 | assert(ev.project == tp) 68 | } 69 | 70 | "update a project" in { 71 | val outcome = testDriver.run(UpdateProject( 72 | name = tpUpdate.name, 73 | owner = tpUpdate.owner, 74 | team = tpUpdate.team, 75 | description = tpUpdate.description, 76 | imageUrl = tpUpdate.imgUrl 77 | )) 78 | val ev = outcome.events.head.asInstanceOf[ProjectUpdated] 79 | assert(ev.project == tpUpdate) 80 | } 81 | 82 | "get a project" in { 83 | val outcome = testDriver.run(GetProject(entityId)) 84 | val proj = outcome.replies.head.asInstanceOf[Project] 85 | assert(proj == tpUpdate) 86 | } 87 | } 88 | 89 | 90 | "Project Entity " should { 91 | "Create a new Project" in{ 92 | val outcome = taskTestDriver.run(CreateProject( 93 | name = tp2.name, 94 | owner = tp2.owner, 95 | team = tp2.team, 96 | description = tp2.description, 97 | imageUrl = tp2.imgUrl 98 | )) 99 | val ev = outcome.events.head.asInstanceOf[ProjectCreated] 100 | assert(ev.project == tp2) 101 | } 102 | 103 | "Add a Task to the project" in{ 104 | val outcome = taskTestDriver.run(AddTask(name = task1.name, description = task1.description, section = task1.section)) 105 | val ev = outcome.events.head.asInstanceOf[TaskAdded] 106 | assert(ev.task.name == task1.name && ev.task.section == task1.section) 107 | } 108 | 109 | "Update the task info" in{ 110 | val outcome = taskTestDriver.run(GetProject(tp2.id)) 111 | val proj = outcome.replies.head.asInstanceOf[Project] 112 | assert(proj.id == tp2.id) 113 | assert(proj.tasks.size == 1) 114 | val task = task1.copy(id = proj.tasks.head.id) 115 | val outcome2 = taskTestDriver.run(UpdateTask( 116 | id = task.id, 117 | name = task.name, 118 | description = task.description, 119 | done = task.done, 120 | assigned = task.assigned, 121 | startDate = task.startDate, 122 | parent = task.parent, 123 | endDate = task.endDate, 124 | section = task.section 125 | )) 126 | val ev = outcome2.events.head.asInstanceOf[TaskUpdated] 127 | assert(ev.task.name == task.name && task.description == task.description) 128 | } 129 | 130 | "Adding another Task to the project" in{ 131 | val outcome = taskTestDriver.run(AddTask(name = task1.name, description = task1.description, section = task1.section)) 132 | val ev = outcome.events.head.asInstanceOf[TaskAdded] 133 | assert(ev.task.name == task1.name && ev.task.section == task1.section) 134 | val outcome2 = taskTestDriver.run(GetProject(tp2.id)) 135 | val proj = outcome2.replies.head.asInstanceOf[Project] 136 | assert(proj.id == tp2.id) 137 | assert(proj.tasks.size == 2) 138 | } 139 | 140 | "Delete Task to the project" in{ 141 | val outcome = taskTestDriver.run(GetProject(tp2.id)) 142 | val proj = outcome.replies.head.asInstanceOf[Project] 143 | assert(proj.id == tp2.id) 144 | assert(proj.tasks.size == 2) 145 | val taskToDel = proj.tasks.head 146 | val outcome2 = taskTestDriver.run(DeleteTask(taskToDel.id)) 147 | val outcome3 = taskTestDriver.run(GetProject(tp2.id)) 148 | val proj2 = outcome3.replies.head.asInstanceOf[Project] 149 | assert(proj2.id == tp2.id) 150 | assert(proj2.tasks.size == 1) 151 | } 152 | 153 | "Add a note to a task" in { 154 | val outcome = taskTestDriver.run(GetProject(tp2.id)) 155 | val proj = outcome.replies.head.asInstanceOf[Project] 156 | assert(proj.id == tp2.id) 157 | assert(proj.tasks.size == 1) 158 | val task = proj.tasks.head 159 | val noteTxt = "Testing Note Text" 160 | taskTestDriver.run(AddNote(task.id, proj.owner, noteTxt)) 161 | taskTestDriver.run(AddNote(task.id, proj.owner, noteTxt)) 162 | val outcome2 = taskTestDriver.run(GetProject(tp2.id)) 163 | val proj2 = outcome2.replies.head.asInstanceOf[Project] 164 | assert(proj2.id == tp2.id) 165 | assert(proj2.tasks.size == 1) 166 | val task2 = proj2.tasks.head 167 | assert(task2.notes.size == 2) 168 | assert(task2.notes.forall(_.note == noteTxt)) 169 | } 170 | 171 | "Delete a note" in { 172 | val outcome = taskTestDriver.run(GetProject(tp2.id)) 173 | val proj = outcome.replies.head.asInstanceOf[Project] 174 | assert(proj.id == tp2.id) 175 | assert(proj.tasks.size == 1) 176 | val task = proj.tasks.head 177 | val note = task.notes.head 178 | taskTestDriver.run(DeleteNote(task.id, note.id)) 179 | val outcome2 = taskTestDriver.run(GetProject(tp2.id)) 180 | val proj2 = outcome2.replies.head.asInstanceOf[Project] 181 | assert(proj2.id == tp2.id) 182 | assert(proj2.tasks.size == 1) 183 | val task2 = proj2.tasks.head 184 | assert(task2.notes.size == 1) 185 | val noteTxt = "Testing Note Text" 186 | assert(task2.notes.forall(_.note == noteTxt)) 187 | } 188 | 189 | "Adding another Task as a subtask" in{ 190 | val outcome = taskTestDriver.run(GetProject(tp2.id)) 191 | val proj = outcome.replies.head.asInstanceOf[Project] 192 | assert(proj.id == tp2.id) 193 | assert(proj.tasks.size == 1) 194 | val task = proj.tasks.head 195 | val outcome2 = taskTestDriver.run(AddTask(name = task1.name, description = task1.description, section = task1.section, parent = Some(task1.id))) 196 | val ev2 = outcome2.events.head.asInstanceOf[TaskAdded] 197 | assert(ev2.task.name == task1.name && ev2.task.section == task1.section && ev2.task.parent == Some(task1.id)) 198 | val outcome3 = taskTestDriver.run(GetProject(tp2.id)) 199 | val proj2 = outcome3.replies.head.asInstanceOf[Project] 200 | assert(proj2.id == tp2.id) 201 | assert(proj2.tasks.size == 2) 202 | } 203 | } 204 | 205 | } 206 | 207 | object ProjectEntitySpec{ 208 | val entityId = UUID.randomUUID() 209 | val entityId2 = UUID.randomUUID() 210 | 211 | val testUserId = UUID.randomUUID() 212 | val testTeamId = UUID.randomUUID() 213 | 214 | lazy val tp = Project( 215 | id = entityId, 216 | name = "Test Name", 217 | owner = testUserId, 218 | team = testTeamId, 219 | description = "This is a a description of the project", 220 | imgUrl = Some("http://some-image.com/image.png"), 221 | tasks = Set.empty 222 | ) 223 | 224 | val testUserId2 = UUID.randomUUID() 225 | val testTeamId2 = UUID.randomUUID() 226 | 227 | lazy val tpUpdate = Project( 228 | id = entityId, 229 | name = "Test Name2", 230 | owner = testUserId2, 231 | team = testTeamId2, 232 | description = "Updated description", 233 | imgUrl = Some("http://some-image.com/image2.png"), 234 | tasks = Set.empty 235 | ) 236 | 237 | lazy val tp2 = Project( 238 | id = entityId2, 239 | name = "Test Name2", 240 | owner = testUserId2, 241 | team = testTeamId2, 242 | description = "Updated description", 243 | imgUrl = Some("http://some-image.com/image2.png"), 244 | tasks = Set.empty 245 | ) 246 | 247 | lazy val task1 = Task( 248 | id = UUID.randomUUID(), 249 | project = UUID.randomUUID(), 250 | name = "Task 1", 251 | description = "Task Description", 252 | done = false, 253 | assigned = None, 254 | startDate = None, 255 | endDate = None, 256 | lastUpdated = Instant.now, 257 | section = "Section 1", 258 | ) 259 | 260 | } 261 | -------------------------------------------------------------------------------- /projectmanager-impl/src/test/scala/io/surfkit/projectmanager/impl/ProjectManagerServiceSpec.scala: -------------------------------------------------------------------------------- 1 | package io.surfkit.projectmanager.impl 2 | 3 | import java.util.UUID 4 | 5 | import com.lightbend.lagom.scaladsl.server.LocalServiceLocator 6 | import com.lightbend.lagom.scaladsl.testkit.ServiceTest 7 | import org.scalatest.{AsyncWordSpec, BeforeAndAfterAll, Matchers} 8 | import io.surfkit.projectmanager.api._ 9 | 10 | class ProjectManagerServiceSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll { 11 | 12 | private val server = ServiceTest.startServer( 13 | ServiceTest.defaultSetup 14 | .withCassandra() 15 | ) { ctx => 16 | new ServiceManagerApplication(ctx) with LocalServiceLocator 17 | } 18 | 19 | val client = server.serviceClient.implement[ServiceManagerService] 20 | 21 | override protected def afterAll() = server.stop() 22 | import ProjectEntitySpec._ 23 | 24 | var createdId = UUID.randomUUID() 25 | 26 | "ServiceManager service" should { 27 | 28 | "add a new project" in { 29 | client.createProject.invoke(io.surfkit.servicemanager.api.CreateProject( 30 | name = tp.name, 31 | owner = tp.owner, 32 | team = tp.team, 33 | description = tp.description, 34 | imageUrl = tp.imgUrl 35 | )).map{ answer => 36 | createdId = answer.id 37 | answer should === (tp.id == answer.id) 38 | } 39 | } 40 | 41 | "update a project with new values" in { 42 | client.updateProject.invoke(io.surfkit.servicemanager.api.Project( 43 | id = createdId, 44 | name = tp2.name, 45 | owner = tp2.owner, 46 | team = tp2.team, 47 | description = tp2.description, 48 | tasks = Set.empty, 49 | imgUrl = tp2.imgUrl 50 | )).map{ answer => 51 | answer should === (tp2.id == createdId) 52 | } 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ./sbt-dist/bin/sbt "$@" -------------------------------------------------------------------------------- /sbt-dist/bin/sbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### ------------------------------- ### 5 | ### Helper methods for BASH scripts ### 6 | ### ------------------------------- ### 7 | 8 | realpath () { 9 | ( 10 | TARGET_FILE="$1" 11 | FIX_CYGPATH="$2" 12 | 13 | cd "$(dirname "$TARGET_FILE")" 14 | TARGET_FILE=$(basename "$TARGET_FILE") 15 | 16 | COUNT=0 17 | while [ -L "$TARGET_FILE" -a $COUNT -lt 100 ] 18 | do 19 | TARGET_FILE=$(readlink "$TARGET_FILE") 20 | cd "$(dirname "$TARGET_FILE")" 21 | TARGET_FILE=$(basename "$TARGET_FILE") 22 | COUNT=$(($COUNT + 1)) 23 | done 24 | 25 | # make sure we grab the actual windows path, instead of cygwin's path. 26 | if [[ "x$FIX_CYGPATH" != "x" ]]; then 27 | echo "$(cygwinpath "$(pwd -P)/$TARGET_FILE")" 28 | else 29 | echo "$(pwd -P)/$TARGET_FILE" 30 | fi 31 | ) 32 | } 33 | 34 | 35 | # Uses uname to detect if we're in the odd cygwin environment. 36 | is_cygwin() { 37 | local os=$(uname -s) 38 | case "$os" in 39 | CYGWIN*) return 0 ;; 40 | *) return 1 ;; 41 | esac 42 | } 43 | 44 | # TODO - Use nicer bash-isms here. 45 | CYGWIN_FLAG=$(if is_cygwin; then echo true; else echo false; fi) 46 | 47 | 48 | # This can fix cygwin style /cygdrive paths so we get the 49 | # windows style paths. 50 | cygwinpath() { 51 | local file="$1" 52 | if [[ "$CYGWIN_FLAG" == "true" ]]; then 53 | echo $(cygpath -w $file) 54 | else 55 | echo $file 56 | fi 57 | } 58 | 59 | . "$(dirname "$(realpath "$0")")/sbt-launch-lib.bash" 60 | 61 | 62 | declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" 63 | declare -r sbt_opts_file=".sbtopts" 64 | declare -r etc_sbt_opts_file="${sbt_home}/conf/sbtopts" 65 | declare -r win_sbt_opts_file="${sbt_home}/conf/sbtconfig.txt" 66 | 67 | usage() { 68 | cat < path to global settings/plugins directory (default: ~/.sbt) 77 | -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11 series) 78 | -ivy path to local Ivy repository (default: ~/.ivy2) 79 | -mem set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem)) 80 | -no-share use all local caches; no sharing 81 | -no-global uses global caches, but does not use global ~/.sbt directory. 82 | -jvm-debug Turn on JVM debugging, open at the given port. 83 | -batch Disable interactive mode 84 | 85 | # sbt version (default: from project/build.properties if present, else latest release) 86 | -sbt-version use the specified version of sbt 87 | -sbt-jar use the specified jar as the sbt launcher 88 | -sbt-rc use an RC version of sbt 89 | -sbt-snapshot use a snapshot version of sbt 90 | 91 | # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) 92 | -java-home alternate JAVA_HOME 93 | 94 | # jvm options and output control 95 | JAVA_OPTS environment variable, if unset uses "$java_opts" 96 | SBT_OPTS environment variable, if unset uses "$default_sbt_opts" 97 | .sbtopts if this file exists in the current directory, it is 98 | prepended to the runner args 99 | /etc/sbt/sbtopts if this file exists, it is prepended to the runner args 100 | -Dkey=val pass -Dkey=val directly to the java runtime 101 | -J-X pass option -X directly to the java runtime 102 | (-J is stripped) 103 | -S-X add -X to sbt's scalacOptions (-S is stripped) 104 | 105 | In the case of duplicated or conflicting options, the order above 106 | shows precedence: JAVA_OPTS lowest, command line options highest. 107 | EOM 108 | } 109 | 110 | 111 | 112 | process_my_args () { 113 | while [[ $# -gt 0 ]]; do 114 | case "$1" in 115 | -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; 116 | -no-share) addJava "$noshare_opts" && shift ;; 117 | -no-global) addJava "-Dsbt.global.base=$(pwd)/project/.sbtboot" && shift ;; 118 | -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; 119 | -sbt-dir) require_arg path "$1" "$2" && addJava "-Dsbt.global.base=$2" && shift 2 ;; 120 | -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; 121 | -batch) exec &2 "$@" 20 | } 21 | vlog () { 22 | [[ $verbose || $debug ]] && echoerr "$@" 23 | } 24 | dlog () { 25 | [[ $debug ]] && echoerr "$@" 26 | } 27 | 28 | jar_file () { 29 | echo "$(cygwinpath "${sbt_home}/bin/sbt-launch.jar")" 30 | } 31 | 32 | acquire_sbt_jar () { 33 | sbt_jar="$(jar_file)" 34 | 35 | if [[ ! -f "$sbt_jar" ]]; then 36 | echoerr "Could not find launcher jar: $sbt_jar" 37 | exit 2 38 | fi 39 | } 40 | 41 | execRunner () { 42 | # print the arguments one to a line, quoting any containing spaces 43 | [[ $verbose || $debug ]] && echo "# Executing command line:" && { 44 | for arg; do 45 | if printf "%s\n" "$arg" | grep -q ' '; then 46 | printf "\"%s\"\n" "$arg" 47 | else 48 | printf "%s\n" "$arg" 49 | fi 50 | done 51 | echo "" 52 | } 53 | 54 | # THis used to be exec, but we loose the ability to re-hook stty then 55 | # for cygwin... Maybe we should flag the feature here... 56 | "$@" 57 | } 58 | 59 | addJava () { 60 | dlog "[addJava] arg = '$1'" 61 | java_args=( "${java_args[@]}" "$1" ) 62 | } 63 | addSbt () { 64 | dlog "[addSbt] arg = '$1'" 65 | sbt_commands=( "${sbt_commands[@]}" "$1" ) 66 | } 67 | addResidual () { 68 | dlog "[residual] arg = '$1'" 69 | residual_args=( "${residual_args[@]}" "$1" ) 70 | } 71 | addDebugger () { 72 | addJava "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$1" 73 | } 74 | 75 | get_mem_opts () { 76 | # if we detect any of these settings in ${JAVA_OPTS} or ${JAVA_TOOL_OPTIONS} we need to NOT output our settings. 77 | # The reason is the Xms/Xmx, if they don't line up, cause errors. 78 | if [[ "${JAVA_OPTS}" == *-Xmx* ]] || [[ "${JAVA_OPTS}" == *-Xms* ]] || [[ "${JAVA_OPTS}" == *-XX:MaxPermSize* ]] || [[ "${JAVA_OPTS}" == *-XX:MaxMetaspaceSize* ]] || [[ "${JAVA_OPTS}" == *-XX:ReservedCodeCacheSize* ]]; then 79 | echo "" 80 | elif [[ "${JAVA_TOOL_OPTIONS}" == *-Xmx* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-Xms* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-XX:MaxPermSize* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-XX:MaxMetaspaceSize* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-XX:ReservedCodeCacheSize* ]]; then 81 | echo "" 82 | elif [[ "${SBT_OPTS}" == *-Xmx* ]] || [[ "${SBT_OPTS}" == *-Xms* ]] || [[ "${SBT_OPTS}" == *-XX:MaxPermSize* ]] || [[ "${SBT_OPTS}" == *-XX:MaxMetaspaceSize* ]] || [[ "${SBT_OPTS}" == *-XX:ReservedCodeCacheSize* ]]; then 83 | echo "" 84 | else 85 | # a ham-fisted attempt to move some memory settings in concert 86 | # so they need not be messed around with individually. 87 | local mem=${1:-1024} 88 | local codecache=$(( $mem / 8 )) 89 | (( $codecache > 128 )) || codecache=128 90 | (( $codecache < 512 )) || codecache=512 91 | local class_metadata_size=$(( $codecache * 2 )) 92 | local class_metadata_opt=$([[ "$java_version" < "1.8" ]] && echo "MaxPermSize" || echo "MaxMetaspaceSize") 93 | 94 | local arg_xms=$([[ "${java_args[@]}" == *-Xms* ]] && echo "" || echo "-Xms${mem}m") 95 | local arg_xmx=$([[ "${java_args[@]}" == *-Xmx* ]] && echo "" || echo "-Xmx${mem}m") 96 | local arg_rccs=$([[ "${java_args[@]}" == *-XX:ReservedCodeCacheSize* ]] && echo "" || echo "-XX:ReservedCodeCacheSize=${codecache}m") 97 | local arg_meta=$([[ "${java_args[@]}" == *-XX:${class_metadata_opt}* ]] && echo "" || echo "-XX:${class_metadata_opt}=${class_metadata_size}m") 98 | 99 | echo "${arg_xms} ${arg_xmx} ${arg_rccs} ${arg_meta}" 100 | fi 101 | } 102 | 103 | require_arg () { 104 | local type="$1" 105 | local opt="$2" 106 | local arg="$3" 107 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then 108 | echo "$opt requires <$type> argument" 109 | exit 1 110 | fi 111 | } 112 | 113 | is_function_defined() { 114 | declare -f "$1" > /dev/null 115 | } 116 | 117 | process_args () { 118 | while [[ $# -gt 0 ]]; do 119 | case "$1" in 120 | -h|-help) usage; exit 1 ;; 121 | -v|-verbose) verbose=1 && shift ;; 122 | -d|-debug) debug=1 && shift ;; 123 | 124 | -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; 125 | -mem) require_arg integer "$1" "$2" && sbt_mem="$2" && shift 2 ;; 126 | -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;; 127 | -batch) exec &1 | awk -F '"' '/version/ {print $2}') 146 | vlog "[process_args] java_version = '$java_version'" 147 | } 148 | 149 | # Detect that we have java installed. 150 | checkJava() { 151 | local required_version="$1" 152 | # Now check to see if it's a good enough version 153 | if [[ "$java_version" == "" ]]; then 154 | echo 155 | echo No java installations was detected. 156 | echo Please go to http://www.java.com/getjava/ and download 157 | echo 158 | exit 1 159 | elif [[ ! "$java_version" > "$required_version" ]]; then 160 | echo 161 | echo The java installation you have is not up to date 162 | echo $script_name requires at least version $required_version+, you have 163 | echo version $java_version 164 | echo 165 | echo Please go to http://www.java.com/getjava/ and download 166 | echo a valid Java Runtime and install before running $script_name. 167 | echo 168 | exit 1 169 | fi 170 | } 171 | 172 | 173 | run() { 174 | # no jar? download it. 175 | [[ -f "$sbt_jar" ]] || acquire_sbt_jar "$sbt_version" || { 176 | # still no jar? uh-oh. 177 | echo "Download failed. Obtain the sbt-launch.jar manually and place it at $sbt_jar" 178 | exit 1 179 | } 180 | 181 | # process the combined args, then reset "$@" to the residuals 182 | process_args "$@" 183 | set -- "${residual_args[@]}" 184 | argumentCount=$# 185 | 186 | # TODO - java check should be configurable... 187 | checkJava "1.6" 188 | 189 | #If we're in cygwin, we should use the windows config, and terminal hacks 190 | if [[ "$CYGWIN_FLAG" == "true" ]]; then 191 | stty -icanon min 1 -echo > /dev/null 2>&1 192 | addJava "-Djline.terminal=jline.UnixTerminal" 193 | addJava "-Dsbt.cygwin=true" 194 | fi 195 | 196 | # run sbt 197 | execRunner "$java_cmd" \ 198 | $(get_mem_opts $sbt_mem) \ 199 | ${JAVA_OPTS} \ 200 | ${SBT_OPTS:-$default_sbt_opts} \ 201 | ${java_args[@]} \ 202 | -jar "$sbt_jar" \ 203 | "${sbt_commands[@]}" \ 204 | "${residual_args[@]}" 205 | 206 | exit_code=$? 207 | 208 | # Clean up the terminal from cygwin hacks. 209 | if [[ "$CYGWIN_FLAG" == "true" ]]; then 210 | stty icanon echo > /dev/null 2>&1 211 | fi 212 | exit $exit_code 213 | } 214 | -------------------------------------------------------------------------------- /sbt-dist/bin/sbt-launch.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/sbt-dist/bin/sbt-launch.jar -------------------------------------------------------------------------------- /sbt-dist/bin/sbt.bat: -------------------------------------------------------------------------------- 1 | @REM SBT launcher script 2 | @REM 3 | @REM Envioronment: 4 | @REM JAVA_HOME - location of a JDK home dir (mandatory) 5 | @REM SBT_OPTS - JVM options (optional) 6 | @REM Configuration: 7 | @REM sbtconfig.txt found in the SBT_HOME. 8 | 9 | @REM ZOMG! We need delayed expansion to build up CFG_OPTS later 10 | @setlocal enabledelayedexpansion 11 | 12 | @echo off 13 | set SBT_HOME=%~dp0 14 | 15 | rem FIRST we load the config file of extra options. 16 | set FN=%SBT_HOME%\..\conf\sbtconfig.txt 17 | set CFG_OPTS= 18 | FOR /F "tokens=* eol=# usebackq delims=" %%i IN ("%FN%") DO ( 19 | set DO_NOT_REUSE_ME=%%i 20 | rem ZOMG (Part #2) WE use !! here to delay the expansion of 21 | rem CFG_OPTS, otherwise it remains "" for this loop. 22 | set CFG_OPTS=!CFG_OPTS! !DO_NOT_REUSE_ME! 23 | ) 24 | 25 | rem We use the value of the JAVACMD environment variable if defined 26 | set _JAVACMD=%JAVACMD% 27 | 28 | if "%_JAVACMD%"=="" ( 29 | if not "%JAVA_HOME%"=="" ( 30 | if exist "%JAVA_HOME%\bin\java.exe" set "_JAVACMD=%JAVA_HOME%\bin\java.exe" 31 | ) 32 | ) 33 | 34 | if "%_JAVACMD%"=="" set _JAVACMD=java 35 | 36 | rem We use the value of the JAVA_OPTS environment variable if defined, rather than the config. 37 | set _JAVA_OPTS=%JAVA_OPTS% 38 | if "%_JAVA_OPTS%"=="" set _JAVA_OPTS=%CFG_OPTS% 39 | 40 | :run 41 | 42 | "%_JAVACMD%" %_JAVA_OPTS% %SBT_OPTS% -cp "%SBT_HOME%sbt-launch.jar" xsbt.boot.Boot %* 43 | if ERRORLEVEL 1 goto error 44 | goto end 45 | 46 | :error 47 | @endlocal 48 | exit /B 1 49 | 50 | 51 | :end 52 | @endlocal 53 | exit /B 0 54 | -------------------------------------------------------------------------------- /sbt-dist/conf/sbtconfig.txt: -------------------------------------------------------------------------------- 1 | # Set the java args to high 2 | 3 | -Xmx512M 4 | 5 | -XX:MaxPermSize=256m 6 | 7 | -XX:ReservedCodeCacheSize=128m 8 | 9 | 10 | 11 | # Set the extra SBT options 12 | 13 | -Dsbt.log.format=true 14 | 15 | -------------------------------------------------------------------------------- /sbt-dist/conf/sbtopts: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------ # 2 | # The SBT Configuration file. # 3 | # ------------------------------------------------ # 4 | 5 | 6 | # Disable ANSI color codes 7 | # 8 | #-no-colors 9 | 10 | # Starts sbt even if the current directory contains no sbt project. 11 | # 12 | -sbt-create 13 | 14 | # Path to global settings/plugins directory (default: ~/.sbt) 15 | # 16 | #-sbt-dir /etc/sbt 17 | 18 | # Path to shared boot directory (default: ~/.sbt/boot in 0.11 series) 19 | # 20 | #-sbt-boot ~/.sbt/boot 21 | 22 | # Path to local Ivy repository (default: ~/.ivy2) 23 | # 24 | #-ivy ~/.ivy2 25 | 26 | # set memory options 27 | # 28 | #-mem 29 | 30 | # Use local caches for projects, no sharing. 31 | # 32 | #-no-share 33 | 34 | # Put SBT in offline mode. 35 | # 36 | #-offline 37 | 38 | # Sets the SBT version to use. 39 | #-sbt-version 0.11.3 40 | 41 | # Scala version (default: latest release) 42 | # 43 | #-scala-home 44 | #-scala-version 45 | 46 | # java version (default: java from PATH, currently $(java -version |& grep version)) 47 | # 48 | #-java-home 49 | 50 | -------------------------------------------------------------------------------- /sbt.bat: -------------------------------------------------------------------------------- 1 | @REM SBT launcher script 2 | 3 | .\sbt-dist\bin\sbt.bat %* 4 | -------------------------------------------------------------------------------- /screen-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/screen-shot.png -------------------------------------------------------------------------------- /tasktick-pwa/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # `npm` transients 9 | package-lock.json 10 | 11 | # Gulp generated 12 | client/www/bundle.js 13 | client/www/bundle.js.map 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | # parcel-bundler cache (https://parceljs.org/) 68 | .cache 69 | 70 | # next.js build output 71 | .next 72 | 73 | # nuxt.js build output 74 | .nuxt 75 | 76 | # vuepress build output 77 | .vuepress/dist 78 | 79 | # Serverless directories 80 | .serverless 81 | 82 | -------------------------------------------------------------------------------- /tasktick-pwa/build/server/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var express = require("express"); 4 | var compression = require("compression"); 5 | var app = express(); 6 | app.use(function (req, res, next) { 7 | console.log(req.url); 8 | next(); 9 | }); 10 | app.use(compression()); 11 | app.get("/t/**", function (req, res) { 12 | res.sendFile("index.html", { root: __dirname + "/../../www" }); 13 | }); 14 | app.get("/invite/**", function (req, res) { 15 | res.sendFile("index.html", { root: __dirname + "/../../www" }); 16 | }); 17 | app.get("/dev/**", function (req, res) { 18 | res.sendFile("developer.html", { root: __dirname + "/../../www" }); 19 | }); 20 | app.use(express.static(__dirname + "/../../www")); 21 | app.listen(3001); 22 | -------------------------------------------------------------------------------- /tasktick-pwa/build/shared/foo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.FOO = "FOO!"; 4 | -------------------------------------------------------------------------------- /tasktick-pwa/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var ts = require('gulp-typescript'); 3 | var webpack = require('webpack'); 4 | var gutil = require('gulp-util'); 5 | var nodemon = require('nodemon'); 6 | 7 | /** 8 | * Launch server and hot reload on changes. 9 | */ 10 | gulp.task('start', ["build:server", "build:client"], cb => { 11 | nodemon({ 12 | watch: ["build"], 13 | script: "build/server/main.js", 14 | ignore: ["src", "node_modules"] 15 | }) 16 | nodemon.once("start", cb); 17 | }) 18 | 19 | gulp.task('watch', [], () => { 20 | nodemon({ 21 | watch: ["build"], 22 | script: "build/server/main.js", 23 | ignore: ["src", "node_modules"] 24 | }) 25 | var config = [ 26 | webpackConfig("./src/client/index", "bundle.js")]; 27 | config.forEach(function(x) { x.watch = true }); 28 | webpack(config, (err, stats) => { 29 | if(err) { 30 | throw new gutil.PluginError("webpack:build", err); 31 | } 32 | else { 33 | gutil.log("[webpack]", stats.toString({})); 34 | } 35 | }) 36 | }) 37 | 38 | gulp.task('start-production', ["build:server", "production"], cb => { 39 | nodemon({ 40 | watch: ["build"], 41 | script: "build/server/main.js", 42 | ignore: ["src", "node_modules"] 43 | }) 44 | nodemon.once("start", cb); 45 | }) 46 | 47 | gulp.task('watch-production', ["start-production"], () => { 48 | gulp.watch(["src/client/**"], ["production"]); 49 | gulp.watch(["src/server/**"], ["build:server"]); 50 | gulp.watch(["src/shared/**"], ["production", "build:server"]); 51 | }); 52 | 53 | var tsProject = ts.createProject('tsconfig.json'); 54 | 55 | function webpackConfig(entry, out) { 56 | return { 57 | entry: entry, 58 | devtool: "source-map", 59 | stats: { 60 | errorDetails: true 61 | }, 62 | node: { 63 | fs: "empty" 64 | }, 65 | module: { 66 | loaders: [ 67 | { 68 | test: /\.tsx?$/, 69 | loader: 'ts-loader' 70 | }, 71 | { 72 | test: /\.less$/, 73 | loaders: ["style", "css", "less"] 74 | }, 75 | { 76 | test: /(\.png|\.jpg)$/, 77 | loader: "url-loader" 78 | }, 79 | { 80 | test: /\.(glsl|frag|vert)$/, 81 | loader: __dirname+'/glsl-loader' 82 | } 83 | ] 84 | }, 85 | resolve: { 86 | extensions: [".ts", ".js", ".tsx", ""] 87 | }, 88 | output: { 89 | path: "www/", 90 | filename: out 91 | }, 92 | plugins: [ 93 | new webpack.DefinePlugin({ 94 | 'process.env': { 95 | 'DEBUG': true 96 | } 97 | }), 98 | new webpack.optimize.DedupePlugin() 99 | ] 100 | } 101 | } 102 | 103 | function webpackProductionConfig(entry, out) { 104 | var entry = webpackConfig(entry, out); 105 | entry.plugins = [ 106 | new webpack.DefinePlugin({ 107 | 'process.env': { 108 | 'NODE_ENV': JSON.stringify('production'), 109 | 'DEBUG': false 110 | } 111 | }), 112 | new webpack.optimize.DedupePlugin(), 113 | new webpack.optimize.UglifyJsPlugin({ mangleProperties: true }), 114 | ] 115 | return entry; 116 | } 117 | 118 | gulp.task('build:server', () => { 119 | gulp.src('src/**/*.ts') 120 | .pipe(tsProject()) 121 | .pipe(gulp.dest('build')); 122 | }) 123 | 124 | gulp.task('build:client', cb => { 125 | var config = webpackConfig("./src/client/index", "bundle.js"); 126 | 127 | webpack(config, (err, stats) => { 128 | if(err) { 129 | throw new gutil.PluginError("webpack:build", err); 130 | } 131 | else { 132 | gutil.log("[webpack]", stats.toString({})); 133 | cb(); 134 | } 135 | }) 136 | }) 137 | 138 | gulp.task('production', cb => { 139 | var config = webpackProductionConfig("./src/client/index.tsx", "bundle.js") 140 | 141 | webpack(config, (err, stats) => { 142 | if(err) { 143 | throw new gutil.PluginError("webpack:build", err); 144 | } 145 | else { 146 | gutil.log("[webpack]", stats.toString({})); 147 | cb(); 148 | } 149 | }) 150 | }) 151 | 152 | gulp.task('default', ["build:server", "build:client"]); 153 | 154 | -------------------------------------------------------------------------------- /tasktick-pwa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tasktick", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "corey auger", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@material-ui/core": "^3.3.2", 13 | "@material-ui/icons": "^3.0.1", 14 | "@types/react": "^16.4.18", 15 | "@types/react-dom": "^16.0.9", 16 | "brace": "^0.11.1", 17 | "classnames": "^2.2.5", 18 | "compression": "1.6.2", 19 | "express": "4.14.0", 20 | "gulp-ts": "^0.3.0", 21 | "mobx": "5.5.2", 22 | "mobx-react": "^5.3.6", 23 | "mobx-react-router": "^4.0.5", 24 | "react": "^16.6.0", 25 | "react-dom": "^16.6.0", 26 | "react-hot-loader": "^4.3.12", 27 | "react-router": "^4.3.1", 28 | "ws": "1.1.1" 29 | }, 30 | "devDependencies": { 31 | "@types/classnames": "^2.2.3", 32 | "@types/compression": "0.0.33", 33 | "@types/es6-shim": "0.31.32", 34 | "@types/express": "4.0.33", 35 | "@types/history": "4.5.0", 36 | "@types/node": "^10.12.2", 37 | "@types/ws": "0.0.35", 38 | "css-loader": "0.25.0", 39 | "file-loader": "0.9.0", 40 | "gulp": "^3.9.1", 41 | "gulp-typedoc": "^2.1.1", 42 | "gulp-typescript": "^3.1.2", 43 | "gulp-util": "3.0.7", 44 | "history": "4.5.0", 45 | "html5-notification": "3.0.0", 46 | "less": "2.7.1", 47 | "less-loader": "2.2.3", 48 | "mobx-react-devtools": "^6.0.3", 49 | "nodemon": "^1.11.0", 50 | "style-loader": "0.13.1", 51 | "ts-loader": "3.2.0", 52 | "typedoc": "0.9.0", 53 | "typescript": "2.8.1", 54 | "url-loader": "0.5.7", 55 | "webpack": "1.13.3", 56 | "webpack-stream": "3.2.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tasktick-pwa/src/client/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route } from 'react-router'; 3 | import classNames from 'classnames'; 4 | import {observer, inject} from 'mobx-react'; 5 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 6 | import CssBaseline from '@material-ui/core/CssBaseline'; 7 | import Drawer from '@material-ui/core/Drawer'; 8 | import AppBar from '@material-ui/core/AppBar'; 9 | import Toolbar from '@material-ui/core/Toolbar'; 10 | import List from '@material-ui/core/List'; 11 | import Typography from '@material-ui/core/Typography'; 12 | import Divider from '@material-ui/core/Divider'; 13 | import IconButton from '@material-ui/core/IconButton'; 14 | import Badge from '@material-ui/core/Badge'; 15 | import MenuIcon from '@material-ui/icons/Menu'; 16 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; 17 | import NotificationsIcon from '@material-ui/icons/Notifications'; 18 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 19 | import createStyles from '@material-ui/core/styles/createStyles'; 20 | import withRoot from './withRoot'; 21 | import stores from './stores/index'; 22 | 23 | import ListItem from '@material-ui/core/ListItem'; 24 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 25 | import ListItemText from '@material-ui/core/ListItemText'; 26 | import ListSubheader from '@material-ui/core/ListSubheader'; 27 | import PeopleIcon from '@material-ui/icons/People'; 28 | import BarChartIcon from '@material-ui/icons/BarChart'; 29 | import LayersIcon from '@material-ui/icons/Layers'; 30 | import AssignmentIcon from '@material-ui/icons/Assignment'; 31 | 32 | import {TasktickSocket} from './socket/WebSocket' 33 | import SignIn from './pages/SignIn'; 34 | import Projects from './pages/Projects'; 35 | import { uuidv4, Project } from './stores/data'; 36 | import { Menu, MenuItem } from '@material-ui/core'; 37 | import Account from './pages/Account'; 38 | import Users from './pages/Users'; 39 | 40 | 41 | 42 | const drawerWidth = 240; 43 | 44 | const styles = (theme: Theme) => 45 | createStyles({ 46 | root: { 47 | display: 'flex', 48 | }, 49 | toolbar: { 50 | paddingRight: 24, // keep right padding when drawer closed 51 | }, 52 | toolbarIcon: { 53 | display: 'flex', 54 | alignItems: 'center', 55 | justifyContent: 'flex-end', 56 | padding: '0 8px', 57 | ...theme.mixins.toolbar, 58 | }, 59 | appBar: { 60 | zIndex: theme.zIndex.drawer + 1, 61 | transition: theme.transitions.create(['width', 'margin'], { 62 | easing: theme.transitions.easing.sharp, 63 | duration: theme.transitions.duration.leavingScreen, 64 | }), 65 | }, 66 | appBarShift: { 67 | marginLeft: drawerWidth, 68 | width: `calc(100% - ${drawerWidth}px)`, 69 | transition: theme.transitions.create(['width', 'margin'], { 70 | easing: theme.transitions.easing.sharp, 71 | duration: theme.transitions.duration.enteringScreen, 72 | }), 73 | }, 74 | menuButton: { 75 | marginLeft: 12, 76 | marginRight: 36, 77 | }, 78 | menuButtonHidden: { 79 | display: 'none', 80 | }, 81 | title: { 82 | flexGrow: 1, 83 | }, 84 | drawerPaper: { 85 | position: 'relative', 86 | whiteSpace: 'nowrap', 87 | width: drawerWidth, 88 | transition: theme.transitions.create('width', { 89 | easing: theme.transitions.easing.sharp, 90 | duration: theme.transitions.duration.enteringScreen, 91 | }), 92 | }, 93 | drawerPaperClose: { 94 | overflowX: 'hidden', 95 | transition: theme.transitions.create('width', { 96 | easing: theme.transitions.easing.sharp, 97 | duration: theme.transitions.duration.leavingScreen, 98 | }), 99 | width: theme.spacing.unit * 7, 100 | [theme.breakpoints.up('sm')]: { 101 | width: theme.spacing.unit * 9, 102 | }, 103 | }, 104 | appBarSpacer: theme.mixins.toolbar, 105 | content: { 106 | flexGrow: 1, 107 | padding: theme.spacing.unit * 3, 108 | height: '100vh', 109 | overflow: 'auto', 110 | backgroundColor: "#f1f0f1", 111 | }, 112 | h5: { 113 | marginBottom: theme.spacing.unit * 2, 114 | }, 115 | logo:{ 116 | mark: { 117 | width: 36, 118 | height: 36, 119 | } 120 | } 121 | }); 122 | 123 | type State = { 124 | open: boolean; 125 | anchorEl: any; 126 | }; 127 | 128 | type Props = { 129 | store: any 130 | } 131 | 132 | @inject('routing') 133 | @observer 134 | class App extends React.Component, State> { 135 | state = { 136 | open: true, 137 | anchorEl: null 138 | }; 139 | constructor(props){ 140 | super(props); 141 | if(window.localStorage.getItem("authToken")) 142 | props.store.socketStore.connect(new TasktickSocket(window.localStorage.getItem("authToken"))) 143 | } 144 | 145 | handleDrawerOpen = () => { 146 | this.setState({ open: true }); 147 | }; 148 | 149 | handleDrawerClose = () => { 150 | this.setState({ open: false }); 151 | }; 152 | onSelectProject = (id) => () =>{ 153 | const { location, push, goBack } = stores.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 154 | push("/p/project/"+id) 155 | } 156 | 157 | openMenu = event => { 158 | this.setState({ anchorEl: event.currentTarget }); 159 | }; 160 | 161 | handleClose = () => { 162 | this.setState({ anchorEl: null }); 163 | }; 164 | handleLogout = () =>{ 165 | window.localStorage.clear(); 166 | if(this.props.store.socketStore.socket) 167 | this.props.store.socketStore.socket.close() 168 | this.props.store.clear() 169 | const { location, push, goBack } = this.props.store.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 170 | this.handleClose(); 171 | push("/p/signin") 172 | } 173 | handleMyAccpint = () =>{ 174 | const { location, push, goBack } = this.props.store.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 175 | this.handleClose(); 176 | push("/p/account") 177 | } 178 | 179 | render() { 180 | console.log("APP RENDER!!") 181 | const projectList = this.props.store.projectStore.projects 182 | const { classes } = this.props; 183 | const { location, push, goBack } = stores.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 184 | return ( 185 | 186 | 187 | 188 |

189 | 193 | 194 | 195 | 204 | 205 | 206 | 213 | TaskTick 214 | 215 | 216 | 217 | 218 | 219 | 220 | 226 | Profile 227 | My account 228 | Logout 229 | 230 | 231 | 232 | 238 |
239 | logo 240 | 241 | 242 | 243 | 244 |
245 | 246 | 247 |
248 | 249 | 250 | 251 | 252 | push("/p/projects")} /> 253 | 254 | 255 | 256 | 257 | 258 | push("/p/users")} /> 259 | 260 | 261 | 262 | 263 | 264 | push("/p/metrics")} /> 265 | 266 |
267 |
268 | 269 | 270 |
271 | Projects 272 | {projectList.map(x => ( 273 | 274 | 275 | 276 | 277 | 278 | 279 | ))} 280 |
281 |
282 |
283 |
284 |
285 | 286 | } /> 287 | } /> 288 | } /> 289 | } /> 290 | } /> 291 | 292 | 293 |
294 |
295 | 296 | 297 | ); 298 | } 299 | } 300 | 301 | export default withRoot((withStyles(styles)(App) )); 302 | -------------------------------------------------------------------------------- /tasktick-pwa/src/client/components/ProjectCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 3 | import classnames from 'classnames'; 4 | import createStyles from '@material-ui/core/styles/createStyles'; 5 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 6 | import withRoot from '../withRoot'; 7 | import Card from '@material-ui/core/Card'; 8 | import CardHeader from '@material-ui/core/CardHeader'; 9 | import CardMedia from '@material-ui/core/CardMedia'; 10 | import CardContent from '@material-ui/core/CardContent'; 11 | import CardActions from '@material-ui/core/CardActions'; 12 | import Collapse from '@material-ui/core/Collapse'; 13 | import Avatar from '@material-ui/core/Avatar'; 14 | import IconButton from '@material-ui/core/IconButton'; 15 | import Typography from '@material-ui/core/Typography'; 16 | import red from '@material-ui/core/colors/red'; 17 | import FavoriteIcon from '@material-ui/icons/Favorite'; 18 | import ShareIcon from '@material-ui/icons/Share'; 19 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 20 | import MoreVertIcon from '@material-ui/icons/MoreVert'; 21 | import List from '@material-ui/core/List'; 22 | import ListItem from '@material-ui/core/ListItem'; 23 | import ListItemText from '@material-ui/core/ListItemText'; 24 | import CardActionArea from '@material-ui/core/CardActionArea'; 25 | import { Project, Task } from '../stores/data'; 26 | import { TextField, Button, Grid, Checkbox } from '@material-ui/core'; 27 | import { observer } from 'mobx-react'; 28 | 29 | const styles = (theme: Theme) => 30 | createStyles({ 31 | card: { 32 | maxWidth: 800, 33 | }, 34 | thin:{ 35 | width: "75%" 36 | }, 37 | media: { 38 | height: 0, 39 | paddingTop: '56.25%', // 16:9 40 | }, 41 | actions: { 42 | display: 'flex', 43 | }, 44 | expand: { 45 | transform: 'rotate(0deg)', 46 | transition: theme.transitions.create('transform', { 47 | duration: theme.transitions.duration.shortest, 48 | }), 49 | marginLeft: 'auto', 50 | [theme.breakpoints.up('sm')]: { 51 | marginRight: -8, 52 | }, 53 | }, 54 | expandOpen: { 55 | transform: 'rotate(180deg)', 56 | }, 57 | avatar: { 58 | backgroundColor: red[500], 59 | }, 60 | strike: { 61 | textDecoration: "line-through", 62 | }, 63 | button: { 64 | margin: theme.spacing.unit, 65 | float: "right" 66 | }, 67 | }); 68 | 69 | type State = { 70 | expanded: boolean 71 | taskName: string 72 | }; 73 | 74 | interface Props { 75 | store: any; 76 | project: Project; 77 | expanded?: boolean; 78 | onTaskSelect: (Task) => void; 79 | onProjectSelect?: (Project) => void; 80 | }; 81 | 82 | @observer 83 | class ProjectCard extends React.Component, State> { 84 | state = { expanded: this.props.expanded, taskName: "" }; 85 | 86 | componentDidMount(){ 87 | console.log("MOUNT PROJECT>..") 88 | this.props.store.socketStore.socket.send("GetProject", {id: this.props.project.id}) 89 | } 90 | componentDidUpdate(prevProps){ 91 | if(prevProps.project.id != this.props.project.id){ 92 | // refresh the project.. 93 | console.log("REFRESH PROJECT>..") 94 | this.props.store.socketStore.socket.send("GetProject", {id: this.props.project.id}) 95 | } 96 | } 97 | 98 | handleExpandClick = () => { 99 | this.setState(state => ({ expanded: !state.expanded })); 100 | }; 101 | onTaskSelect = (x: Task) => { 102 | this.props.onTaskSelect(x); 103 | } 104 | selectServiceType = () =>{ 105 | if(this.props.project) 106 | this.props.onProjectSelect(this.props.project) 107 | } 108 | updateTaskName = (event) => { 109 | this.setState({ taskName: event.target.value }); 110 | }; 111 | addTask = () =>{ 112 | //project: UUID, name: String, description: String, section: String, parent: Option[UUID] = None 113 | this.props.store.socketStore.socket.send("NewTask", {project: this.props.project.id, name: this.state.taskName, description: "", section: "Default"}) 114 | this.setState({taskName: ""}) 115 | } 116 | toggleTaskDone = (t: Task) => () =>{ 117 | t.done=!t.done 118 | this.props.store.socketStore.socket.send("EditTask", {task: t}) 119 | } 120 | 121 | render() { 122 | const classes = this.props.classes 123 | const sd = this.props.project 124 | //console.log("Project !!!:", this.props.store.taskStore.tasks) 125 | return ( 126 | 127 | 130 | P 131 | 132 | } 133 | action={ 134 | 135 | 136 | 137 | } 138 | title={sd.name} 139 | subheader={sd.description} 140 | /> 141 | 142 | 147 | 148 | 149 | {sd.description} 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 165 | 166 | 167 | 168 | 169 | 170 | Tasks: 171 |
172 | 173 |
174 | < TextField onChange={this.updateTaskName} value={this.state.taskName} autoFocus margin="dense" id="task" label="New Task" type="text" fullWidth /> 175 |
176 | 177 |
178 |
179 | 180 | {sd.tasks.map(x => this.props.store.taskStore.tasks[x] ).filter(x => x).sort( (a:Task, b:Task) => (a.name < b.name ? -1 : 1) ).map( (x: Task ) => ( 181 | this.onTaskSelect(x) } > 182 | 187 | 188 | ) 189 | )} 190 | 191 |
192 |
193 |
194 |
195 | ); 196 | } 197 | } 198 | 199 | export default withRoot(withStyles(styles)(ProjectCard)); -------------------------------------------------------------------------------- /tasktick-pwa/src/client/components/TablePager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 3 | import FirstPageIcon from '@material-ui/icons/FirstPage'; 4 | import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; 5 | import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; 6 | import LastPageIcon from '@material-ui/icons/LastPage'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import createStyles from '@material-ui/core/styles/createStyles'; 9 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 10 | 11 | import withRoot from '../withRoot'; 12 | 13 | const styles = (theme: Theme) => 14 | createStyles({ 15 | root: { 16 | flexShrink: 0, 17 | color: theme.palette.text.secondary, 18 | marginLeft: theme.spacing.unit * 2.5, 19 | }, 20 | }); 21 | 22 | 23 | interface Props { 24 | count: number; 25 | onChangePage: (Event, number) => void; 26 | page: number; 27 | rowsPerPage: number; 28 | direction: "rtl" | "ltr"; 29 | }; 30 | 31 | class TablePager extends React.Component, undefined> { 32 | 33 | handleFirstPageButtonClick = event => { 34 | this.props.onChangePage(event, 0); 35 | }; 36 | 37 | handleBackButtonClick = event => { 38 | this.props.onChangePage(event, this.props.page - 1); 39 | }; 40 | 41 | handleNextButtonClick = event => { 42 | this.props.onChangePage(event, this.props.page + 1); 43 | }; 44 | 45 | handleLastPageButtonClick = event => { 46 | this.props.onChangePage( 47 | event, 48 | Math.max(0, Math.ceil(this.props.count / this.props.rowsPerPage) - 1), 49 | ); 50 | }; 51 | 52 | render() { 53 | const classes = this.props.classes; 54 | return ( 55 |
56 | 61 | {this.props.direction === 'rtl' ? : } 62 | 63 | 68 | {this.props.direction === 'rtl' ? : } 69 | 70 | = Math.ceil(this.props.count / this.props.rowsPerPage) - 1} 73 | aria-label="Next Page" 74 | > 75 | {this.props.direction === 'rtl' ? : } 76 | 77 | = Math.ceil(this.props.count / this.props.rowsPerPage) - 1} 80 | aria-label="Last Page" 81 | > 82 | {this.props.direction === 'rtl' ? : } 83 | 84 |
85 | ); 86 | } 87 | } 88 | 89 | export default withRoot(withStyles(styles)(TablePager)); -------------------------------------------------------------------------------- /tasktick-pwa/src/client/components/TaskCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 3 | import classnames from 'classnames'; 4 | import createStyles from '@material-ui/core/styles/createStyles'; 5 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 6 | import withRoot from '../withRoot'; 7 | import Card from '@material-ui/core/Card'; 8 | import CardHeader from '@material-ui/core/CardHeader'; 9 | import CardMedia from '@material-ui/core/CardMedia'; 10 | import CardContent from '@material-ui/core/CardContent'; 11 | import CardActions from '@material-ui/core/CardActions'; 12 | import Collapse from '@material-ui/core/Collapse'; 13 | import Avatar from '@material-ui/core/Avatar'; 14 | import IconButton from '@material-ui/core/IconButton'; 15 | import Typography from '@material-ui/core/Typography'; 16 | import red from '@material-ui/core/colors/red'; 17 | import FavoriteIcon from '@material-ui/icons/Favorite'; 18 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 19 | import MoreVertIcon from '@material-ui/icons/MoreVert'; 20 | import List from '@material-ui/core/List'; 21 | import ListItem from '@material-ui/core/ListItem'; 22 | import ListItemText from '@material-ui/core/ListItemText'; 23 | import CardActionArea from '@material-ui/core/CardActionArea'; 24 | import { Project, Task, Note } from '../stores/data'; 25 | import { TextField, Button, Grid, Checkbox } from '@material-ui/core'; 26 | import { observer } from 'mobx-react'; 27 | 28 | const styles = (theme: Theme) => 29 | createStyles({ 30 | card: { 31 | }, 32 | media: { 33 | height: 0, 34 | paddingTop: '56.25%', // 16:9 35 | }, 36 | actions: { 37 | display: 'flex', 38 | }, 39 | textField: { 40 | marginLeft: theme.spacing.unit, 41 | marginRight: theme.spacing.unit, 42 | width: 200, 43 | }, 44 | expand: { 45 | transform: 'rotate(0deg)', 46 | transition: theme.transitions.create('transform', { 47 | duration: theme.transitions.duration.shortest, 48 | }), 49 | marginLeft: 'auto', 50 | [theme.breakpoints.up('sm')]: { 51 | marginRight: -8, 52 | }, 53 | }, 54 | expandOpen: { 55 | transform: 'rotate(180deg)', 56 | }, 57 | avatar: { 58 | backgroundColor: red[500], 59 | }, 60 | slim: { 61 | width: "75%" 62 | }, 63 | button: { 64 | margin: theme.spacing.unit, 65 | float: "right" 66 | }, 67 | }); 68 | 69 | type State = { 70 | name: string 71 | note: string 72 | description: string 73 | startDate?: number 74 | endDate?: number 75 | }; 76 | 77 | interface Props { 78 | store: any; 79 | task: Task; 80 | //onNoteAdd: (Note) => void; 81 | }; 82 | 83 | @observer 84 | class TaskCard extends React.Component, State> { 85 | state = { name: this.props.task.name, note: "", description: this.props.task.description, startDate: this.props.task.startDate, endDate: this.props.task.endDate }; 86 | 87 | componentDidUpdate(prevProps){ 88 | if(prevProps.task.id != this.props.task.id){ 89 | this.setState({ name: this.props.task.name, note: "", description: this.props.task.description, startDate: this.props.task.startDate, endDate: this.props.task.endDate }); 90 | } 91 | } 92 | 93 | updateTaskName = (event) => { 94 | this.setState({ name: event.target.value }); 95 | }; 96 | updateEndDate = (event) => { 97 | const time = new Date(event.target.value).getTime() 98 | this.setState({ endDate: time }); 99 | }; 100 | updateStartDate = (event) => { 101 | const time = new Date(event.target.value).getTime() 102 | this.setState({ startDate: time }); 103 | }; 104 | updateTaskDescription = (event) => { 105 | this.setState({ description: event.target.value }); 106 | }; 107 | updateNoteName= (event) => { 108 | this.setState({ note: event.target.value }); 109 | }; 110 | toggleTaskDone = (t: Task) => () =>{ 111 | t.done=!t.done 112 | this.updateTask(t); 113 | } 114 | addNote = () =>{ 115 | this.props.store.socketStore.socket.send("NewNote", {task: this.props.task.id, project: this.props.task.project, note: this.state.note}) 116 | this.setState({note: ""}) 117 | } 118 | updateTask = (t: Task) =>{ 119 | this.props.store.socketStore.socket.send("EditTask", {task: t}) 120 | } 121 | saveTask = () =>{ 122 | let t = this.props.task 123 | t.name = this.state.name 124 | t.description = this.state.description 125 | t.endDate = this.state.endDate 126 | t.startDate = this.state.startDate 127 | console.log("T", t) 128 | this.updateTask(t) 129 | } 130 | 131 | render() { 132 | const classes = this.props.classes 133 | const sd = this.props.task 134 | //console.log("Project !!!:", this.props.store.taskStore.tasks) 135 | return ( 136 | 137 | 140 | T 141 | 142 | } 143 | action={ 144 | 149 | } 150 | title={sd.name} 151 | subheader={sd.description} 152 | /> 153 | 154 | 155 | {sd.description} 156 | 157 | 158 | 159 |
160 | 161 | 162 | 172 | 183 |
184 |
185 | Notes: 186 |
187 | 188 |
189 |
190 | 191 | {sd.notes.filter(x => x).map( (x: Note ) => ( 192 | 193 | 194 | ) 195 | )} 196 | 197 |
198 |
199 | 200 |
201 |
202 | 203 |
204 | ); 205 | } 206 | } 207 | 208 | export default withRoot(withStyles(styles)(TaskCard)); -------------------------------------------------------------------------------- /tasktick-pwa/src/client/components/TraceTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {observer} from 'mobx-react'; 3 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 4 | import createStyles from '@material-ui/core/styles/createStyles'; 5 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 6 | import Paper from '@material-ui/core/Paper'; 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | import Table from '@material-ui/core/Table'; 9 | import TableBody from '@material-ui/core/TableBody'; 10 | import TableCell from '@material-ui/core/TableCell'; 11 | import TableFooter from '@material-ui/core/TableFooter'; 12 | import TablePagination from '@material-ui/core/TablePagination'; 13 | import TableRow from '@material-ui/core/TableRow'; 14 | import TableHead from '@material-ui/core/TableHead'; 15 | import withRoot from '../withRoot'; 16 | import OutIcon from '@material-ui/icons/ArrowLeft'; 17 | import InIcon from '@material-ui/icons/ExitToApp'; 18 | import ErrorIcon from '@material-ui/icons/Error'; 19 | import TablePager from './TablePager'; 20 | 21 | 22 | const styles = (theme: Theme) => 23 | createStyles({ 24 | root: { 25 | width: '100%' 26 | }, 27 | table: { 28 | minWidth: 500, 29 | }, 30 | tableWrapper: { 31 | overflowX: 'auto', 32 | }, 33 | }); 34 | 35 | type State = { 36 | page: number, 37 | rowsPerPage: number; 38 | selectedIds: string[]; 39 | }; 40 | 41 | interface Props { 42 | store: any; 43 | multiSelect?: boolean; 44 | service?: string 45 | onSelect?: (Trace) => void; 46 | }; 47 | 48 | @observer 49 | class TraceTable extends React.Component, State> { 50 | state = { 51 | page: 0, 52 | rowsPerPage: 25, 53 | selectedIds: [] 54 | }; 55 | 56 | handleChangePage = (event, page) => { 57 | this.setState({...this.state, page }); 58 | }; 59 | 60 | handleChangeRowsPerPage = event => { 61 | this.setState({...this.state, rowsPerPage: event.target.value }); 62 | }; 63 | handleClick = (event, selectedId:string) => { 64 | if(this.props.multiSelect){ 65 | if(this.isInSelected(selectedId)) 66 | this.setState({...this.state, selectedIds: this.state.selectedIds.filter(x => x != selectedId) }); 67 | else 68 | this.setState({...this.state, selectedIds: [...this.state.selectedIds, selectedId] }); 69 | }else{ 70 | this.setState({...this.state, selectedIds: [selectedId] }); 71 | } 72 | if(this.props.onSelect)this.props.onSelect(this.props.store.traceStore.events[selectedId]) 73 | } 74 | isSelected = (id: string): boolean => 75 | this.state.selectedIds[this.state.selectedIds.length -1 ] == id 76 | isInSelected = (id: string): boolean => 77 | this.state.selectedIds.indexOf(id) != -1 78 | 79 | render() { 80 | const traceStream = this.props.store.traceStore.stream 81 | const classes = this.props.classes; 82 | 83 | //const rows = (this.props.service ? traceStream.filter( (x:Trace) => x.service == this.props.service ) : traceStream).map( x => ({id: x.id, dir: x.dir, name: x.event.meta.eventType, service: x.service, time: x.event.meta.occurredAt})); 84 | const rows = [] 85 | const emptyRows = this.state.rowsPerPage - Math.min(this.state.rowsPerPage, rows.length - this.state.page * this.state.rowsPerPage); 86 | return ( 87 |
88 | 89 | 90 | 91 | {this.props.multiSelect && } 92 | 93 | Event Type 94 | Service 95 | Time 96 | 97 | 98 | 99 | {rows.slice(this.state.page * this.state.rowsPerPage, this.state.page * this.state.rowsPerPage + this.state.rowsPerPage).map(row => { 100 | return ( 101 | this.handleClick(event, row.id)} 103 | selected={this.isSelected(row.id)} 104 | > 105 | {this.props.multiSelect && 106 | 107 | } 108 | { (row.dir == "in" ? : row.dir == "out" ? : ) } 109 | 110 | {row.name} 111 | 112 | {row.service} 113 | {row.time} 114 | 115 | ); 116 | })} 117 | {emptyRows > 0 && ( 118 | 119 | 120 | 121 | )} 122 | 123 | 124 | 125 | 135 | 136 | 137 |
138 |
139 |
); 140 | } 141 | } 142 | 143 | export default withRoot(withStyles(styles)(TraceTable)); -------------------------------------------------------------------------------- /tasktick-pwa/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Provider } from 'mobx-react'; 4 | import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; 5 | 6 | 7 | import createBrowserHistory from 'history/createBrowserHistory'; 8 | import { syncHistoryWithStore } from 'mobx-react-router'; 9 | import { Router, Route } from 'react-router'; 10 | 11 | import App from './App'; 12 | 13 | const browserHistory = createBrowserHistory(); 14 | 15 | import stores from './stores/index'; 16 | 17 | const history = syncHistoryWithStore(browserHistory, stores.routing); 18 | 19 | ReactDOM.render( 20 | 21 | 22 | 23 | 24 | , 25 | document.getElementById('root') 26 | ); 27 | -------------------------------------------------------------------------------- /tasktick-pwa/src/client/pages/Account.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {observer} from 'mobx-react'; 3 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 4 | import createStyles from '@material-ui/core/styles/createStyles'; 5 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import withRoot from '../withRoot'; 8 | import ProjectCard from '../components/ProjectCard'; 9 | import { Task, Project, uuidv4 } from '../stores/data'; 10 | import TaskCard from '../components/TaskCard'; 11 | import { Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Button, Card, CardActionArea, CardMedia, CardContent, Typography, CardActions, Avatar, CardHeader } from '@material-ui/core'; 12 | import AddIcon from '@material-ui/icons/Add'; 13 | import { Fab } from '@material-ui/core'; 14 | 15 | const styles = (theme: Theme) => 16 | createStyles({ 17 | root: { 18 | 19 | }, 20 | tableContainer: { 21 | height: 320, 22 | }, 23 | paper: { 24 | padding: theme.spacing.unit * 2, 25 | textAlign: 'center', 26 | color: theme.palette.text.secondary, 27 | }, 28 | fab: { 29 | margin: theme.spacing.unit, 30 | position: "absolute", 31 | right: 0, 32 | bottom: 0, 33 | }, 34 | button: { 35 | margin: theme.spacing.unit, 36 | float: "right" 37 | }, 38 | media: { 39 | height: 340, 40 | }, 41 | }); 42 | 43 | type State = { 44 | 45 | }; 46 | 47 | interface Props { 48 | store: any; 49 | }; 50 | 51 | @observer 52 | class Account extends React.Component, State> { 53 | state = { 54 | 55 | }; 56 | 57 | linkGithub = () => { 58 | window.open('/api/auth/github', '_blank'); 59 | } 60 | 61 | render() { 62 | const { location, push, goBack } = this.props.store.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 63 | const user = this.props.store.userStore.users.length && this.props.store.userStore.users[0] 64 | const pathname = location.pathname.split('/') 65 | const classes = this.props.classes; 66 | return (
67 | 68 | 69 | 70 | 71 | 76 | 77 | 78 | {user && user.firstName} {user && user.lastName} 79 | 80 | 81 | {user && user.email} 82 | 83 | 84 | 85 | 86 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 99 | U 100 | 101 | } 102 | title={"Account Information"} 103 | subheader={"update your account information"} 104 | /> 105 | 106 | 107 | {"this is some text."} 108 | 109 | 110 | 111 |
112 | 113 | 114 |
115 |
116 | 117 |
118 |
119 |
120 | 121 |
122 |
123 |
); 124 | } 125 | } 126 | 127 | export default withRoot(withStyles(styles)(Account)); -------------------------------------------------------------------------------- /tasktick-pwa/src/client/pages/Projects.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {observer} from 'mobx-react'; 3 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 4 | import createStyles from '@material-ui/core/styles/createStyles'; 5 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import withRoot from '../withRoot'; 8 | import ProjectCard from '../components/ProjectCard'; 9 | import { Task, Project, uuidv4 } from '../stores/data'; 10 | import TaskCard from '../components/TaskCard'; 11 | import { Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Button } from '@material-ui/core'; 12 | import AddIcon from '@material-ui/icons/Add'; 13 | import { Fab } from '@material-ui/core'; 14 | 15 | const styles = (theme: Theme) => 16 | createStyles({ 17 | root: { 18 | 19 | }, 20 | tableContainer: { 21 | height: 320, 22 | }, 23 | paper: { 24 | padding: theme.spacing.unit * 2, 25 | textAlign: 'center', 26 | color: theme.palette.text.secondary, 27 | }, 28 | fab: { 29 | margin: theme.spacing.unit, 30 | position: "absolute", 31 | right: 0, 32 | bottom: 0, 33 | }, 34 | }); 35 | 36 | type State = { 37 | task: Task; 38 | name: string; 39 | description: string; 40 | showAddProject: boolean; 41 | }; 42 | 43 | interface Props { 44 | store: any; 45 | project?: string 46 | }; 47 | 48 | @observer 49 | class Projects extends React.Component, State> { 50 | state = { 51 | task: undefined, 52 | name: "", 53 | description: "", 54 | showAddProject: false 55 | }; 56 | 57 | onTaskSelect = (task: Task) => { 58 | this.setState({...this.state, task}); 59 | } 60 | clearTask = () => { 61 | this.setState({...this.state, task: undefined}); 62 | } 63 | onProjectSelect = (project: Project) => { 64 | const { location, push, goBack } = this.props.store.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 65 | this.setState({task: undefined}) 66 | push('/p/project/'+project.id) 67 | } 68 | showAddProject = () =>{ 69 | this.setState({showAddProject:true}) 70 | } 71 | updateProjectName = (event) => { 72 | this.setState({ name: event.target.value }); 73 | }; 74 | updateProjectDescription = (event) => { 75 | this.setState({ description: event.target.value }); 76 | }; 77 | hideAddProject = () =>{ 78 | this.setState({showAddProject:false, name: "", description: ""}) 79 | } 80 | saveProject = () => { 81 | // this is a hack right now to send 'uuidv4()' so that we have a value to serialize to a UUID (note owner is replaced service side anyways) 82 | this.props.store.socketStore.socket.send("NewProject", {name: this.state.name, description: this.state.description, owner: uuidv4(), team: uuidv4()}) 83 | this.hideAddProject(); 84 | } 85 | 86 | render() { 87 | const { location, push, goBack } = this.props.store.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 88 | const pathname = location.pathname.split('/') 89 | const selectedProject = this.props.project ? this.props.project : (pathname.length == 4) ? pathname[3] : undefined; 90 | const projectList = this.props.store.projectStore.projects 91 | const selectProject = this.props.store.projectStore.projects.find(x => x.id == selectedProject) 92 | if(this.state.task && selectProject.id != this.state.task.project){ 93 | this.setState({task: undefined}) 94 | } 95 | const classes = this.props.classes; 96 | return (
97 | {selectProject ? ( 98 | 99 | 100 | 101 | 102 | {this.state.task ? 103 | 104 | :null} 105 | 106 | 107 | ):( 108 | 109 | {projectList.map( (x: Project) => )} 110 | )} 111 | 112 | 113 | 114 | 115 | 120 | New Project 121 | 122 | 123 | Create a new project. 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
); 134 | } 135 | } 136 | 137 | export default withRoot(withStyles(styles)(Projects)); -------------------------------------------------------------------------------- /tasktick-pwa/src/client/pages/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Avatar from '@material-ui/core/Avatar'; 3 | import Button from '@material-ui/core/Button'; 4 | import CssBaseline from '@material-ui/core/CssBaseline'; 5 | import FormControl from '@material-ui/core/FormControl'; 6 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | import Input from '@material-ui/core/Input'; 9 | import InputLabel from '@material-ui/core/InputLabel'; 10 | import LockIcon from '@material-ui/icons/LockOutlined'; 11 | import Paper from '@material-ui/core/Paper'; 12 | import Typography from '@material-ui/core/Typography'; 13 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 14 | import createStyles from '@material-ui/core/styles/createStyles'; 15 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 16 | import withRoot from '../withRoot'; 17 | import { Link } from '@material-ui/core'; 18 | import zIndex from '@material-ui/core/styles/zIndex'; 19 | import { TasktickSocket } from '../socket/WebSocket'; 20 | 21 | const styles = (theme: Theme) => 22 | createStyles({ 23 | layout: { 24 | width: 'auto', 25 | display: 'block', // Fix IE 11 issue. 26 | marginLeft: theme.spacing.unit * 3, 27 | marginRight: theme.spacing.unit * 3, 28 | [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: { 29 | width: 400, 30 | marginLeft: 'auto', 31 | marginRight: 'auto', 32 | }, 33 | }, 34 | whiteout: { 35 | position: "fixed", 36 | left: 0, 37 | right: 0, 38 | top: 0, 39 | bottom: 0, 40 | width: "100%", 41 | height: "100%", 42 | backgroundColor: "white", 43 | zIndex: 5000, 44 | }, 45 | paper: { 46 | marginTop: theme.spacing.unit * 8, 47 | display: 'flex', 48 | flexDirection: 'column', 49 | alignItems: 'center', 50 | padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`, 51 | }, 52 | avatar: { 53 | margin: theme.spacing.unit, 54 | backgroundColor: theme.palette.secondary.main, 55 | }, 56 | form: { 57 | width: '100%', // Fix IE 11 issue. 58 | marginTop: theme.spacing.unit, 59 | }, 60 | button: { 61 | margin: theme.spacing.unit, 62 | }, 63 | submit: { 64 | marginTop: theme.spacing.unit * 3, 65 | }, 66 | }); 67 | 68 | type State = { 69 | email: string, 70 | name: string, 71 | password: string 72 | mode: string 73 | }; 74 | 75 | interface Props { 76 | store: any; 77 | }; 78 | 79 | class SignIn extends React.Component, State> { 80 | state = { name: "", email: "", password: "", mode: "login" }; 81 | 82 | doSignIn = (event) => { 83 | const userAction = async () => { 84 | const response = await fetch('/api/user/login', { 85 | method: 'POST', 86 | body: JSON.stringify({email: this.state.email, password: this.state.password }), // string or object 87 | headers:{ 88 | 'Content-Type': 'application/json' 89 | } 90 | }); 91 | const json = await response.json(); //extract JSON from the http response 92 | console.log("ret", json) 93 | window.localStorage.setItem('authToken', json.authToken) 94 | window.localStorage.setItem('refreshToken', json.refreshToken) 95 | this.props.store.socketStore.connect(new TasktickSocket(window.localStorage.getItem("authToken"))) 96 | const { location, push, goBack } = this.props.store.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 97 | push("/p/projects") 98 | } 99 | userAction() 100 | event.preventDefault(); 101 | } 102 | doRegister = (event) => { 103 | const userAction = async () => { 104 | const response = await fetch('/api/user/register', { 105 | method: 'POST', 106 | body: JSON.stringify({email: this.state.email, password: this.state.password, firstName: this.state.name, lastName: this.state.name }), // string or object 107 | headers:{ 108 | 'Content-Type': 'application/json' 109 | } 110 | }); 111 | const json = await response.json(); //extract JSON from the http response 112 | console.log("ret", json) 113 | window.localStorage.setItem('authToken', json.authToken) 114 | window.localStorage.setItem('refreshToken', json.refreshToken) 115 | this.setState({mode: "login"}) 116 | const { location, push, goBack } = this.props.store.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 117 | push("/p/projects") 118 | } 119 | userAction() 120 | event.preventDefault(); 121 | } 122 | handlePasswordChange = (event) => { 123 | this.setState({ password: event.target.value }); 124 | }; 125 | handleEmailChange = (event) => { 126 | this.setState({ email: event.target.value }); 127 | }; 128 | handleNameChange = (event) => { 129 | this.setState({ name: event.target.value }); 130 | }; 131 | setMode = (mode: string) => () => { 132 | this.setState({mode}) 133 | } 134 | 135 | render() { 136 | const classes = this.props.classes; 137 | return ( 138 |
139 |
140 | {this.state.mode == "login" ? 141 | 142 | 143 | 144 | 145 | Sign in 146 | 147 |
148 | 149 | Email Address 150 | 151 | 152 | 153 | Password 154 | 161 | 162 | } label="Remember me" /> 163 | 173 | 174 | 175 |
176 | : 177 | 178 | 179 | 180 | 181 | Register 182 | 183 |
184 | 185 | Full Name 186 | 187 | 188 | 189 | Email Address 190 | 191 | 192 | 193 | Password 194 | 201 | 202 | 203 | Confirm 204 | 210 | 211 | } label="Remember me" /> 212 | 222 | 223 | 224 |
225 | } 226 |
227 |
228 | ); 229 | } 230 | } 231 | 232 | export default withRoot(withStyles(styles)(SignIn)); 233 | -------------------------------------------------------------------------------- /tasktick-pwa/src/client/pages/Users.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {observer} from 'mobx-react'; 3 | import { Theme } from '@material-ui/core/styles/createMuiTheme'; 4 | import createStyles from '@material-ui/core/styles/createStyles'; 5 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import withRoot from '../withRoot'; 8 | import ProjectCard from '../components/ProjectCard'; 9 | import { Task, Project, uuidv4 } from '../stores/data'; 10 | import TaskCard from '../components/TaskCard'; 11 | import { Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Button, Card, CardActionArea, CardMedia, CardContent, Typography, CardActions } from '@material-ui/core'; 12 | import AddIcon from '@material-ui/icons/Add'; 13 | import { Fab } from '@material-ui/core'; 14 | 15 | const styles = (theme: Theme) => 16 | createStyles({ 17 | root: { 18 | 19 | }, 20 | tableContainer: { 21 | height: 320, 22 | }, 23 | paper: { 24 | padding: theme.spacing.unit * 2, 25 | textAlign: 'center', 26 | color: theme.palette.text.secondary, 27 | }, 28 | fab: { 29 | margin: theme.spacing.unit, 30 | position: "absolute", 31 | right: 0, 32 | bottom: 0, 33 | }, 34 | card: { 35 | maxWidth: 345, 36 | }, 37 | media: { 38 | height: 140, 39 | }, 40 | }); 41 | 42 | type State = { 43 | 44 | }; 45 | 46 | interface Props { 47 | store: any; 48 | }; 49 | 50 | @observer 51 | class Users extends React.Component, State> { 52 | state = { 53 | 54 | }; 55 | 56 | render() { 57 | const { location, push, goBack } = this.props.store.routing; // CA - inject above did not work.. you should see this as a "prop" (investigate) 58 | const pathname = location.pathname.split('/') 59 | const classes = this.props.classes; 60 | return (
61 | 62 | 63 | 64 | 65 | 66 | 67 |
); 68 | } 69 | } 70 | 71 | export default withRoot(withStyles(styles)(Users)); -------------------------------------------------------------------------------- /tasktick-pwa/src/client/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tasktick-pwa/src/client/socket/WebSocket.tsx: -------------------------------------------------------------------------------- 1 | import {SocketEvent, toDateTime, Project, User, Task, Note} from '../stores/data'; 2 | import stores from '../stores' 3 | 4 | const WS_OPEN = 1 5 | 6 | const typeMap = { 7 | "io.surfkit.gateway.api.ProjectList" : (e: SocketEvent) =>{ 8 | e.payload.projects.forEach( x => { 9 | const project: Project = { 10 | id: x.id, 11 | name: x.name, 12 | owner: x.owner, 13 | team: x.team, 14 | description: x.description, 15 | imgUrl: x.imgUrl, 16 | tasks: x.tasks.map(y => y.id) 17 | } 18 | stores.projectStore.addProject(project) 19 | x.tasks.forEach(y => { 20 | stores.taskStore.addTask(y as Task) 21 | stores.projectStore.addTask(y as Task) 22 | }) 23 | }) 24 | }, 25 | "io.surfkit.gateway.api.ProjectRefList" : (e: SocketEvent) =>{ 26 | e.payload.projects.forEach( x => { 27 | const project: Project = { 28 | id: x.id, 29 | name: x.name, 30 | owner: "", 31 | team: "", 32 | description: "", 33 | tasks: [] 34 | } 35 | stores.projectStore.addProject(project) 36 | }) 37 | }, 38 | "io.surfkit.gateway.api.UserList" : (e: SocketEvent) =>{ 39 | e.payload.users.forEach( x => { 40 | stores.userStore.addUser(x as User) 41 | }) 42 | }, 43 | "io.surfkit.gateway.api.TaskList" : (e: SocketEvent) =>{ 44 | e.payload.tasks.forEach( x => { 45 | stores.taskStore.addTask(x as Task) 46 | stores.projectStore.addTask(x as Task) 47 | }) 48 | }, 49 | "io.surfkit.gateway.api.NoteList" : (e: SocketEvent) =>{ 50 | e.payload.notes.forEach( x => { 51 | stores.taskStore.addNote(x as Note) 52 | }) 53 | }, 54 | } 55 | 56 | /* 57 | RECEIVED: {"payload":{"msg":"pong","_type":"io.surfkit.gateway.api.Test"}} 58 | */ 59 | 60 | export class TasktickSocket{ 61 | 62 | ws: WebSocket = null 63 | cancelable: any = null 64 | queue = [] 65 | 66 | constructor(token: string){ 67 | console.log("RARG !!") 68 | this.ws = new WebSocket("ws://localhost:9000/ws/stream/"+token) 69 | this.ws.onopen = (event) => { 70 | console.log("WEBSOCKET IS CONNECTED3 !!!") 71 | const getUser = JSON.stringify({payload:{ts: new Date().getTime(), _type: 'io.surfkit.gateway.api.GetUser'}}) 72 | console.log("sending getUser", getUser) 73 | this.ws.send(getUser) 74 | const GetProjects = JSON.stringify({payload:{skip: 0, take: 50, _type: 'io.surfkit.gateway.api.GetProjects'}}) 75 | console.log("sending GetProjects", GetProjects) 76 | this.ws.send(GetProjects) 77 | while(this.queue.length != 0){ 78 | const data = this.queue.shift() 79 | this.send(data.eventType, data.payload) 80 | } 81 | } 82 | this.ws.onmessage = async (event) => { 83 | console.log("got a WS message", event) 84 | const socketEvent = JSON.parse(event.data) as SocketEvent 85 | console.log("ws socketEvent["+socketEvent.payload._type+"]", socketEvent) 86 | if(typeMap[socketEvent.payload._type]){ 87 | typeMap[socketEvent.payload._type](socketEvent) 88 | } 89 | } 90 | this.ws.onclose = (event) => { 91 | console.error("WebSocket is closed now. !!!!!!!!!!!!!!!!!!!"); 92 | if(this.cancelable){ 93 | clearInterval(this.cancelable); 94 | this.cancelable = null; 95 | } 96 | }; 97 | this.cancelable = setInterval(() =>{ 98 | console.log("lub dub..") 99 | this.ws.send(JSON.stringify( {payload:{ts: new Date().getTime(), _type: 'io.surfkit.gateway.api.HeartBeat'}} ) ) 100 | }, 25 * 1000) // every 25 seconds send a heart-beat to keep alive 101 | } 102 | 103 | send(eventType: string, payload: any){ 104 | if(this.ws.readyState != WS_OPEN){ 105 | this.queue.push({eventType, payload}) 106 | }else{ 107 | const socketEvent = this.mkSocketEvent(eventType, payload) 108 | console.log("SEND EVENT: " + eventType, socketEvent) 109 | this.ws.send(JSON.stringify(socketEvent)) 110 | } 111 | } 112 | close = () =>{ 113 | if(this.ws) 114 | this.ws.close(); 115 | } 116 | 117 | mkSocketEvent = (eventType: string, payload: any) => ( 118 | { 119 | payload: {...payload, _type: "io.surfkit.gateway.api."+eventType} 120 | } as SocketEvent 121 | ) 122 | } 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /tasktick-pwa/src/client/stores/data.tsx: -------------------------------------------------------------------------------- 1 | 2 | // data time format "yyyy-MM-dd'T'HH:mm:ss.SSS" Z with tz 3 | export const toDateTime = (date: Date): string => 4 | date.getUTCFullYear() + "-" + (date.getUTCMonth()+1) + "-" + date.getUTCDate() + "T" + date.getUTCHours() + ":" + date.getUTCMinutes() + ":" + date.getUTCSeconds() + "." + date.getMilliseconds() + "Z" 5 | 6 | 7 | export type uuid = string 8 | 9 | 10 | export interface SocketEvent{ 11 | payload: any; 12 | } 13 | 14 | export interface User{ 15 | id: uuid, 16 | firstName: string 17 | lastName: string 18 | email: string 19 | } 20 | 21 | export interface Note{ 22 | id: uuid, 23 | user: uuid, 24 | note: string, 25 | date: number 26 | project: uuid 27 | task: uuid 28 | } 29 | 30 | export interface Task{ 31 | id: uuid; 32 | project: uuid; 33 | name: string; 34 | description: string, 35 | done: boolean, 36 | assigned?: uuid, 37 | startDate?: number, 38 | endDate?: number, 39 | lastUpdated: number, 40 | section: string, 41 | parent?: uuid, 42 | notes: Note[] 43 | } 44 | 45 | export interface Project{ 46 | id: uuid, 47 | name: string, 48 | owner: uuid, 49 | team: uuid, 50 | description: string, 51 | imgUrl?: string, 52 | tasks: uuid[] 53 | } 54 | 55 | export interface ServiceException{ 56 | message: string; 57 | stackTrace: string[]; 58 | extra: any 59 | } 60 | 61 | export const uuidv4 = () => { 62 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 63 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 64 | return v.toString(16); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /tasktick-pwa/src/client/stores/index.tsx: -------------------------------------------------------------------------------- 1 | import { RouterStore } from 'mobx-react-router'; 2 | import { Note, Project, Task, User} from './data' 3 | import { observable, computed } from 'mobx'; 4 | import { TasktickSocket } from '../socket/WebSocket'; 5 | 6 | 7 | const routingStore = new RouterStore(); 8 | 9 | class SocketStore{ 10 | socket: TasktickSocket; 11 | 12 | connect(tb: TasktickSocket){ 13 | this.socket = tb; 14 | } 15 | } 16 | 17 | class UserStore { 18 | @observable users: User[] = []; 19 | constructor() {} 20 | addUser(e: User) { 21 | this.users.push(e); 22 | } 23 | } 24 | 25 | class ProjectStore { 26 | @observable projects: Project[] = []; 27 | constructor() {} 28 | addProject(p: Project) { 29 | this.projects = [...this.projects.filter(x => x.id != p.id), p]; 30 | } 31 | addTask(t: Task){ 32 | const proj = this.projects.find( x => x.id == t.project) 33 | if(proj){ 34 | proj.tasks = [...proj.tasks.filter(x => x != t.id), t.id] 35 | } 36 | } 37 | } 38 | 39 | class TaskStore{ 40 | @observable tasks: { [id:string]:Task } = {}; 41 | addTask(t: Task) { 42 | this.tasks[t.id] = t 43 | } 44 | addNote(n: Note){ 45 | const t = this.tasks[n.task] 46 | if(t){ 47 | t.notes = [...t.notes.filter(x => x.id != n.id), n] 48 | } 49 | } 50 | } 51 | 52 | 53 | const socketStore = new SocketStore(); 54 | const taskStore = new TaskStore(); 55 | const projectStore = new ProjectStore(); 56 | const userStore = new UserStore(); 57 | 58 | const stores = { 59 | routing: routingStore, 60 | socketStore, 61 | taskStore, 62 | projectStore, 63 | userStore, 64 | clear(){ 65 | taskStore.tasks = {} 66 | projectStore.projects = [] 67 | } 68 | } 69 | 70 | export default stores; -------------------------------------------------------------------------------- /tasktick-pwa/src/client/withRoot.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; 3 | import purple from '@material-ui/core/colors/purple'; 4 | import green from '@material-ui/core/colors/green'; 5 | import CssBaseline from '@material-ui/core/CssBaseline'; 6 | 7 | // A theme with custom primary and secondary color. 8 | // It's optional. 9 | const theme = createMuiTheme({ 10 | palette: { 11 | primary: { 12 | main: "#265692", 13 | light: "", 14 | dark: "" 15 | }, 16 | secondary: green, 17 | }, 18 | typography: { 19 | useNextVariants: true, 20 | }, 21 | }); 22 | 23 | function withRoot

(Component: React.ComponentType

) { 24 | function WithRoot(props: P) { 25 | // MuiThemeProvider makes the theme available down the React tree 26 | // thanks to React context. 27 | return ( 28 | 29 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | return WithRoot; 37 | } 38 | 39 | export default withRoot; 40 | -------------------------------------------------------------------------------- /tasktick-pwa/src/server/main.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as compression from 'compression'; 3 | 4 | let app = express(); 5 | app.use(function(req, res, next) { 6 | console.log(req.url); 7 | next(); 8 | }) 9 | app.use(compression()); 10 | app.get("/t/**", (req,res) => { 11 | res.sendFile("index.html", { root: __dirname+"/../../www"}) 12 | }); 13 | app.get("/invite/**", (req,res) => { 14 | res.sendFile("index.html", { root: __dirname+"/../../www"}) 15 | }); 16 | app.get("/dev/**", (req,res) => { 17 | res.sendFile("developer.html", { root: __dirname+"/../../www"}) 18 | }); 19 | app.use(express.static(__dirname+"/../../www")); 20 | 21 | app.listen(3001); -------------------------------------------------------------------------------- /tasktick-pwa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": false, 6 | "noImplicitAny": false, 7 | "removeComments": true, 8 | "experimentalDecorators": true, 9 | "noLib": false, 10 | "jsx": "react", 11 | "outDir": "dist" 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "static" 16 | ] 17 | } 18 | 19 | -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg0.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg1.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg2.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg3.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg4.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg5.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg6.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg7.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg8.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bg9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bg9.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bga.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bgb.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bgc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bgc.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bgd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bgd.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bge.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/bgf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/bgf.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/logo.png -------------------------------------------------------------------------------- /tasktick-pwa/www/imgs/typebus-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyauger/tasktick/0419550dbe76ebec5f677bff7c057ca7dff2f88b/tasktick-pwa/www/imgs/typebus-logo.png -------------------------------------------------------------------------------- /tasktick-pwa/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TaskTick 5 | 6 | 7 | 8 | 9 |

10 | 11 | 12 | --------------------------------------------------------------------------------