diff --git a/README.md b/README.md index 93aaaaa..549c02c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,10 @@ This repository contains the back end for the Zerobase smart tracing platform. R Any commands shown in this read-me are written in bash. If you are on Windows or use an alternate shell, like fish, please adjust the commands accordingly. -## Set up for the project +There are other README files throughout the project. They are located in the source folders that most closely align with the +readme contents. + +## Development Setup ### Kotlin The backend is written in Kotlin. While you can work on it in any editor, such as vim or VS Code, it is significantly easier to use an IDE. We recommend [IntelliJ](https://www.jetbrains.com/idea/download/index.html). @@ -26,11 +29,23 @@ managers, such as `brew` on macOS, for easy updating (recommended way) but can a * Follow these installation instructions [here](https://maven.apache.org/install.html) * Add the full path to your ~/.bash_profile, ~/.zshrc, or similar. - ### Docker -Docker is used to run a graph database while running the project locally. +Docker is used to run external services locally for front-end development or tests instead of requiring access to a cloud environment. +Some of the resources can be run without docker, but some don't really have local install options. Install docker manually by following +this [guide](https://www.docker.com/get-started) or via a package manager. + +## Running Locally for Front-end Development or Testing -* Install docker manually by following this [guide](https://www.docker.com/get-started) or via a package manager. +### Docker Compose +We have a docker compose file that will spin up all the pieces necessary and expose the API on your local machine. There are several +environment variables that can be used to configure it: + +* `DB_PORT`: Used if you want to connect to the gremlin server manually and play with the graph. +* `APP_VERSION`: By default, it will use latest. If you need to run a specific version, or one that isn't in DockerHub yet, set this to the version tag you want. +* `APP_PORT`: By default, it will be 9000. + +### Manually with Docker +#### Database - Gremlin * Pull the Gremlin server image ```sh $ docker pull tinkerpop/gremlin-server @@ -46,18 +61,35 @@ Docker is used to run a graph database while running the project locally. ``` By default, there are no credentials for the local install +#### Localstack - AWS Fakes +Follow their startup documentation: https://github.com/localstack/localstack. + +#### App +```sh +$ docker run -d --name=zerobase-api \ + -e WRITE_ENDPOINT= \ + -e DB_PORT= \ + -e AWS_SES_ENDPOINT= + zerobaseio/smart-tracing-api: +``` + +### Manually without Docker #### Run Gremlin without Docker If you're unable to use Docker to run Gremlin, there is an alternative available. The desktop version can be downloaded from [here](https://www.apache.org/dyn/closer.lua/tinkerpop/3.4.6/apache-tinkerpop-gremlin-server-3.4.6-bin.zip). The configuration instructions are found [here](http://tinkerpop.apache.org/docs/3.4.6/reference/#gremlin-server). The default configuration should suffice. -## Project +#### AWS SES +Follow the documentation for a non-Docker SES fake. Here's one: https://github.com/csi-lk/aws-ses-local + +#### Project After cloning the project there are two ways to deploy it locally: using an IDE or via the command line. By default, the app listens on port 9000. You can override that with an environment variable of `PORT` if you need to. The `local-config.yml` defaults to `localhost` -and `8182` for the database. Both can be overriden with environment variables, using `DB_HOST` and `DB_PORT` respectively. +and `8182` for the database. Both can be overriden with environment variables, using `WRITE_ENDPOINT` and `DB_PORT` respectively. + +**Running in an IDE** -### Running in an IDE The following directions use Intellij as the IDE, but the steps should be similar if you are using a different IDE. * If necessary, navigate to File/Project Structure. Update the SDK for the project to JDK11. @@ -78,7 +110,7 @@ set an environment variable of `DB_PORT` to the port your gremlin server is runn * Click `OK` and run the configuration you just made. -### Running from the command line. +**Running from the command line.** * From the project's root directory, build the project. ```sh $ mvn clean install @@ -93,8 +125,10 @@ set an environment variable of `DB_PORT` to the port your gremlin server is runn ```sh $ PORT=8888 java -jar target/smart-tracing-api.jar server target/classes/local-config.yml ``` + ### Debugging / Calling end points * Once you're running the project, you can double check if all is well by visiting `http://localhost:8081` in your browser. You shoudl see a simple page that exposes metrics and healthcheck results. You can also curl `http://localhost:8081/healthchecks` if you prefer the command line. * App endpoints are available at `http://localhost:9000` + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c31e4bb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.7" + +services: + + database: + image: tinkerpop/gremlin-server:latest + ports: + - ${DB_PORT:-8182} + + aws: + image: localstack/localstack:latest + environment: + - SERVICES=ses + - DEBUG=${AWS_DEBUG:-false} + + app: + image: zerobaseio/smart-tracing-api:${APP_VERSION:-latest} + ports: + - ${APP_PORT:-9000} + depends_on: + - database + - aws + environment: + - WRITE_ENDPOINT=database + - READ_ENDPOINT=database + - DB_PORT=8182 + - AWS_SES_ENDPOINT=http://aws:4579 + - PORT=${APP_PORT:-9000} + + diff --git a/examples/example-zerobase-qr.pdf b/examples/example-zerobase-qr.pdf new file mode 100644 index 0000000..9cf39c7 Binary files /dev/null and b/examples/example-zerobase-qr.pdf differ diff --git a/images/qr_logo_overlay.png b/images/qr_logo_overlay.png new file mode 100644 index 0000000..ae8fd74 Binary files /dev/null and b/images/qr_logo_overlay.png differ diff --git a/images/zerobase_screenshot_pdf.PNG b/images/zerobase_screenshot_pdf.PNG new file mode 100644 index 0000000..716354a Binary files /dev/null and b/images/zerobase_screenshot_pdf.PNG differ diff --git a/pdfs/zerobase-qr.pdf b/pdfs/zerobase-qr.pdf new file mode 100644 index 0000000..727a979 Binary files /dev/null and b/pdfs/zerobase-qr.pdf differ diff --git a/pom.xml b/pom.xml index 1a50daa..dfd4367 100644 --- a/pom.xml +++ b/pom.xml @@ -29,9 +29,9 @@ 1.3.70 io.zerobase.smarttracing.MainKt - 2.0.2 + 2.0.5 [3.4.6,4) - 1.8 + 11 @@ -220,7 +220,6 @@ jakarta.ws.rs jakarta.ws.rs-api - 2.1.6 org.apache.commons @@ -244,10 +243,6 @@ com.google.guava guava - - com.github.spullara.mustache.java - compiler - javax.mail mail @@ -264,9 +259,45 @@ jakarta.activation 1.2.1 + + org.thymeleaf + thymeleaf + 3.0.6.RELEASE + + + net.sf.jtidy + jtidy + r938 + compile + + + org.xhtmlrenderer + flying-saucer-pdf-openpdf + 9.1.20 + compile + + + com.google.zxing + core + 3.3.0 + + + com.google.zxing + javase + 3.3.0 + - + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.platform + junit-platform-runner + test + diff --git a/src/main/kotlin/io/zerobase/smarttracing/GraphDao.kt b/src/main/kotlin/io/zerobase/smarttracing/GraphDao.kt index f52d23b..3edd000 100644 --- a/src/main/kotlin/io/zerobase/smarttracing/GraphDao.kt +++ b/src/main/kotlin/io/zerobase/smarttracing/GraphDao.kt @@ -8,7 +8,6 @@ import io.zerobase.smarttracing.utils.LoggerDelegate import org.apache.tinkerpop.gremlin.process.traversal.Traversal import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource import org.apache.tinkerpop.gremlin.structure.T -import org.apache.tinkerpop.gremlin.structure.VertexProperty import java.util.* private fun Traversal.getIfPresent(): T? { diff --git a/src/main/kotlin/io/zerobase/smarttracing/Main.kt b/src/main/kotlin/io/zerobase/smarttracing/Main.kt index 539ecd4..cadd9a7 100644 --- a/src/main/kotlin/io/zerobase/smarttracing/Main.kt +++ b/src/main/kotlin/io/zerobase/smarttracing/Main.kt @@ -9,20 +9,24 @@ import io.dropwizard.configuration.SubstitutingSourceProvider import io.dropwizard.setup.Bootstrap import io.dropwizard.setup.Environment import io.zerobase.smarttracing.config.GraphDatabaseFactory +import io.zerobase.smarttracing.notifications.AmazonEmailSender +import io.zerobase.smarttracing.notifications.NotificationFactory +import io.zerobase.smarttracing.notifications.NotificationManager +import io.zerobase.smarttracing.pdf.DocumentFactory import io.zerobase.smarttracing.resources.* import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource import org.eclipse.jetty.servlets.CrossOriginFilter -import java.util.* -import javax.servlet.DispatcherType -import javax.servlet.FilterRegistration +import org.thymeleaf.TemplateEngine +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver +import org.w3c.tidy.Tidy import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.ses.SesClient -import com.github.mustachejava.DefaultMustacheFactory -import io.zerobase.smarttracing.notifications.AmazonEmailSender -import io.zerobase.smarttracing.notifications.NotificationFactory -import io.zerobase.smarttracing.notifications.NotificationManager import java.net.URI +import java.nio.charset.StandardCharsets +import java.util.* import javax.mail.Session +import javax.servlet.DispatcherType +import javax.servlet.FilterRegistration typealias MultiMap = Map> @@ -66,7 +70,28 @@ class Main: Application() { config.aws.ses.endpoint?.let(sesClientBuilder::endpointOverride) val emailSender = AmazonEmailSender(sesClientBuilder.build(), session, config.notifications.email.fromAddress) val notificationManager = NotificationManager(emailSender) - val notificationFactory = NotificationFactory(DefaultMustacheFactory(config.notifications.templateLocation)) + val notificationFactory = NotificationFactory(TemplateEngine().apply { + templateResolvers = setOf(ClassLoaderTemplateResolver().apply { + prefix = "/notifications" + suffix = ".html" + characterEncoding = StandardCharsets.UTF_8.displayName() + }) + }) + + val resolver = ClassLoaderTemplateResolver().apply { + prefix = "/pdfs" + suffix = ".html" + characterEncoding = StandardCharsets.UTF_8.displayName() + } + val templateEngine = TemplateEngine().apply { + templateResolvers = setOf(resolver) + } + + val documentFactory = DocumentFactory(templateEngine, Tidy().apply { + inputEncoding = StandardCharsets.UTF_8.displayName() + outputEncoding = StandardCharsets.UTF_8.displayName() + xhtml = true + }) val dao = GraphDao(graph, phoneUtil) diff --git a/src/main/kotlin/io/zerobase/smarttracing/notifications/AmazonEmailSender.kt b/src/main/kotlin/io/zerobase/smarttracing/notifications/AmazonEmailSender.kt index ff1f7fc..5ce21e1 100644 --- a/src/main/kotlin/io/zerobase/smarttracing/notifications/AmazonEmailSender.kt +++ b/src/main/kotlin/io/zerobase/smarttracing/notifications/AmazonEmailSender.kt @@ -1,19 +1,19 @@ package io.zerobase.smarttracing.notifications +import software.amazon.awssdk.core.SdkBytes +import software.amazon.awssdk.services.ses.SesClient +import software.amazon.awssdk.services.ses.model.RawMessage +import software.amazon.awssdk.services.ses.model.SendRawEmailRequest +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets +import javax.activation.DataHandler import javax.mail.Message import javax.mail.Session import javax.mail.internet.InternetAddress +import javax.mail.internet.MimeBodyPart import javax.mail.internet.MimeMessage import javax.mail.internet.MimeMultipart -import javax.mail.internet.MimeBodyPart import javax.mail.util.ByteArrayDataSource -import java.io.ByteArrayOutputStream -import javax.activation.DataHandler - -import software.amazon.awssdk.services.ses.SesClient -import software.amazon.awssdk.core.SdkBytes -import software.amazon.awssdk.services.ses.model.* -import java.nio.charset.StandardCharsets class AmazonEmailSender( private val client: SesClient, diff --git a/src/main/kotlin/io/zerobase/smarttracing/notifications/Notifications.kt b/src/main/kotlin/io/zerobase/smarttracing/notifications/Notifications.kt index dc4a41f..2b9c94b 100644 --- a/src/main/kotlin/io/zerobase/smarttracing/notifications/Notifications.kt +++ b/src/main/kotlin/io/zerobase/smarttracing/notifications/Notifications.kt @@ -1,14 +1,14 @@ package io.zerobase.smarttracing.notifications -import com.github.mustachejava.MustacheFactory import com.google.common.net.MediaType import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import org.thymeleaf.TemplateEngine enum class NotificationMedium { EMAIL } -class NotificationFactory(private val templateFactory: MustacheFactory) { +class NotificationFactory(private val templateEngine: TemplateEngine) { } diff --git a/src/main/kotlin/io/zerobase/smarttracing/notifications/README.md b/src/main/kotlin/io/zerobase/smarttracing/notifications/README.md new file mode 100644 index 0000000..322227f --- /dev/null +++ b/src/main/kotlin/io/zerobase/smarttracing/notifications/README.md @@ -0,0 +1,26 @@ +## Notifications +We are (or will be) sending a variety of notifications through (eventually) a variety of mediums. It's just email for now, +but SMS is on the road map, and we may have other options as well in the future like push notifications and phone calls. + +This module has been designed to abstract the way the notifications are sent and the differing content as much as possible +from the calling site. + +For notifications that require templates, we are standardizing on Thymeleaf as our template engine. + +### Components + +#### Notification +This is the sealed class that provides the API for triggering rendering. Each sub-class handles the actual rendering of its content. +We are using a sealed class instead of an interface to force all implementations to be co-located. + +#### Notification Factory +To avoid passing around a template engine instance to all the places that want to create a notification, we +are hiding it behind a factory class. Each notification sub-class should have a constructor method on the factory and it's only +constructor should be marked as `internal` to prevent direct construction. + +#### Notification Manager +This is the entry point for sending notifications. It accepts an instance of `Notification` and contact info. It will decide +what medium should be used for communication - for example, if no phone number is available for SMS, it will use email. The +notification will be asked to render itself to produce a string. + +Once the notification has been rendered, individual providers of communication mediums will be invoked to transmit the notification. diff --git a/src/main/kotlin/io/zerobase/smarttracing/pdf/Documents.kt b/src/main/kotlin/io/zerobase/smarttracing/pdf/Documents.kt new file mode 100644 index 0000000..ea98e52 --- /dev/null +++ b/src/main/kotlin/io/zerobase/smarttracing/pdf/Documents.kt @@ -0,0 +1,62 @@ +package io.zerobase.smarttracing.pdf + +import com.google.common.io.Resources +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import io.zerobase.smarttracing.utils.LoggerDelegate +import org.thymeleaf.TemplateEngine +import org.thymeleaf.context.Context +import org.w3c.tidy.Tidy +import org.xhtmlrenderer.pdf.ITextRenderer +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.net.URL +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.util.* + +class DocumentFactory(private val templateEngine: TemplateEngine, private val xhtmlConverter: Tidy) { + fun siteOnboarding(qrCode: ByteArray): SiteOnboarding = SiteOnboarding(qrCode, templateEngine, xhtmlConverter) +} + +sealed class Document(private val templateEngine: TemplateEngine, private val xhtmlConverter: Tidy) { + companion object { + val log by LoggerDelegate() + + } + + protected abstract val templateLocation: URL + protected abstract val context: Context + + fun render(): ByteArray { + log.debug("rendering document: {}", this::class) + val html = templateEngine.process("${templateLocation}/main", context) + val xhtml = convertToXhtml(html) + log.trace("rendered html. converting to pdf...") + val renderer = ITextRenderer().apply { + setDocumentFromString(xhtml, templateLocation.toString()) + } + renderer.layout() + val byteOutputStream = ByteArrayOutputStream() + byteOutputStream.use(renderer::createPDF) + log.debug("pdf created successfully") + return byteOutputStream.toByteArray() + } + + private fun convertToXhtml(html: String): String { + val inputStream = ByteArrayInputStream(html.toByteArray(StandardCharsets.UTF_8)) + val outputStream = ByteArrayOutputStream() + xhtmlConverter.parseDOM(inputStream, outputStream) + return outputStream.toString(StandardCharsets.UTF_8) + } +} + +@SuppressFBWarnings("EI_EXPOSE_REP2") +class SiteOnboarding internal constructor(private val qrCode: ByteArray, templateEngine: TemplateEngine, xhtmlConverter: Tidy) + : Document(templateEngine, xhtmlConverter) { + override val templateLocation: URL = Resources.getResource("qr-code") + override val context: Context by lazy { + val tempPath = Files.createTempFile("zb-", ".png") + Files.write(tempPath, qrCode) + Context(Locale.US, mapOf("qrCode" to tempPath.toString())) + } +} diff --git a/src/main/kotlin/io/zerobase/smarttracing/pdf/README.md b/src/main/kotlin/io/zerobase/smarttracing/pdf/README.md new file mode 100644 index 0000000..d80a0d4 --- /dev/null +++ b/src/main/kotlin/io/zerobase/smarttracing/pdf/README.md @@ -0,0 +1,31 @@ +## PDF Generation + +The backend is generating PDFs for notifications (usually emailed). It's a multi-step process and depends on +some conventions. + +### The Process +1. A Thymeleaf HTML template gets rendered. +1. The HTML is converted to XHTML for consistency using Tidy. +1. The XHTML is rendered as a PDF using the LGPL version of iText. + +### The Conventions +* HTML templates are stored in `src/main/resources/pdfs` with a unique directory per template +* The root template must be named `main.html` + +### Code Structure +The Kotlin type for PDF generation abstraction is called `Document`. It's a sealed class, so new document +types need to be created in `src/main/kotlin/io/zerobase/smarttracing/pdf/Documents.kt`. This pattern allows each +sub-type to provide separate arguments and processing, while also requiring that all sub-types are located in +the `Documents.kt` + +Each sub-class of `Document` will need to override 2 properties: +* `val templateLocation: URL` - a Resource URL to the template-specific directory +* `val context: Context` - the data model for the template to render + +#### Constructing instances of `Document` subclasses +Since rendering the template as a PDF requires a Thymeleaf engine and a Tidy, we want to avoid passing those +objects into REST resources just for passing it down (see the Law of Demeter). As such, there is a +`DocumentFactory` class that has those instances and should be passed around to the places that need to create +instances of `Document`. Each sub-type should have a constructor function on the factory. + +To make sure that all instances are constructed through the factory, the constructor should be marked as `internal` diff --git a/src/main/kotlin/io/zerobase/smarttracing/qr/QRCodeGenerator.kt b/src/main/kotlin/io/zerobase/smarttracing/qr/QRCodeGenerator.kt new file mode 100644 index 0000000..718fcc4 --- /dev/null +++ b/src/main/kotlin/io/zerobase/smarttracing/qr/QRCodeGenerator.kt @@ -0,0 +1,68 @@ +package io.zerobase.smarttracing.qr + +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.client.j2se.MatrixToImageWriter +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import io.zerobase.smarttracing.utils.LoggerDelegate +import java.awt.AlphaComposite +import java.awt.Color +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.net.URL +import javax.imageio.ImageIO +import kotlin.math.roundToInt +import com.google.zxing.Writer as BarcodeWriter + + +class QRCodeGenerator(private val logo: URL, + private val writer: BarcodeWriter, + private val options: Map = mapOf(EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.H) +) { + companion object { + private val log by LoggerDelegate() + } + + private fun getQRCodeWithOverlay(qrcode: BufferedImage, overlay: BufferedImage): ByteArray { + log.debug("adding logo overlay to qr code...") + val deltaHeight = qrcode.height - overlay.height + val deltaWidth = qrcode.width - overlay.width + val combined = BufferedImage(qrcode.width, qrcode.height, BufferedImage.TYPE_INT_ARGB) + val g2 = combined.graphics as Graphics2D + g2.drawImage(qrcode, 0, 0, null) + val overlayTransparency = .9f + g2.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, overlayTransparency) + g2.drawImage(overlay, (deltaWidth / 2f).roundToInt(), (deltaHeight / 2f).roundToInt(), null) + val outputStream = ByteArrayOutputStream() + outputStream.use { + ImageIO.write(combined, "png", outputStream) + outputStream.flush() + } + return outputStream.toByteArray() + } + + private fun scale(image: BufferedImage, scaledWidth: Int, scaledHeight: Int): BufferedImage { + log.debug("scaling image from {}x{} to {}x{}", image.width, image.height, scaledWidth, scaledHeight) + val imageBuff = BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB) + val g: Graphics = imageBuff.createGraphics() + try{ + // 0 and 0 for i and i1 change the position of the scaled instance; to set transparent must add in alpha param for color + g.drawImage(image.getScaledInstance(scaledWidth, scaledHeight, BufferedImage.SCALE_SMOOTH), 0, 0, Color(0, 0, 0, 0), null) + return imageBuff + } finally { + g.dispose() + } + } + + fun generate(text: String, width: Int, height: Int): ByteArray { + val bitMatrix = writer.encode(text, BarcodeFormat.QR_CODE, width, height, options) + val image = MatrixToImageWriter.toBufferedImage(bitMatrix) + val overlay = ImageIO.read(logo) + val scaledWidth = (image.width * 2 / 9f).roundToInt() + val scaledHeight = (image.height * 2 / 9f).roundToInt() + + return getQRCodeWithOverlay(image, scale(overlay, scaledWidth, scaledHeight)) + } +} diff --git a/src/main/kotlin/io/zerobase/smarttracing/resources/OrganizationsResource.kt b/src/main/kotlin/io/zerobase/smarttracing/resources/OrganizationsResource.kt index d4387f5..cc15a70 100644 --- a/src/main/kotlin/io/zerobase/smarttracing/resources/OrganizationsResource.kt +++ b/src/main/kotlin/io/zerobase/smarttracing/resources/OrganizationsResource.kt @@ -6,11 +6,11 @@ import io.zerobase.smarttracing.models.IdWrapper import io.zerobase.smarttracing.models.Location import io.zerobase.smarttracing.models.OrganizationId import io.zerobase.smarttracing.models.SiteId +import io.zerobase.smarttracing.notifications.NotificationFactory +import io.zerobase.smarttracing.notifications.NotificationManager import javax.ws.rs.* import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response -import io.zerobase.smarttracing.notifications.NotificationFactory -import io.zerobase.smarttracing.notifications.NotificationManager /** * Requests from clients. diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 4c1d330..2cfd3c2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -27,8 +27,14 @@ database: enableAwsSigner: ${ENABLE_AWS_SIGNING:-true} enableSsl: ${ENABLE_SSL:-true} -appConfig: - fromEmail: ${FROM_EMAIL:-""} +aws: + ses: + region: ${REGION:-us-east-1} + endpoint: ${AWS_SES_ENDPOINT:-null} + +notifications: + email: + fromAddress: ${NOTIFICATION_FROM_EMAIL_ADDRESS:-noreply@zerobase.io} siteTypeCategories: BUSINESS: diff --git a/src/main/resources/local-config.yml b/src/main/resources/local-config.yml index 00743db..cc7c459 100644 --- a/src/main/resources/local-config.yml +++ b/src/main/resources/local-config.yml @@ -9,6 +9,15 @@ database: write: ${DB_HOST:-localhost} port: ${DB_PORT:-8182} +aws: + ses: + region: ${REGION:-us-east-1} + endpoint: ${FAKE_SES_ENDPOINT:-null} + +notifications: + email: + fromAddress: ${NOTIFICATION_FROM_EMAIL_ADDRESS:-noreply@zerobase.io} + siteTypeCategories: BUSINESS: - GROCERY diff --git a/src/main/resources/pdfs/qr-code/css/style.css b/src/main/resources/pdfs/qr-code/css/style.css new file mode 100644 index 0000000..b94cf60 --- /dev/null +++ b/src/main/resources/pdfs/qr-code/css/style.css @@ -0,0 +1,176 @@ +body { + font-family: sans-serif; + font-size: 12px; + text-align: center; + outline: 100px solid #1C6EA4; + outline-offset: -18px; + color: #1b2935; +} +h1 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 50px; + font-style: normal; + font-variant: normal; + font-weight: 700; + line-height: 45px; +} +h2 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 22px; + font-style: normal; + font-variant: normal; + font-weight: 200; + line-height: 26px; +} + +h3 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 18px; + font-style: normal; + font-variant: normal; + font-weight: 700; + line-height: 23px; +} +p { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 18px; + font-style: normal; + font-variant: normal; + font-weight: 400; + line-height: 23px; + margin-bottom: 0px; +} +li { + font-size: 18px; +} + +blockquote { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 21px; + font-style: normal; + font-variant: normal; + font-weight: 400; + line-height: 30px; +} + +pre { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-style: normal; + font-variant: normal; + font-weight: 400; + line-height: 18.5714px; +} +ol { + text-align: left; +} +@page { + + @bottom-left { + content: element(footer); + vertical-align: top; + padding-top: 10px; + } + + @top-right { + content: element(header); + vertical-align: bottom; + padding-bottom: 10px; + } + + margin-top: 3.3cm; + margin-left: 1cm; + margin-right: 1cm; + margin-bottom: 3.3cm; + + size: A4 portrait; +} + +div.header { + position: running(header); +} + +div.center { + margin-left: auto; + margin-right: auto; + display:table; +} + +div.footer { + display: block; + margin-top: 0.5cm; + position: running(footer); +} + +#pagenumber:before { + content: counter(page); +} + +#pagecount:before { + content: counter(pages); +} + +.logo-container { + text-align: right; +} + +.page-count { + text-align: right; +} + +.logo { + width: 200px; +} + +.footer { + text-align: center; +} + +.barcode { + font-family: "Bar-Code 39"; + font-size: 26px; +} + +.right { + text-align: right; +} +.center { + text-align: center; +} + +.left { + text-align: left; +} + +.bottom { + vertical-align: bottom; +} + +.address-block { + height: 100px; +} + +table { + width: 100%; +} + +hr { + background-color: #000000; + border: dashed #000000 0.5px; + height: 1px; +} + +.page-break { + page-break-after: always; +} + +.next-page { + page-break-before: always +} + +.qr-code { + display: block; + margin-left: auto; + margin-right: auto; + width: 70%; +} diff --git a/src/main/resources/pdfs/qr-code/footer.html b/src/main/resources/pdfs/qr-code/footer.html new file mode 100644 index 0000000..5938d8f --- /dev/null +++ b/src/main/resources/pdfs/qr-code/footer.html @@ -0,0 +1,16 @@ +
+
+
+
+ +
+
+ Privacy first. Anonymous. Accessible to all. +
+
+ Page of +
+
+ + +
diff --git a/src/main/resources/pdfs/qr-code/header.html b/src/main/resources/pdfs/qr-code/header.html new file mode 100644 index 0000000..b2c181d --- /dev/null +++ b/src/main/resources/pdfs/qr-code/header.html @@ -0,0 +1,3 @@ +
+

COVID-19 CHECK-IN

+
\ No newline at end of file diff --git a/src/main/resources/pdfs/qr-code/img/logo.png b/src/main/resources/pdfs/qr-code/img/logo.png new file mode 100644 index 0000000..4d20edc Binary files /dev/null and b/src/main/resources/pdfs/qr-code/img/logo.png differ diff --git a/src/main/resources/pdfs/qr-code/letterhead.html b/src/main/resources/pdfs/qr-code/letterhead.html new file mode 100644 index 0000000..96d4adf --- /dev/null +++ b/src/main/resources/pdfs/qr-code/letterhead.html @@ -0,0 +1,5 @@ +
+

Welcome to [[${data.getBusinessname()}]]! We are participating in [[${data.getTown()}]], [[${data.getState()}]]'s + anonymous contact tracing system to stop the spread of COVID-19.

+
+
\ No newline at end of file diff --git a/src/main/resources/pdfs/qr-code/main.html b/src/main/resources/pdfs/qr-code/main.html new file mode 100644 index 0000000..e379248 --- /dev/null +++ b/src/main/resources/pdfs/qr-code/main.html @@ -0,0 +1,49 @@ + + + + + + + + + +
+ +
+ +

Please take three seconds right now to help our community by checking in. + +

+

+ + The more community members participating, the more protected we all are. + +

+

You could save a life. +

+ + logo + +

To protect your privacy, we do not track any information related to your identity.

+ +

Please Check In

+
    +
  1. Open the camera on your smartphone
  2. +
  3. Point your phone at the QR code above & the message "Open zerobase.io" will appear
  4. +
  5. Click to open zerobase.io and you will see a "Scan Succeeded" message!
  6. +
  7. All done! You can also share your phone number to receive anonymous notification in case you might have been exposed.
  8. +
+ +

Thank you for doing your part in keeping [[${data.getTown()}]], [[${data.getState()}]] healthy!

+ + +
+ + +
+ + + + + + diff --git a/src/main/resources/qr/zerobase_qr_logo.png b/src/main/resources/qr/zerobase_qr_logo.png new file mode 100644 index 0000000..c60e159 Binary files /dev/null and b/src/main/resources/qr/zerobase_qr_logo.png differ