Compare commits

..

No commits in common. "main" and "0.2.2" have entirely different histories.
main ... 0.2.2

49 changed files with 5548 additions and 8796 deletions

2
.gitignore vendored
View File

@ -1,7 +1,6 @@
.DS_Store
.terraform
node_modules
/api/deploy
/web/dist
/dist
@ -26,4 +25,3 @@ pnpm-debug.log*
.*.sw?
api/hff_entry_forms_api
api/test/runner

24
.lvimrc
View File

@ -1,24 +0,0 @@
" The below configuration depends on global versions of some of these tools.
" All tooling can be installed with the following:
"
" asdf local nodejs latest
" npm install -g typescript prettier eslint vue-tsc
"
let g:ale_fixers = {
\ '*': ['remove_trailing_lines', 'trim_whitespace'],
\ 'javascript': ['eslint', 'prettier'],
\ 'typescript': ['prettier'],
\ 'vue': ['prettier'],
\}
let g:ale_linters = {
\ 'markdown': [],
\ 'html': ['HTMLHint', 'proselint', 'write-good'],
\ 'rst': [],
\ 'typescript': ['typescript', 'eslint'],
\ 'javascript': ['eslint'],
\ 'vue': ['vuetsc', 'prettier'],
\}
let g:ale_fix_on_save = 1
"let g:ale_linters_explicit = 1

View File

@ -1,2 +0,0 @@
opentofu 1.8.1
nim 1.6.20

View File

@ -1,35 +1,32 @@
VERSION:=$(shell git describe --always)
TARGET_ENV ?= dev
build: api-image dist/hff-entry-forms-web.${TARGET_ENV}.tar.gz
build: dist/hff-entry-forms-api.tar.gz dist/hff-entry-forms-web.tar.gz
clean:
-rm -r dist
make -C api clean
make -C web clean
test:
(cd api && nimble unittest)
-rm -r web/dist
-docker container prune
-docker image prune
update-version:
operations/update-version.sh
api-image:
make -C api build-image
dist/hff-entry-forms-web-${VERSION}.${TARGET_ENV}.tar.gz:
dist/hff-entry-forms-web.tar.gz:
-mkdir dist
TARGET_ENV=${TARGET_ENV} make -C web build
tar czf dist/hff-entry-forms-web-${VERSION}.${TARGET_ENV}.tar.gz -C web/dist .
TARGET_ENV=$(TARGET_ENV) make -C web build
tar czf dist/hff-entry-forms-web-${VERSION}.tar.gz -C web/dist .
cp dist/hff-entry-forms-web-${VERSION}.tar.gz dist/hff-entry-forms-web.tar.gz
deploy-api:
TARGET_ENV=${TARGET_ENV} make -C api build-image push-image publish
make -C api build-image push-image
cd operations/terraform && terraform apply -target module.${TARGET_ENV}_env.aws_ecs_task_definition.hff_entry_forms_api -target module.${TARGET_ENV}_env.aws_ecs_service.hff_entry_forms_api
deploy-web: dist/hff-entry-forms-web-${VERSION}.${TARGET_ENV}.tar.gz
deploy-web: dist/hff-entry-forms-web.tar.gz
mkdir -p temp-deploy/hff-entry-forms-web-${VERSION}
tar xzf dist/hff-entry-forms-web-${VERSION}.${TARGET_ENV}.tar.gz -C temp-deploy/hff-entry-forms-web-${VERSION}
aws s3 sync temp-deploy/hff-entry-forms-web-${VERSION} s3://forms.hopefamilyfellowship.com/${TARGET_ENV}/webroot
tar xzf dist/hff-entry-forms-web-${VERSION}.tar.gz -C temp-deploy/hff-entry-forms-web-${VERSION}
aws s3 sync temp-deploy/hff-entry-forms-web-${VERSION} s3://forms.hopefamilyfellowship.com/$(TARGET_ENV)/webroot
TARGET_ENV=${TARGET_ENV} operations/invalidate-cdn-cache.sh
rm -r temp-deploy
deploy: test deploy-api deploy-web
deploy: deploy-api deploy-web

View File

@ -1,79 +0,0 @@
# HFF Entry Forms
## Local Development
### Web
```sh
cd web
npm install
make serve & # or run in a separate terminal/pane
make build # as-needed to rebuild (not using hot-reload)
```
### API
```sh
cd api
make serve
make serve-docker # alternatively
```
## Procedures
### Version Bump
1. Ensure you know the current deployed version and the current "marked
version." Check the current deployed version by hitting the live API:
curl https://forms-api.hopefamilyfellowship.com/v1/version
and by querying ECR:
aws ecr describe-images --repository-name 063932952339.dkr.ecr.us-west-2.amazonaws.com/hff_entry_forms_api
Check the current "marked version" by looking at the latest git tag:
git tag -n
2. If necessary, bump the version using:
make update-version
This will invoke `operations/update-version.sh` which:
- updates `web/package.json`,
- updates `web/package-lock.json`,
- updates `api/src/hff_entry_forms_apipkg/version.nim`,
- updates `api/hff_entry_forms_api.nimble`,
- commits all of the above, and
- tags the commit with the new version.
#### Release Candidates (`-rcX` versions)
When we are preparing to release version a new version, we first create release
candidates. So, in preparation for releasing, for example, version 1.2.4, we
do the following:
```sh
# Development is concluded, ready to release 1.2.4
$ make update-version
Last Version: 1.2.3
New Version: 1.2.4-rc1
$ TARGET_ENV=dev make deploy
# verify successful build and deployment
# validate the release in the dev environment
$ make update-version
Last Version: 1.2.4-rc1
New Version: 1.2.4
$ TARGET_ENV=dev make deploy
# verify successful build and deployment
$ TARGET_ENV=prod make deploy
# verify successful build and deployment
# validate the release in the prod environment
```

View File

@ -1,11 +1,12 @@
FROM 063932952339.dkr.ecr.us-west-2.amazonaws.com/alpine-nim:nim-1.6.10 AS build
FROM 063932952339.dkr.ecr.us-west-2.amazonaws.com/alpine-nim:nim-1.4.8 AS build
MAINTAINER jonathan@jdbernard.com
COPY hff_entry_forms_api.nimble /hff_entry_forms_api/
COPY src /hff_entry_forms_api/src
WORKDIR /hff_entry_forms_api
RUN nimble build -y --passC:-fpermissive
RUN nimble build -y
FROM alpine:3.16.4
FROM alpine
EXPOSE 80
RUN apk -v --update add --no-cache \
ca-certificates \

View File

@ -4,16 +4,7 @@ SOURCES=$(wildcard src/*.nim) $(wildcard src/hff_entry_forms_apipkg/*.nim)
ECR_ACCOUNT_URL ?= 063932952339.dkr.ecr.us-west-2.amazonaws.com
# The port on the host machine (not the container)
ifeq ($(TARGET_ENV),prod)
PORT=6006
else
PORT=6005
endif
# The server to target when publishing the API
TARGET_SERVER ?= sobeck.jdb-software.com
TARGET_ENV ?= local
PORT ?= 8300
# The Notion integration token.
AUTH_SECRET ?= 123abc
@ -78,9 +69,6 @@ serve-docker: build-image
# Utility
# -------
clean:
-docker image rm $(ECR_ACCOUNT_URL)/hff_entry_forms_api:$(VERSION)
# Authenticate docker to the AWS private elastic container repository.
ecr-auth:
aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin $(ECR_ACCOUNT_URL)
@ -91,20 +79,3 @@ echo-vars:
"VERSION=$(VERSION)\n" \
"PORT=$(PORT)\n" \
"INTEGRATION_TOKEN=$(INTEGRATION_TOKEN)\n"
publish:
-rm -r deploy
-mkdir deploy
m4 \
-D "HFF_ENTRY_FORMS_API_VERSION=$(VERSION)" \
-D "TARGET_ENV=$(TARGET_ENV)" \
-D "TARGET_PORT=$(PORT)" \
hff_entry_forms_api.service \
> deploy/hff_entry_forms_api.$(TARGET_ENV).service
-ssh deployer@$(TARGET_SERVER) "docker stop hff_entry_forms_api.$(TARGET_ENV).service && sudo systemctl stop hff_entry_forms_api.$(TARGET_ENV)"
ssh deployer@$(TARGET_SERVER) "aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin $(ECR_ACCOUNT_URL) && docker pull $(ECR_ACCOUNT_URL)/hff_entry_forms_api:$(VERSION)"
scp \
deploy/hff_entry_forms_api.$(TARGET_ENV).service \
deployer@$(TARGET_SERVER):/etc/systemd/system/hff_entry_forms_api.$(TARGET_ENV).service
ssh deployer@$(TARGET_SERVER) "sudo systemctl daemon-reload"
ssh deployer@$(TARGET_SERVER) "sudo systemctl start hff_entry_forms_api.$(TARGET_ENV)"

View File

@ -1,6 +1,6 @@
# Package
version = "0.3.3"
version = "0.2.2"
author = "Jonathan Bernard"
description = "Hope Family Fellowship entry forms."
license = "GPL-3.0-or-later"
@ -15,11 +15,7 @@ requires @["docopt", "jester"]
requires "https://git.jdb-software.com/jdb/nim-cli-utils.git >= 0.6.3"
requires "https://git.jdb-software.com/jdb/nim-time-utils.git >= 0.5.0"
requires "https://git.jdb-software.com/jdb/update-nim-package-version.git"
requires "https://git.jdb-software.com/hope-family-fellowship/notion-api-client.git"
requires "https://git.jdb-software.com/jdb/update-nim-package-version"
task updateVersion, "Update the version of this package.":
exec "update_nim_package_version hff_entry_forms_api 'src/hff_entry_forms_apipkg/version.nim'"
task unittest, "Runs the unit test suite.":
exec "nim c -r test/runner"
exec "update_nim_package_version hff_entry_forms_api 'src/hff_entry_forms_apipkg/version.nim'"

View File

@ -1,16 +0,0 @@
[Unit]
Description=HFF Entry Forms (TARGET_ENV)
After=network-online.target
Requires=docker.service
[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker rm %n
ExecStart=/usr/bin/docker run --rm -p TARGET_PORT:80 --name %n \
--env-file /etc/hff_entry_forms/TARGET_ENV.env \
063932952339.dkr.ecr.us-west-2.amazonaws.com/hff_entry_forms_api:HFF_ENTRY_FORMS_API_VERSION
ExecStop=/usr/bin/docker stop --name %n
[Install]
WantedBy=default.target

View File

@ -1,5 +1,4 @@
import std/[json, logging, os, sequtils, strutils, tables]
import cliutils, docopt
import cliutils, docopt, json, logging, sequtils, strutils, tables
import hff_entry_forms_apipkg/api
import hff_entry_forms_apipkg/version
@ -27,13 +26,12 @@ proc loadConfig(args: Table[string, docopt.Value]): HffEntryFormsApiConfig =
let cfg = CombinedConfig(docopt: args, json: json)
result = HffEntryFormsApiConfig(
debug: cfg.hasKey("debug") and cfg.getVal("debug") == "true",
debug: args["--debug"],
eventParentId: cfg.getVal("event-parent-id"),
integrationToken: cfg.getVal("integration-token"),
knownOrigins: cfg.getVal("known-origins")[1..^2].split(',').mapIt(it.strip[1..^2]),
knownOrigins: cfg.getVal("known-origins")[1..^2].split(',').mapIt(it[1..^2]),
notionApiBaseUrl: cfg.getVal("notion-api-base-url"),
notionVersion: cfg.getVal("notion-version"),
notionConfigDbId: cfg.getVal("notion-config-db-id"),
port: parseInt(cfg.getVal("port", "8300")))
when isMainModule:
@ -44,7 +42,7 @@ Usage:
Options:
-c, --config <cfgFile> Location of the config file to use (defaults to
-C, --config <cfgFile> Location of the config file to use (defaults to
hff_entry_forms_api.config.json)
-d, --debug Log debugging information.
@ -54,18 +52,17 @@ Options:
let consoleLogger = newConsoleLogger(
levelThreshold=lvlInfo,
fmtStr="$appname - $levelname: ",
useStderr=true)
fmtStr="$app - $levelname: ")
logging.addHandler(consoleLogger)
# Initialize our service context
let args = docopt(doc, version = HFF_ENTRY_FORMS_API_VERSION)
let cfg = loadConfig(args)
if cfg.debug:
if args["--debug"]:
consoleLogger.levelThreshold = lvlDebug
let cfg = loadConfig(args)
if args["serve"]: start(cfg)
except:

View File

@ -26,7 +26,7 @@ template jsonResp(code: HttpCode,
headersToSend: RawHeaders = @{:} ) =
## Immediately send a JSON response and stop processing the request.
let reqOrigin =
if headers(request).hasKey("Origin"): $(headers(request)["Origin"])
if request.headers.hasKey("Origin"): $(request.headers["Origin"])
else: ""
let corsHeaders =
@ -34,13 +34,11 @@ template jsonResp(code: HttpCode,
@{
"Access-Control-Allow-Origin": reqOrigin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": $(reqMethod(request)),
"Access-Control-Allow-Methods": $(request.reqMethod),
"Access-Control-Allow-Headers": "Authorization,X-CSRF-TOKEN"
}
else: @{:}
#debug "Request origin: $#\nKnown origins: $#\nAdding headers:\n$#" %
# [ reqOrigin, cfg.knownOrigins.join(" | "), $corsHeaders ]
halt(
code,
headersToSend & corsHeaders & @{
@ -83,7 +81,7 @@ template errorResp(err: ref ApiError): void =
template optionsResp(allowedMethods: seq[HttpMethod]) =
let reqOrigin =
if headers(request).hasKey("Origin"): $(headers(request)["Origin"])
if request.headers.hasKey("Origin"): $(request.headers["Origin"])
else: ""
let corsHeaders =

View File

@ -1,6 +1,4 @@
import std/json, std/times
import hff_notion_api_client/utils, timeutils
import json, times, timeutils
type
EventProposal* = object
@ -28,6 +26,14 @@ proc getOrFail(n: JsonNode, key: string): JsonNode =
proc parseIso8601(n: JsonNode, key: string): DateTime =
return parseIso8601(n.getOrFail(key).getStr)
proc textProp(value: string): JsonNode =
return %*[
{
"type": "text",
"text": { "content": value }
}
]
proc parseEventProposal*(n: JsonNode): EventProposal {.raises: [JsonParsingError].} =
try:
@ -43,37 +49,36 @@ proc parseEventProposal*(n: JsonNode): EventProposal {.raises: [JsonParsingError
except:
raise newException(JsonParsingError, "Invalid EventProposal: " & getCurrentExceptionMsg())
proc toPage*(ep: EventProposal): JsonNode =
proc asNotionPage*(ep: EventProposal): JsonNode =
result = %*{
"properties": {
"Event": makeTextProp("title", ep.name),
"Event": { "title": textProp(ep.name) },
"Date": { "date": { "start": formatIso8601(ep.date) } },
"Department": { "multi_select": [ { "name": ep.department } ] },
"Location": makeTextProp("rich_text", ep.location),
"Owner": makeTextProp("rich_text", ep.owner),
"State": { "select": { "name": "Proposed" } },
"Visibility": { "select": { "name": "Public" } }
"Location": { "rich_text": textProp(ep.location) },
"Owner": { "rich_text": textProp(ep.owner) },
"State": { "select": { "name": "Proposed" } }
},
"children": [
{
"object": "block",
"type": "heading_2",
"heading_2": makeTextProp("text", "Purpose")
"heading_2": { "text": textProp("Purpose") }
},
{
"object": "block",
"type": "paragraph",
"paragraph": makeTextProp("text", ep.purpose)
"paragraph": { "text": textProp(ep.purpose) }
},
{
"object": "block",
"type": "heading_2",
"heading_2": makeTextProp("text", "Description")
"heading_2": { "text": textProp("Description") }
},
{
"object": "block",
"type": "paragraph",
"paragraph": makeTextProp("text", ep.description)
"paragraph": { "text": textProp(ep.description) }
}
]
}

View File

@ -1,29 +1,28 @@
import std/[json, httpclient, logging, options, sequtils, strutils]
import hff_notion_api_client
import hff_notion_api_client/config
import json, logging, std/httpclient, sequtils, strutils
import ./models, ./service
var notionClient = none[NotionClient]()
proc getNotionClient(cfg: HffEntryFormsApiConfig): NotionClient =
if notionClient.isNone:
notionClient = some(initNotionClient(NotionClientConfig(
apiVersion: cfg.notionVersion,
apiBaseUrl: cfg.notionApiBaseUrl,
configDbId: cfg.notionConfigDbId,
integrationToken: cfg.integrationToken)))
return notionClient.get
proc makeHttpClient(cfg: HffEntryFormsApiConfig): HttpClient =
let headers = newHttpHeaders([
("Content-Type", "application/json"),
("Authorization", "Bearer " & cfg.integrationToken),
("Notion-Version", cfg.notionVersion)
], true)
debug $headers
return newHttpClient(headers = headers, )
proc getEventProposalConfig*(cfg: HffEntryFormsApiConfig): EventProposalConfig =
let notion = getNotionClient(cfg)
let http = makeHttpClient(cfg)
var bodyJson: JsonNode
try: bodyJson = notion.fetchDatabaseObject(cfg.eventParentId)
except:
let apiResp = http.get(cfg.notionApiBaseUrl & "/databases/" & cfg.eventParentId)
debug apiResp.status
if not apiResp.status.startsWith("2"):
debug apiResp.body
raiseApiError(Http500,
"unable to read event propsal configuration from notion API")
let bodyJson = parseJson(apiResp.body)
let departmentOptionsJson = bodyJson{
"properties", "Department", "multi_select", "options"}
@ -38,9 +37,13 @@ proc getEventProposalConfig*(cfg: HffEntryFormsApiConfig): EventProposalConfig =
)
proc createProposedEvent*(cfg: HffEntryFormsApiConfig, ep: EventProposal): bool =
let notion = getNotionClient(cfg)
let http = makeHttpClient(cfg)
let epNotionPage = ep.asNotionPage
epNotionPage["parent"] = %*{ "database_id": cfg.eventParentId }
try:
discard notion.createDbPage(cfg.eventParentId, ep)
return true
except: return false
let apiResp = http.post(cfg.notionApiBaseUrl & "/pages", $epNotionPage)
debug apiResp.status
if not apiResp.status.startsWith("2"): debug apiResp.body
return apiResp.status.startsWith("2")

View File

@ -12,7 +12,6 @@ type
knownOrigins*: seq[string]
notionApiBaseUrl*: string
notionVersion*: string
notionConfigDbId*: string
port*: int
proc newApiError*(parent: ref Exception = nil, respCode: HttpCode, respMsg: string, msg = ""): ref ApiError =

View File

@ -1 +1 @@
const HFF_ENTRY_FORMS_API_VERSION* = "0.3.3"
const HFF_ENTRY_FORMS_API_VERSION* = "0.2.2"

View File

@ -1,2 +0,0 @@
switch("path", "../src")
switch("verbosity", "0")

View File

@ -1,3 +0,0 @@
import std/unittest
import ./tmodels

View File

@ -1,20 +0,0 @@
import std/json, std/times, std/unittest
import hff_entry_forms_apipkg/models
suite "models":
test "toPage(EventProposal)":
let ep = EventProposal(
name: "Test Event",
description: "A test event.",
purpose: "Event example for unit testing.",
department: "Testing",
location: "Hope Family Fellowship",
owner: "Jonathan Bernard",
date: parse("2021-10-30", "YYYY-MM-dd"),
budgetInDollars: 56)
let expectedJson = """{"properties":{"Event":{"title":[{"type":"text","text":{"content":"Test Event"}}]},"Date":{"date":{"start":"2021-10-30T00:00:00-05:00"}},"Department":{"multi_select":[{"name":"Testing"}]},"Location":{"rich_text":[{"type":"text","text":{"content":"Hope Family Fellowship"}}]},"Owner":{"rich_text":[{"type":"text","text":{"content":"Jonathan Bernard"}}]},"State":{"select":{"name":"Proposed"}},"Visibility":{"select":{"name":"Public"}}},"children":[{"object":"block","type":"heading_2","heading_2":{"text":[{"type":"text","text":{"content":"Purpose"}}]}},{"object":"block","type":"paragraph","paragraph":{"text":[{"type":"text","text":{"content":"Event example for unit testing."}}]}},{"object":"block","type":"heading_2","heading_2":{"text":[{"type":"text","text":{"content":"Description"}}]}},{"object":"block","type":"paragraph","paragraph":{"text":[{"type":"text","text":{"content":"A test event."}}]}}]}"""
check $(ep.toPage) == expectedJson

View File

@ -2,12 +2,10 @@
## System Components
* DNS - managed by AWS
* API Server - hosted on `sobeck.jdb-software.com`
* DNS - hosted on GoDaddy
* API Server - hosted on the `ortis` ECS cluster at JDB Software
* API Loadbalancer - using the main load balancer for JDB Software
* Web App - Served by CloudFront from an S3 bucket managed by JDB Software
* Certificates - Manually created and validated for \*.HFF.com
* Notion Integration - defined in Notion, token provided via the environment
specific config files at `/etc/hff_entry_forms/{dev,prod}.env` and passed
into the docker container at runtime ad defined in the SystemD service file
(see `api/hff_entry_forms_api.service`)
* Notion Integration - defined in Notion, token provided via AWS secrets
manager to the API instance running on the ortis cluster.

View File

@ -1,36 +0,0 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/aws" {
version = "5.62.0"
hashes = [
"h1:DzXMlmL2hRPfACAbN1PUhnLDGY9Kl0vbrt05qSfGsxA=",
"zh:2cb519ce7f3cbcb88b2e93dd3b3424ad85a347fc0e7429661945da5df8a20fda",
"zh:2fc7ed911cceaa1652d1f4090eaa91e8463aba86873910bccf16601260379886",
"zh:395b32d157adeb92571a0efd230c73bbee01744782a50356fb16e8946bd63ffb",
"zh:43303d36af40a568cd40bd54dc9e8430e18c4a4d78682b459dca8c755c717a0c",
"zh:65b2c6e955deeeffb9d9cd4ed97e8c532a453ba690d0e3d88c740f9036bccc4d",
"zh:a9d09dc9daf33b16894ed7d192ceb4c402261da58cded503a3ffa1dd2373e3fb",
"zh:c5e9f8bc4397c2075b6dc62458be51b93322517affd760c161633d56b0b9a334",
"zh:db0921c091402179edd549f8aa4f12dce18aab09d4302e800c67d6ec6ff88a86",
"zh:e7d13f9c0891446d03c29e4fcd60de633f71bbf1bc9786fca47a0ee356ac979a",
"zh:f128a725dbdbd31b9ed8ea478782152339c9fab4d635485763c8da2a477fe3f6",
]
}
provider "registry.opentofu.org/hashicorp/external" {
version = "2.3.3"
hashes = [
"h1:bDJy8Mj5PMTEuxm6Wu9A9dATBL+mQDmHx8NnLzjvCcc=",
"zh:1ec36864a1872abdfd1c53ba3c6837407564ac0d86ab80bf4fdc87b41106fe68",
"zh:2117e0edbdc88f0d22fe02fe6b2cfbbbc5d5ce40f8f58e484d8d77d64dd7340f",
"zh:4bcfdacd8e2508c16e131de9072cecd359e0ade3b8c6798a049883f37a5872ea",
"zh:4da71bc601a37bf8b7413c142d43f5f28e97e531d4836ee8624f41b9fb62e250",
"zh:55b9eebac79a46f88db5615f1ee0ac4c3f9351caa4eb8542171ef5d87de60338",
"zh:74d64afaef190321f8ddf1c4a9c6489d6cf51098704a2456c1553406e8306328",
"zh:8a357e51a0ec69872fafc64da3c6a1039277d325255ef5a264b727d83995d18b",
"zh:aacd2e6c13fe19115d51cd28a40a28da017bb48c2e18dec4460d1c37506b1495",
"zh:e19c8bdf0e059341d008a50f9138c44009e9ebb3a8047a300e6bc63ed8af8ea0",
"zh:fafa9639d8b8402e35f3864c6cfb0762ec57cc365a8f383e2acf81105b1b9eea",
]
}

View File

@ -0,0 +1,70 @@
resource "aws_secretsmanager_secret" "hff_entry_forms_api" {
name = "${local.environment_name}-Config"
tags = { Environment = local.environment_name }
}
resource "aws_ecs_task_definition" "hff_entry_forms_api" {
family = local.environment_name
network_mode = "bridge"
requires_compatibilities = ["EC2"]
execution_role_arn = aws_iam_role.ecs_task.arn
# See https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html
container_definitions = jsonencode([
{
name = local.environment_name
image = "${var.ecr_repo.repository_url}:${data.external.git_describe.result.version}"
cpu = 128
memory = 128
memoryReservation = 32
environment = [
{
name = "PORT"
value = "80"
}
]
portMappings = [
{
protocol = "tcp"
containerPort = 80
}
]
secrets = [
{
name = "INTEGRATION_TOKEN"
description = "Connection string with user credentials."
valueFrom = "${aws_secretsmanager_secret.hff_entry_forms_api.arn}:integrationToken::"
},
{
name = "KNOWN_ORIGINS"
description = "Connection string with user credentials."
valueFrom = "${aws_secretsmanager_secret.hff_entry_forms_api.arn}:knownOrigins::"
}
]
}
])
tags = {
Name = local.api_domain_name
Environment = local.environment_name
}
}
resource "aws_ecs_service" "hff_entry_forms_api" {
name = local.environment_name
cluster = data.terraform_remote_state.jdbsoft.outputs.aws_ecs_cluster_ortis.id
task_definition = aws_ecs_task_definition.hff_entry_forms_api.arn
desired_count = 1
launch_type = "EC2"
load_balancer {
target_group_arn = aws_lb_target_group.hff_entry_forms_api.arn
container_name = local.environment_name
container_port = 80
}
tags = {
Name = local.api_domain_name
Environment = local.environment_name
}
}

View File

@ -0,0 +1,69 @@
resource "aws_iam_role" "ecs_task" {
name = "${local.environment_name}-EcsTaskRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
inline_policy {
name = "AllowSecretsAccessForHffEntryFormsApiTasks"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"kms:Decrypt"
]
Resource = [
aws_secretsmanager_secret.hff_entry_forms_api.arn
]
}
]
})
}
inline_policy {
name = "AllowAccessToEcrForHffEntryFormsApiTasks"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ecr:GetAuthorizationToken"
]
Resource = [ "*" ]
},
{
Effect = "Allow"
Action = [
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:DescribeImages",
"ecr:GetDownloadUrlForLayer"
]
Resource = [
var.ecr_repo.arn
]
}
]
})
}
tags = {
Name = "HffEntryForms-EcsTaskRole"
Environment = local.environment_name
}
}

View File

@ -1,6 +1,6 @@
resource "aws_lb_target_group" "hff_entry_forms_api" {
name = "${local.environment_name}-${substr(uuid(), 0, 2)}"
port = var.target_port
port = 80
protocol = "HTTP"
target_type = "instance"
vpc_id = data.terraform_remote_state.jdbsoft.outputs.aws_vpc_jdbsoft.id
@ -41,9 +41,3 @@ resource "aws_lb_listener_rule" "hff_entry_forms_api" {
Environment = local.environment_name
}
}
resource "aws_lb_target_group_attachment" "hff_entry_forms_api" {
target_group_arn = aws_lb_target_group.hff_entry_forms_api.arn
target_id = data.terraform_remote_state.jdbsoft.outputs.sobeck-instance-id
port = var.target_port
}

View File

@ -12,10 +12,6 @@ variable "ecr_repo" {
description = "ECR repository information."
}
variable "target_port" {
description = "The port the deployed service will listen on."
}
variable "api_certificate_arn" {
description = "ARN of the certificate to use for the API loadbalancer."
}

View File

@ -15,7 +15,6 @@ module "dev_env" {
artifact_bucket = aws_s3_bucket.hff_entry_forms
cloudfront_certificate_arn = var.cloudfront_certificate_arn
ecr_repo = aws_ecr_repository.hff_entry_forms_api
target_port = 6005
}
module "prod_env" {
@ -26,14 +25,11 @@ module "prod_env" {
artifact_bucket = aws_s3_bucket.hff_entry_forms
cloudfront_certificate_arn = var.cloudfront_certificate_arn
ecr_repo = aws_ecr_repository.hff_entry_forms_api
target_port = 6006
}
data "aws_iam_policy_document" "cloudfront_access_policy" {
source_policy_documents = [
module.dev_env.oai_access_policy.json,
module.prod_env.oai_access_policy.json
]
source_json = "${module.dev_env.oai_access_policy.json}"
override_json = "${module.prod_env.oai_access_policy.json}"
}
resource "aws_s3_bucket_policy" "hff_entry_forms" {

View File

@ -10,7 +10,7 @@ rootDir=$(git rev-parse --show-toplevel)
cd "$rootDir"
currentBranch=$(git rev-parse --abbrev-ref HEAD)
if [ "$currentBranch" != "main" ]; then
if [ "$currentBranch" != "develop" ]; then
printf "You are currently on the '%s' branch. Is this intended (yes/no)? " "$currentBranch"
read -r confirmation
@ -60,3 +60,4 @@ git commit -m "Update package version to ${newVersion}"
printf ">> Tagging commit.\n"
printf "git tag -m \"Version %s\" \"%s\"\n" "$newVersion" "$newVersion"
git tag -m "Version ${newVersion}" "${newVersion}"

View File

@ -3,6 +3,3 @@ build:
serve:
npm run serve
clean:
-rm -r dist

24
web/README.md Normal file
View File

@ -0,0 +1,24 @@
# hff-entry-form-web
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

13563
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "hff-entry-form-web",
"version": "0.3.3",
"version": "0.2.2",
"private": true,
"scripts": {
"serve": "npx servor dist",
@ -30,7 +30,6 @@
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.0.0",
"git-describe": "^4.1.0",
"prettier": "^2.2.1",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",

View File

@ -2,14 +2,9 @@
html { font-size: 16px; }
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
#app {
font-family: "Segoe UI", Helvetica, Arial, sans-serif;
box-sizing: border-box;
button, input, select, textarea {
font-size: inherit;
@ -19,7 +14,6 @@ html { font-size: 16px; }
button {
color: #1084AC;
cursor: pointer;
background-color: #1084AC0A;
border: solid thin #1084AC;
border-radius: 0.25em;
@ -43,26 +37,6 @@ html { font-size: 16px; }
}
&:disabled { opacity: 0.75; }
}
button.low-profile {
color: black;
background-color: unset;
border: none;
box-shadow: none;
font-weight: normal;
transition-property: color, font-weight;
transition-duration: 250ms;
&:not(:disabled) {
&:hover, &:focus {
color: #1084AC;
font-weight: bold;
background-color: unset;
box-shadow: none;
}
}
}
}
@ -92,20 +66,8 @@ html { font-size: 16px; }
@include forSize(mobile) {
body { margin: 0; }
#app #debug-information .mobile { display: inline-block; }
}
@include forSize(tablet) {
#app #debug-information .tablet { display: inline-block; }
}
@include forSize(desktop) {
#app #debug-information .desktop { display: inline-block; }
}
@include forSize(ultrawide) {
html { font-size: 24px; }
#app #debug-information .ultrawide { display: inline-block; }
}

View File

@ -1,6 +0,0 @@
import { defineComponent } from 'vue';
import DebugInfo from '@/components/DebugInfo.vue';
export default defineComponent({
components: { DebugInfo },
});

View File

@ -1,6 +1,4 @@
<template>
<router-view />
<DebugInfo />
</template>
<script lang="ts" src="./App.ts"></script>
<style lang="scss" src="./App.scss"></style>

View File

@ -1,23 +0,0 @@
#debug-information {
position: fixed;
left: 1em;
bottom: 1em;
opacity: 0.5;
font-size: 80%;
.toggle-content { display: none; }
.toggle-content.visible { display: block; }
.mobile, .tablet, .desktop, .ultrawide { display: none; }
ul, li {
margin: 0;
padding: 0;
list-style: none;
}
label {
display: inline-block;
width: 8em;
}
}

View File

@ -1,16 +0,0 @@
import { defineComponent, ref } from 'vue';
import EyeClosedIcon from '@/components/svg/EyeClosedIcon.vue';
import EyeOpenIcon from '@/components/svg/EyeOpenIcon.vue';
import VERSION from '@/version-info';
export default defineComponent({
name: 'DebugInfo',
components: { EyeOpenIcon, EyeClosedIcon },
setup: function DebugInfo() {
const showDebugInfo = ref(false);
const version = ref(VERSION);
return { showDebugInfo, version };
},
});

View File

@ -1,22 +0,0 @@
<template>
<div id="debug-information">
<div class="toggle-content" :class="{ visible: showDebugInfo }">
<ul>
<li class="version"><label>version:</label>{{ version.version }}</li>
<li class="hash"><label>build hash:</label>{{ version.hash }}</li>
<li class="raw"><label>build hash (raw):</label>{{ version.raw }}</li>
</ul>
<label>device size: </label>
<label class="mobile">mobile</label>
<label class="tablet">tablet</label>
<label class="desktop">desktop</label>
<label class="ultrawide">ultrawide</label>
</div>
<button class="low-profile" @click="showDebugInfo = !showDebugInfo">
<EyeOpenIcon v-if="showDebugInfo" />
<EyeClosedIcon v-if="!showDebugInfo" />
</button>
</div>
</template>
<script lang="ts" src="./DebugInfo.ts"></script>
<style scoped lang="scss" src="./DebugInfo.scss"></style>

View File

@ -1,16 +0,0 @@
<template>
<svg
width="1em"
height="1em"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.7649 6.07595C14.9991 6.22231 15.0703 6.53078 14.9239 6.76495C14.4849 7.46742 13.9632 8.10644 13.3702 8.66304L14.5712 9.86405C14.7664 10.0593 14.7664 10.3759 14.5712 10.5712C14.3759 10.7664 14.0593 10.7664 13.8641 10.5712L12.6011 9.30816C11.8049 9.90282 10.9089 10.3621 9.93374 10.651L10.383 12.3276C10.4544 12.5944 10.2961 12.8685 10.0294 12.94C9.76266 13.0115 9.4885 12.8532 9.41703 12.5864L8.95916 10.8775C8.48742 10.958 8.00035 10.9999 7.5 10.9999C6.99964 10.9999 6.51257 10.958 6.04082 10.8775L5.58299 12.5864C5.51153 12.8532 5.23737 13.0115 4.97063 12.94C4.7039 12.8685 4.5456 12.5944 4.61706 12.3277L5.06624 10.651C4.09111 10.3621 3.19503 9.90281 2.3989 9.30814L1.1359 10.5711C0.940638 10.7664 0.624058 10.7664 0.428797 10.5711C0.233537 10.3759 0.233537 10.0593 0.428797 9.86404L1.62982 8.66302C1.03682 8.10643 0.515113 7.46742 0.0760677 6.76495C-0.0702867 6.53078 0.000898544 6.22231 0.235064 6.07595C0.46923 5.9296 0.777703 6.00078 0.924057 6.23495C1.40354 7.00212 1.989 7.68056 2.66233 8.2427C2.67315 8.25096 2.6837 8.25971 2.69397 8.26897C4.00897 9.35527 5.65536 9.9999 7.5 9.9999C10.3078 9.9999 12.6563 8.50629 14.0759 6.23495C14.2223 6.00078 14.5308 5.9296 14.7649 6.07595Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -1,16 +0,0 @@
<template>
<svg
width="1em"
height="1em"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -1,17 +0,0 @@
export interface VersionInfo {
version: string;
hash: string;
raw: string;
}
const gitVersion: { hash: string; raw: string } = process.env
.VUE_APP_HFF_ENTRY_FORMS_GIT_HASH
? JSON.parse(process.env.VUE_APP_HFF_ENTRY_FORMS_GIT_HASH)
: { hash: 'missing', raw: 'missing' };
export const VERSION: VersionInfo = {
...gitVersion,
version: process.env.VUE_APP_HFF_ENTRY_FORMS_VERSION || 'unavailable',
};
export default VERSION;

View File

@ -1,22 +1,20 @@
@import "~@/styles/forSize.mixin";
#event-proposal {
margin: 2em auto;
header {
background-color: #FFFCFC;
border-bottom: thin solid #aaa;
margin: 0;
padding: 0.5em;
h1, h2 {
margin: 0;
margin-top: 0.5em;
margin-bottom: 0;
padding: 0;
}
h2 {
font-weight: normal;
margin-bottom: -0.30em;
}
}
&.success header { background-color: #38b00010; }
@ -42,6 +40,7 @@
margin: 0.5em;
display: flex;
span { width: 9em; }
input, textarea, select { flex-grow: 1; }
}
@ -86,21 +85,12 @@
@include forSize(mobile) {
#header-splash {
width: 100%;
margin: 0;
}
#event-proposal {
padding: 0 1em;
margin: 1em;
width: 100%;
label {
flex-wrap: wrap;
span { width: 100%; }
input, select, textarea {
margin: 0.125em 1em;
width: 100%;
}
}
}
}
@ -116,10 +106,7 @@
border: solid thin #bbb;
border-radius: 0.25em;
box-shadow: 0.25em 0.25em 0.75em #aaa;
margin: 2em auto;
width: 30em;
label span { width: 9em; }
}
.successes, .errors { width: 26em; }

View File

@ -1,4 +1,4 @@
import { defineComponent, ref } from 'vue';
import { defineComponent, Ref, ref } from 'vue';
import { logService } from '@jdbernard/logging';
import {
default as api,
@ -28,8 +28,8 @@ export default defineComponent({
props: {},
components: { CircleCheckIcon, CircleCrossIcon, HourGlassIcon, SpinnerIcon },
setup: function TheProposeEventView() {
const departments = ref<{ value: string; color: string }[]>([]);
const formState = ref<FormState>('loading');
const departments: Ref<{ value: string; color: string }[]> = ref([]);
const formState: Ref<FormState> = ref('loading');
setTimeout(async () => {
departments.value = (await api.getEventProposalConfig()).departments;
@ -62,14 +62,14 @@ export default defineComponent({
if (await api.proposeEvent(formVal.event)) {
formState.value = 'success';
successes.push(
`We've recorded the proposed details for ${formVal.event.name}.`,
`We've recorded the proposed details for ${formVal.event.name}.`
);
} else {
formState.value = 'error';
errors.push(
'We were unable to record the proposed details for ' +
formVal.event.name +
". Poke Jonathan and tell him it's broken.",
". Poke Jonathan and tell him it's broken."
);
}

View File

@ -18,11 +18,7 @@
</label>
<label>
<span>Date and time</span>
<input
type="datetime-local"
name="date"
v-model="formVal.event.date"
/>
<input type="date" name="date" v-model="formVal.event.date" />
</label>
<label>
<span>Department / Event Type</span>

View File

@ -1,7 +0,0 @@
const fs = require('fs');
process.env.VUE_APP_HFF_ENTRY_FORMS_VERSION = require('./package.json').version;
process.env.VUE_APP_HFF_ENTRY_FORMS_GIT_HASH = JSON.stringify(
require('git-describe').gitDescribeSync('.')
);