diff --git a/Dockerfile b/.devcontainer/Dockerfile similarity index 75% rename from Dockerfile rename to .devcontainer/Dockerfile index 2de86ce..7dad5a8 100644 --- a/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,14 @@ FROM mcr.microsoft.com/devcontainers/base:buster +ENV CATALYST_HOME="/workspaces/AccessSystem" +ENV PATH="${CATALYST_HOME}/local/bin:${PATH}" + +WORKDIR /workspaces/AccessSystem + +RUN groupadd --gid 1001 swmakers \ + && useradd --uid 1001 --gid swmakers --shell /bin/bash --create-home swmakers + RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends \ perl=5.28.1-6+deb10u1 \ @@ -32,6 +40,9 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ libtimedate-perl \ liburi-perl \ libscalar-list-utils-perl \ + # database support + libpq-dev \ + sqlite3 \ && cpanm -S Carton \ # for Perl::LanguageServer && apt-get -y install --no-install-recommends \ @@ -46,3 +57,12 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ libcoro-perl \ && cpanm Perl::LanguageServer \ && rm -rf /var/lib/apt/lists/* + +COPY cpanfile* "${CATALYST_HOME}/" +COPY vendor/ "${CATALYST_HOME}/vendor/" + +RUN chown -R swmakers:swmakers "${CATALYST_HOME}" + +USER swmakers + +RUN carton install --cached diff --git a/.devcontainer/config/accesssystem_api.conf.dev b/.devcontainer/config/accesssystem_api.conf.dev new file mode 100644 index 0000000..ddad03e --- /dev/null +++ b/.devcontainer/config/accesssystem_api.conf.dev @@ -0,0 +1,30 @@ +using_frontend-proxy 1 + + + site_key 6LdHZx0TAAAAAMuUdG-NScgCCbJ_HMyL1bdeM9vp + secret_key 6LdHZx0TAAAAAEAnQyttqqhDkEVXfUcsnDLPGmvi + + + + mailer SMTP + + host mailhog + port 1025 + helo localhost + sasl_username info@swindon-makerspace.org + sasl_password password + debug 1 + + + + + subdomain swindon-makerspace + domain mock-login:8080 + public_key + private_key + callback_url http://access-system/oneall_login_callback + + + name access_system + mac_secret C7B626AC-87A8-4C10-B5CC-D98BED2DBE0B + diff --git a/.devcontainer/config/accesssystem_api_local.conf.dev b/.devcontainer/config/accesssystem_api_local.conf.dev new file mode 100644 index 0000000..b88734c --- /dev/null +++ b/.devcontainer/config/accesssystem_api_local.conf.dev @@ -0,0 +1,10 @@ + + + dsn DBI:SQLite:db/dev.db + + + + api-key + +base_url http://localhost:3000/ +overlap_days 14 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..bfa2c81 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,71 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose +{ + "name": "Swindon Makerspace Access System devcontainer", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../docker-compose.yaml", + "docker-compose.yaml" + ], + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "access-system", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": true, + "configureZshAsDefaultShell": true, + "installOhMyZsh": true, + "installOhMyZshConfig": true, + "upgradePackages": true, + "username": "automatic", + "userUid": "automatic", + "userGid": "automatic" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "installDirectlyFromGitHubRelease": true, + "version": "latest" + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 3001], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Run commands after the container is created. + "postCreateCommand": ".devcontainer/init-sqlite -d dev.db", + "postStartCommand": ".devcontainer/start-services", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "richterger.perl", + "cfgweb.vscode-perl", + "tamasfe.even-better-toml", + "maattdd.gitless", + "EditorConfig.EditorConfig" + ] + }, + "jetbrains" : { + "settings": { + "com.intellij:app:HttpConfigurable.use_proxy_pac": true + }, + "backend" : "IntelliJ" + }, + }, + "remoteUser": "swmakers" +} diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml new file mode 100644 index 0000000..7204f0d --- /dev/null +++ b/.devcontainer/docker-compose.yaml @@ -0,0 +1,24 @@ +services: + access-system: + # We use a pre-built image to save time. You can also use a Dockerfile to build your own. + # Uncomment if you want to override the service's image with the Dockerfile in the .devcontainer + # folder. Note that the path of the Dockerfile and context is relative to the *primary* + # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" + # array). The sample below assumes your primary file is in the root of your project. + # + # build: + # context: . + # dockerfile: .devcontainer/Dockerfile + + volumes: + # Update this to wherever you want VS Code to mount the folder of your project + - ..:/workspaces:cached + + # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. + # cap_add: + # - SYS_PTRACE + # security_opt: + # - seccomp:unconfined + + # Overrides default command so things don't shut down after the process ends. + command: /bin/sh -c "while sleep 1000; do :; done" diff --git a/.devcontainer/init-config b/.devcontainer/init-config new file mode 100755 index 0000000..92e1b9b --- /dev/null +++ b/.devcontainer/init-config @@ -0,0 +1,33 @@ +#!/bin/bash + +# Define the source and destination directories +src_dir="$(dirname "$0")/config" +dest_dir="$(dirname "$0")/.." + +# Find all .conf.dev files in the source directory +IFS=$'\n' read -rd '' -a config_files <<< "$(find "$src_dir" -name "*.conf.dev" -exec basename {} \;)" + +# Function to copy and rename files +copy_and_rename() { + local src_file="$1" + local dest_file="$2" + + if [ ! -f "$dest_file" ]; then + cp "$src_file" "$dest_file" + echo "Copied $src_file to $dest_file" + else + echo "File $dest_file already exists, not overwriting" + fi +} + +# Process each config file +for config_file in "${config_files[@]}"; do + src_file="$src_dir/$config_file" + dest_file="$dest_dir/${config_file%.dev}" + + if [ -f "$src_file" ]; then + copy_and_rename "$src_file" "$dest_file" + else + echo "Source file $src_file does not exist" + fi +done diff --git a/.devcontainer/init-sqlite b/.devcontainer/init-sqlite new file mode 100755 index 0000000..9e9bb89 --- /dev/null +++ b/.devcontainer/init-sqlite @@ -0,0 +1,86 @@ +#!/bin/sh + +# Function to display usage +usage() { + echo "Initialize an SQLite database for development in db/ folder\n" + echo "Options:" + echo "\t -d , e.g. dev.db" + echo "\t -f Force recreate the database file if it exists" + echo "\nUsage: $0 -d [-f]" + exit 1 +} + +force_recreate=false + +# Parse command line arguments +while getopts ":d:f" opt; do + case ${opt} in + d ) + db_filename=$OPTARG + ;; + f ) + force_recreate=true + ;; + \? ) + usage + ;; + esac +done + +# Check if the db_filename is provided +if [ -z "$db_filename" ]; then + usage +fi + +# Check if the filename ends with .db +if [ "${db_filename##*.}" != "db" ]; then + echo "Error: The database filename must end with .db" + usage +fi + +# Create the db folder if it doesn't exist +db_folder="$(dirname "$0")/../db" +mkdir -p "$db_folder" + +# Create the SQLite database file +db_path="$db_folder/$db_filename" + +# Check if the database file already exists +if [ -f "$db_path" ]; then + if [ "$force_recreate" = true ]; then + # Create a backup of the existing database file with the current date and time + timestamp=$(date +"%Y-%m-%d_%H:%M") + cp "$db_path" "$db_path.$timestamp.bak" + echo "Existing database file backed up as $db_filename.$timestamp.bak" + else + echo "Database file $db_filename already exists. Use -f to force recreate." + exit 0 + fi +fi + +# Create or overwrite the SQLite database file +sqlite3 "$db_path" "VACUUM;" + +# Find the latest SQLite schema file +sql_folder="$(dirname "$0")/../sql" +latest_sql_file=$(ls -1t "$sql_folder"/AccessSystem-Schema*SQLite.sql | head -n 1) + +# Check if the latest SQL file is found +if [ -z "$latest_sql_file" ]; then + echo "No SQLite schema file found in $sql_folder" + exit 1 +fi + +# Run the latest SQL schema file on the database +echo "Initializing database $db_filename with $latest_sql_file...\n" +sqlite3 "$db_path" -cmd ".echo on" ".read $latest_sql_file" + +echo "\nDatabase $db_filename created and initialized with $latest_sql_file" + +# use the .devcontainer/seed-sqlite.sql file to populate the database +seed_sql_file="$(dirname "$0")/seed-sqlite.sql" +if [ -f "$seed_sql_file" ]; then + echo "Seeding database $db_filename with $seed_sql_file...\n" + sqlite3 "$db_path" -cmd ".echo on" ".read $seed_sql_file" + echo "\nDatabase $db_filename seeded with $seed_sql_file" +fi diff --git a/.devcontainer/seed-sqlite.sql b/.devcontainer/seed-sqlite.sql new file mode 100644 index 0000000..2455ac6 --- /dev/null +++ b/.devcontainer/seed-sqlite.sql @@ -0,0 +1,14 @@ +INSERT INTO tiers (id, name, description, price, concessions_allowed, in_use, restrictions) +VALUES (4, 'Sponsor', 'Access 24hours a day, 365 days a year', 3500, true, true, '{}'); +INSERT INTO tiers (id, name, description, price, concessions_allowed, in_use, restrictions) +VALUES (3, 'Standard', 'Access 24hours a day, 365 days a year', 2500, true, true, '{}'); +INSERT INTO tiers (id, name, description, price, concessions_allowed, in_use, restrictions) +VALUES (1, 'MemberOfOtherHackspace', + 'Living outside of Swindon Borough and a fully paid up member of another Maker or Hackspace', 500, false, true, + '{}'); +INSERT INTO tiers (id, name, description, price, concessions_allowed, in_use, restrictions) +VALUES (2, 'Weekend', 'Access 12:00am Saturday until 12:00am Monday, and Wednesdays 6:30pm to 11:59pm only', 1500, true, + true, + '{"times":[{"from":"3:18:00","to":"3:23:59"},{"from":"6:00:01","to":"6:23:59"},{"from":"7:00:01","to":"7:23:59"}]}'); +INSERT INTO tiers (id, name, description, price, concessions_allowed, in_use, restrictions) +VALUES (5, 'MensShed', 'Members of Renew only, rate now retired', 1000, false, false, '{}'); diff --git a/.devcontainer/start-services b/.devcontainer/start-services new file mode 100755 index 0000000..3e1d352 --- /dev/null +++ b/.devcontainer/start-services @@ -0,0 +1,17 @@ +#!/bin/bash + +export PERL5LIB="/workspace/AccessSystem/local/lib/perl5/" +# export PATH="/workspace/AccessSystem/local/bin:${PATH}" + +echo "Initializing service configuration..." +.devcontainer/init-config + +echo "Starting services in the background..." + +echo "Starting database admin (RapidApp) @ http://localhost:3001/admin" +carton exec perl script/accesssystem_daemon_devcontainer.pl start + +echo "Starting API server @ http://localhost:3000" +carton exec perl script/accesssystem_api_daemon_devcontainer.pl start + +echo "It may take a few seconds for the services to be up. Please wait..." diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/build-dev-image.yaml b/.github/workflows/build-dev-image.yaml index b0351b5..fa8f4ca 100644 --- a/.github/workflows/build-dev-image.yaml +++ b/.github/workflows/build-dev-image.yaml @@ -4,6 +4,8 @@ on: push: branches: - master + tags: + - 'v*' pull_request: branches: - master @@ -25,22 +27,51 @@ jobs: - name: 📦 Checkout code uses: actions/checkout@v4 - - name: 🏗️ Build image - run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID},sha=${GITHUB_SHA},ref=${GITHUB_REF}" - - - name: 🔐 Log in to registry - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login $CONTAINER_REGISTRY -u $ --password-stdin - - - name: 💾 Push image - run: | - IMAGE_ID=$CONTAINER_REGISTRY/$GITHUB_REPOSITORY_OWNER/$IMAGE_NAME - VERSION=$(echo "${BRANCH_NAME#refs/heads/}" | sed -e 's/[^a-zA-Z0-9]/-/g') # Get the branch name from the ref and replace non-alphanumeric characters with hyphens - [ "$VERSION" == "master" ] && VERSION=latest - echo "🆔 Image ID: $IMAGE_ID" - echo "#️⃣ GITHUB_SHA: $GITHUB_SHA" - echo "🎯 Version: $VERSION" - echo "🚀 Pushing image to registry........................................." - docker tag $IMAGE_NAME $IMAGE_ID:$GITHUB_SHA - docker push $IMAGE_ID:$GITHUB_SHA - docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION + - name: 🧰 Get branch name + run: | # Get the branch name from the ref and replace non-alphanumeric characters with hyphens + BRANCH=$(echo "${BRANCH_NAME#refs/heads/}" | sed -e 's/[^a-zA-Z0-9]/-/g') + echo "BRANCH=$BRANCH" >> $GITHUB_ENV + + - name: 🏷️ Generate Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.CONTAINER_REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch,priority=610 + type=ref,event=pr + type=semver,pattern={{raw}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=semver,pattern=v{{major}} + type=sha,format=long + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=${{ env.BRANCH_NAME }},priority=10 + annotations: | + runnumber=${{ github.run_id }} + sha=${{ github.sha }} + ref=${{ github.ref }} + + - name: 🔐 Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.CONTAINER_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 🛠️ Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: 🚀 Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: .devcontainer/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index f8436b9..25baf8e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ .plx/ .vscode/perl-lang/ +*.code-workspace +.idea/ # exception for email template assets !root/img/*.png diff --git a/README.md b/README.md index 45c7b9e..57994cf 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Technologies * [Catalyst](https://metacpan.org/pod/Catalyst) - Perl Web Framework -* [RapidApp](https://metacpan.org/pod/RadpiApp) - CRUD built atop Catalyst +* [RapidApp](https://metacpan.org/pod/RapidApp) - CRUD built atop Catalyst * [DBIx::Class](https://metacpan.org/pod/DBIx::Class) - Perl ORM @@ -50,11 +50,11 @@ INSTALL * Install cpanm, either via [App::cpanminus](https://metacpan.org/pod/App::cpanminus) or wget [cpanm](http://xrl.us/cpanm), make the result executable. -* Install carton: cpanm -S Carton +* Install carton: `cpanm -S Carton` * Checkout this git repo, cd into the repo directory. -* Install Perl dependencies for this system: carton install --cached +* Install Perl dependencies for this system: `carton install --cached` SETUP ----- diff --git a/cpanfile b/cpanfile index 72579d6..58f0f45 100644 --- a/cpanfile +++ b/cpanfile @@ -21,7 +21,6 @@ requires 'Template::Stash::XS'; requires 'HTML::FormHandlerX::Field::noCAPTCHA'; requires 'Daemon::Control'; requires 'Catalyst::View::JSON'; -requires 'Daemon::Control'; requires 'LWP::UserAgent'; requires 'LWP::Protocol::https'; requires 'JSON'; diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..1500f88 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,8 @@ +services: + access-system: + image: ghcr.io/swindonmakers/access-system-dev:latest + + mailhog: + image: mailhog/mailhog + ports: + - "8025:8025" diff --git a/root/src/login.tt b/root/src/login.tt index c17f2f7..d614adf 100644 --- a/root/src/login.tt +++ b/root/src/login.tt @@ -2,12 +2,12 @@ diff --git a/script/accesssystem_api_daemon_devcontainer.pl b/script/accesssystem_api_daemon_devcontainer.pl new file mode 100755 index 0000000..dcbb8d5 --- /dev/null +++ b/script/accesssystem_api_daemon_devcontainer.pl @@ -0,0 +1,27 @@ +#!/usr/bin/perl +use warnings; +use strict; +use local::lib '/workspaces/AccessSystem/local/lib/perl5/'; +use Daemon::Control; +my $path = "/workspaces/AccessSystem"; + +exit Daemon::Control->new( + name => "AccessSystem-API", + lsb_start => '$syslog $remote_fs', + lsb_stop => '$syslog', + lsb_sdesc => 'AccessSystem API', + lsb_desc => 'AccessSystem API controls the AccessSystem API daemon.', + path => "$path/script/accesssystem_api_daemon_devcontainer.pl", + directory => "$path", +# init_config => "$path etc/environment", + user => 'swmakers', + group => 'swmakers', + program => "carton exec $path/script/accesssystem_api_server.pl --restart --port 3000 > /proc/1/fd/1 2> /proc/1/fd/2", + + pid_file => '/tmp/accesssystem_api.pid', + stderr_file => '/proc/1/fd/2', + stdout_file => '/proc/1/fd/1', + + fork => 2, + + )->run; diff --git a/script/accesssystem_daemon_devcontainer.pl b/script/accesssystem_daemon_devcontainer.pl new file mode 100755 index 0000000..d83d1b3 --- /dev/null +++ b/script/accesssystem_daemon_devcontainer.pl @@ -0,0 +1,27 @@ +#!/usr/bin/perl +use warnings; +use strict; +use local::lib '/workspaces/AccessSystem/local/lib/perl5/'; +use Daemon::Control; +my $path = "/workspaces/AccessSystem"; + +exit Daemon::Control->new( + name => "AccessSystem-CRUD", + lsb_start => '$syslog $remote_fs', + lsb_stop => '$syslog', + lsb_sdesc => 'AccessSystem CRUD', + lsb_desc => 'AccessSystem CRUD controls the AccessSystem CRUD daemon.', + path => "$path/script/accesssystem_daemon_devcontainer.pl", + directory => "$path", +# init_config => "$path etc/environment", + user => 'swmakers', + group => 'swmakers', + program => "carton exec $path/script/accesssystem_server.pl --restart --port 3001 > /proc/1/fd/1 2> /proc/1/fd/2", + + pid_file => '/tmp/accesssystem_crud.pid', + stderr_file => '/proc/1/fd/2', + stdout_file => '/proc/1/fd/1', + + fork => 2, + + )->run;