mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 00:44:03 -04:00
Compare commits
6 Commits
fix/shared
...
pr-4522
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5827f60045 | ||
|
|
2acfb4773e | ||
|
|
832b0e686f | ||
|
|
c26146c22b | ||
|
|
e67f6bb290 | ||
|
|
9f8eef96a7 |
@@ -16,9 +16,6 @@ CI_ENVIRONMENT = production
|
||||
# Configure with comma-separated list of domains/subdomains:
|
||||
# app.allowedHostnames = 'yourdomain.com,www.yourdomain.com'
|
||||
#
|
||||
# Or via environment variable (useful for Docker/Compose):
|
||||
# ALLOWED_HOSTNAMES=yourdomain.com,www.yourdomain.com
|
||||
#
|
||||
# For local development:
|
||||
# app.allowedHostnames = 'localhost'
|
||||
#
|
||||
|
||||
94
.github/scripts/get-version.sh
vendored
94
.github/scripts/get-version.sh
vendored
@@ -1,94 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Shared version tag generation script for GitHub Actions workflows
|
||||
# Usage: ./get-version.sh [FORMAT] [SHA_LENGTH]
|
||||
#
|
||||
# Formats:
|
||||
# docker-tag - Docker image tag (default)
|
||||
# archive - Archive filename suffix
|
||||
# all - Output all version variables for GITHUB_OUTPUT
|
||||
#
|
||||
# Environment variables:
|
||||
# GITHUB_REF - Git ref (e.g., refs/heads/master, refs/pull/123/merge)
|
||||
# GITHUB_SHA - Git commit SHA
|
||||
# GITHUB_EVENT_NAME - Event that triggered workflow (push, pull_request, etc.)
|
||||
# GITHUB_EVENT_PATH - Path to event JSON (for PR number extraction)
|
||||
# GITHUB_OUTPUT - Path to GITHUB_OUTPUT file (when format=all)
|
||||
|
||||
# Ensure we're in a git repository with source files
|
||||
cd "${GITHUB_WORKSPACE:-.}"
|
||||
|
||||
# Get version from App.php
|
||||
VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g")
|
||||
|
||||
# Standardize SHA length (default: 7 chars)
|
||||
SHA_LENGTH="${2:-7}"
|
||||
SHA="${GITHUB_SHA:0:$SHA_LENGTH}"
|
||||
|
||||
# Initialize variables
|
||||
IMAGE_TAG=""
|
||||
BRANCH=""
|
||||
|
||||
# Detect event type and generate appropriate tag
|
||||
if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "pull_request_review" ]]; then
|
||||
# Extract PR number from event JSON
|
||||
if [[ -f "${GITHUB_EVENT_PATH:-}" ]]; then
|
||||
PR_NUMBER=$(jq -r '.pull_request.number // .number // empty' < "$GITHUB_EVENT_PATH" 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$PR_NUMBER" ]]; then
|
||||
# PR-based tag (for PR deployments)
|
||||
IMAGE_TAG="pr-${PR_NUMBER}-${SHA}"
|
||||
BRANCH="pr-${PR_NUMBER}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback if we couldn't extract PR number
|
||||
if [[ -z "$IMAGE_TAG" ]]; then
|
||||
# Try to extract from GITHUB_REF
|
||||
PR_NUMBER=$(echo "$GITHUB_REF" | grep -oP 'pull/\K[0-9]+' || true)
|
||||
if [[ -n "$PR_NUMBER" ]]; then
|
||||
IMAGE_TAG="pr-${PR_NUMBER}-${SHA}"
|
||||
BRANCH="pr-${PR_NUMBER}"
|
||||
else
|
||||
# Last resort: use SHA only
|
||||
IMAGE_TAG="${SHA}"
|
||||
BRANCH="unknown"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Branch-based tag (for push events)
|
||||
BRANCH="${GITHUB_REF#refs/heads/}"
|
||||
BRANCH=$(echo "$BRANCH" | sed 's/feature\///' | tr '/' '_')
|
||||
|
||||
if [[ "$BRANCH" == "master" ]]; then
|
||||
# Master builds: use version as tag
|
||||
IMAGE_TAG="${VERSION}"
|
||||
else
|
||||
# Feature branch builds: version + branch + sha
|
||||
IMAGE_TAG="${VERSION}-${BRANCH}-${SHA}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Output format based on first argument
|
||||
case "${1:-docker-tag}" in
|
||||
docker-tag)
|
||||
echo "$IMAGE_TAG"
|
||||
;;
|
||||
archive)
|
||||
echo "${VERSION}.${SHA}"
|
||||
;;
|
||||
all)
|
||||
{
|
||||
echo "version=${VERSION}"
|
||||
echo "version-tag=${IMAGE_TAG}"
|
||||
echo "short-sha=${SHA}"
|
||||
echo "branch=${BRANCH}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "::debug::version=${VERSION}, version-tag=${IMAGE_TAG}, short-sha=${SHA}, branch=${BRANCH}"
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unknown format: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
33
.github/workflows/build-release.yml
vendored
33
.github/workflows/build-release.yml
vendored
@@ -22,7 +22,6 @@ jobs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
version-tag: ${{ steps.version.outputs.version-tag }}
|
||||
short-sha: ${{ steps.version.outputs.short-sha }}
|
||||
branch: ${{ steps.version.outputs.branch }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -76,13 +75,16 @@ jobs:
|
||||
- name: Get version info
|
||||
id: version
|
||||
run: |
|
||||
chmod +x .github/scripts/get-version.sh
|
||||
.github/scripts/get-version.sh all 7
|
||||
VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g")
|
||||
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | sed 's/feature\///' | tr '/' '_')
|
||||
TAG=$(echo "${GITHUB_TAG:-$BRANCH}" | tr '/' '_')
|
||||
SHORT_SHA=$(git rev-parse --short=6 HEAD)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version-tag=$VERSION-$BRANCH-$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GITHUB_EVENT_PATH: ${{ github.event_path }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
GITHUB_TAG: ${{ github.ref_name }}
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
@@ -121,14 +123,13 @@ jobs:
|
||||
.
|
||||
!.git
|
||||
!node_modules
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
docker:
|
||||
name: Build Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Download build context
|
||||
@@ -152,14 +153,14 @@ jobs:
|
||||
- name: Determine Docker tags
|
||||
id: tags
|
||||
run: |
|
||||
TAG="${{ needs.build.outputs.version-tag }}"
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${TAG}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ needs.build.outputs.branch }}" = "master" ]; then
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${TAG},${{ secrets.DOCKER_USERNAME }}/opensourcepos:latest" >> $GITHUB_OUTPUT
|
||||
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | tr '/' '_')
|
||||
if [ "$BRANCH" = "master" ]; then
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }},${{ secrets.DOCKER_USERNAME }}/opensourcepos:master" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
@@ -192,7 +193,7 @@ jobs:
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${{ needs.build.outputs.version }}"
|
||||
SHORT_SHA="${{ needs.build.outputs.short-sha }}"
|
||||
SHORT_SHA=$(git rev-parse --short=6 HEAD)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
219
.github/workflows/deploy-core.yml
vendored
219
.github/workflows/deploy-core.yml
vendored
@@ -1,219 +0,0 @@
|
||||
name: Deploy Core
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: 'Docker image tag to deploy'
|
||||
type: string
|
||||
required: true
|
||||
sha:
|
||||
description: 'Git commit SHA to deploy'
|
||||
type: string
|
||||
required: true
|
||||
description:
|
||||
description: 'Deployment description'
|
||||
type: string
|
||||
required: true
|
||||
pr_number:
|
||||
description: 'Pull request number (optional)'
|
||||
type: string
|
||||
required: false
|
||||
outputs:
|
||||
deployment_id:
|
||||
description: 'GitHub deployment ID'
|
||||
value: ${{ jobs.deploy.outputs.deployment_id }}
|
||||
status:
|
||||
description: 'Deployment status (success/failure)'
|
||||
value: ${{ jobs.deploy.outputs.status }}
|
||||
|
||||
concurrency:
|
||||
group: deploy-staging
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to staging
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: staging
|
||||
url: ${{ vars.DEPLOY_URL || 'https://dev.opensourcepos.org' }}
|
||||
deployment: false
|
||||
|
||||
outputs:
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
status: ${{ steps.webhook.outputs.status }}
|
||||
|
||||
steps:
|
||||
- name: Create GitHub Deployment
|
||||
id: deployment
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
REF_SHA: ${{ inputs.sha }}
|
||||
DESCRIPTION: ${{ inputs.description }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOYMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/deployments" \
|
||||
-X POST \
|
||||
-f ref="${REF_SHA}" \
|
||||
-f environment="staging" \
|
||||
-f description="${DESCRIPTION}" \
|
||||
-F auto_merge=false \
|
||||
-F required_contexts[] \
|
||||
--jq '.id')
|
||||
|
||||
if [ -z "$DEPLOYMENT_ID" ]; then
|
||||
echo "::error::Failed to create deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "deployment_id=$DEPLOYMENT_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "Created deployment: $DEPLOYMENT_ID"
|
||||
|
||||
- name: Set deployment status to in_progress
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="in_progress" \
|
||||
-f description="Deployment in progress..." \
|
||||
-f log_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
|
||||
- name: Trigger deployment webhook
|
||||
id: webhook
|
||||
env:
|
||||
DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }}
|
||||
DEPLOY_WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }}
|
||||
DOCKER_REPO_NAME: ${{ secrets.DOCKER_REPO_NAME }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
REF_SHA: ${{ inputs.sha }}
|
||||
DEPLOYMENT_ID: ${{ steps.deployment.outputs.deployment_id }}
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$DEPLOY_WEBHOOK_URL" ]; then
|
||||
echo "::error::DEPLOY_WEBHOOK_URL secret is not configured"
|
||||
echo "Please add the DEPLOY_WEBHOOK_URL secret in your repository settings"
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_NAME="${DOCKER_REPO_NAME:-opensourcepos/opensourcepos}"
|
||||
REPO_NAMESPACE="${REPO_NAME%%/*}"
|
||||
REPO_SHORT_NAME="${REPO_NAME#*/}"
|
||||
PUSHED_AT=$(date +%s)
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--argjson pushed_at "$PUSHED_AT" \
|
||||
--arg pusher "$GITHUB_ACTOR" \
|
||||
--arg tag "$IMAGE_TAG" \
|
||||
--arg repo_name "$REPO_NAME" \
|
||||
--arg name "$REPO_SHORT_NAME" \
|
||||
--arg namespace "$REPO_NAMESPACE" \
|
||||
--arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \
|
||||
--arg deployment_id "$DEPLOYMENT_ID" \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg sha "$REF_SHA" \
|
||||
--arg run_id "$GITHUB_RUN_ID" \
|
||||
--arg actor "$GITHUB_ACTOR" \
|
||||
--argjson pr_number "$PR_NUMBER" \
|
||||
'{
|
||||
callback_url: $callback_url,
|
||||
push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag},
|
||||
repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"},
|
||||
github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor, pull_request: $pr_number}
|
||||
}')
|
||||
else
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--argjson pushed_at "$PUSHED_AT" \
|
||||
--arg pusher "$GITHUB_ACTOR" \
|
||||
--arg tag "$IMAGE_TAG" \
|
||||
--arg repo_name "$REPO_NAME" \
|
||||
--arg name "$REPO_SHORT_NAME" \
|
||||
--arg namespace "$REPO_NAMESPACE" \
|
||||
--arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \
|
||||
--arg deployment_id "$DEPLOYMENT_ID" \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg sha "$REF_SHA" \
|
||||
--arg run_id "$GITHUB_RUN_ID" \
|
||||
--arg actor "$GITHUB_ACTOR" \
|
||||
'{
|
||||
callback_url: $callback_url,
|
||||
push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag},
|
||||
repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"},
|
||||
github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor}
|
||||
}')
|
||||
fi
|
||||
|
||||
echo "Sending webhook..."
|
||||
echo "Image: ${IMAGE_TAG}"
|
||||
echo "Environment: staging"
|
||||
|
||||
HEADERS=(-H "Content-Type: application/json")
|
||||
|
||||
if [ -n "$DEPLOY_WEBHOOK_SECRET" ]; then
|
||||
SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$DEPLOY_WEBHOOK_SECRET" | sed 's/.*= //')
|
||||
HEADERS+=(-H "X-Hub-Signature-256: sha256=$SIGNATURE")
|
||||
echo "Using HMAC-SHA256 signature verification"
|
||||
else
|
||||
echo "::warning::DEPLOY_WEBHOOK_SECRET not set - webhook calls will not be signed"
|
||||
echo "For security, configure DEPLOY_WEBHOOK_SECRET in your repository settings"
|
||||
fi
|
||||
|
||||
HTTP_CODE=$(curl -sS --connect-timeout 10 --max-time 120 \
|
||||
-o response.txt -w "%{http_code}" \
|
||||
-X POST \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$PAYLOAD" \
|
||||
"$DEPLOY_WEBHOOK_URL") || HTTP_CODE="000"
|
||||
|
||||
echo "Response code: $HTTP_CODE"
|
||||
if [ -s response.txt ]; then
|
||||
cat response.txt
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "status=success" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Set deployment status
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
STATE="${{ steps.webhook.outputs.status }}"
|
||||
|
||||
if [ "$STATE" = "success" ]; then
|
||||
DESCRIPTION=$(jq -nr --arg tag "$IMAGE_TAG" \
|
||||
'"Deployed image \($tag) to staging"')
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="success" \
|
||||
-f description="$DESCRIPTION"
|
||||
else
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="failure" \
|
||||
-f description="Deployment failed"
|
||||
exit 1
|
||||
fi
|
||||
202
.github/workflows/deploy-pr.yml
vendored
202
.github/workflows/deploy-pr.yml
vendored
@@ -14,16 +14,17 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare deployment
|
||||
deploy-staging:
|
||||
name: Deploy to staging
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.review.state == 'approved' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
outputs:
|
||||
image_tag: ${{ steps.image.outputs.tag }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
|
||||
environment:
|
||||
name: staging
|
||||
url: ${{ vars.DEPLOY_URL || 'https://dev.opensourcepos.org' }}
|
||||
deployment: false
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
@@ -33,41 +34,162 @@ jobs:
|
||||
|
||||
- name: Get image tag
|
||||
id: image
|
||||
run: |
|
||||
chmod +x .github/scripts/get-version.sh
|
||||
IMAGE_TAG=$(.github/scripts/get-version.sh docker-tag 7)
|
||||
echo "tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
GITHUB_EVENT_PATH: ${{ github.event_path }}
|
||||
GITHUB_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
|
||||
deploy:
|
||||
name: Deploy to staging
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/deploy-core.yml
|
||||
with:
|
||||
image_tag: ${{ needs.prepare.outputs.image_tag }}
|
||||
sha: ${{ needs.prepare.outputs.sha }}
|
||||
description: Deploy PR #${{ needs.prepare.outputs.pr_number }} to staging
|
||||
pr_number: ${{ needs.prepare.outputs.pr_number }}
|
||||
secrets: inherit
|
||||
|
||||
comment:
|
||||
name: Comment deployment status
|
||||
needs: [prepare, deploy]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
|
||||
PR_NUMBER: ${{ needs.prepare.outputs.pr_number }}
|
||||
REF_SHA: ${{ needs.prepare.outputs.sha }}
|
||||
STATUS: ${{ needs.deploy.outputs.status }}
|
||||
|
||||
steps:
|
||||
- name: Comment on PR
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
IMAGE_TAG="pr-${PR_NUMBER}-${PR_SHA:0:7}"
|
||||
echo "tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create GitHub Deployment
|
||||
id: deployment
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REF_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOYMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/deployments" \
|
||||
-X POST \
|
||||
-f ref="${REF_SHA}" \
|
||||
-f environment="staging" \
|
||||
-f description="Deploy PR #${PR_NUMBER} to staging" \
|
||||
-F auto_merge=false \
|
||||
-F required_contexts[] \
|
||||
--jq '.id')
|
||||
|
||||
if [ -z "$DEPLOYMENT_ID" ]; then
|
||||
echo "::error::Failed to create deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "deployment_id=$DEPLOYMENT_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "Created deployment: $DEPLOYMENT_ID"
|
||||
|
||||
- name: Set deployment status to in_progress
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="in_progress" \
|
||||
-f description="Deploying PR #${PR_NUMBER}..." \
|
||||
-f log_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
|
||||
- name: Trigger deployment webhook
|
||||
id: webhook
|
||||
env:
|
||||
DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }}
|
||||
DEPLOY_WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }}
|
||||
DOCKER_REPO_NAME: ${{ secrets.DOCKER_REPO_NAME }}
|
||||
IMAGE_TAG: ${{ steps.image.outputs.tag }}
|
||||
REF_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
DEPLOYMENT_ID: ${{ steps.deployment.outputs.deployment_id }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$DEPLOY_WEBHOOK_URL" ]; then
|
||||
echo "::error::DEPLOY_WEBHOOK_URL secret is not configured"
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_NAME="${DOCKER_REPO_NAME:-opensourcepos/opensourcepos}"
|
||||
REPO_NAMESPACE="${REPO_NAME%%/*}"
|
||||
REPO_SHORT_NAME="${REPO_NAME#*/}"
|
||||
PUSHED_AT=$(date +%s)
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--argjson pushed_at "$PUSHED_AT" \
|
||||
--arg pusher "$GITHUB_ACTOR" \
|
||||
--arg tag "$IMAGE_TAG" \
|
||||
--arg repo_name "$REPO_NAME" \
|
||||
--arg name "$REPO_SHORT_NAME" \
|
||||
--arg namespace "$REPO_NAMESPACE" \
|
||||
--arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \
|
||||
--arg deployment_id "$DEPLOYMENT_ID" \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg sha "$REF_SHA" \
|
||||
--arg run_id "$GITHUB_RUN_ID" \
|
||||
--arg actor "$GITHUB_ACTOR" \
|
||||
--argjson pr_number "$PR_NUMBER" \
|
||||
'{
|
||||
callback_url: $callback_url,
|
||||
push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag},
|
||||
repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"},
|
||||
github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor, pull_request: $pr_number}
|
||||
}')
|
||||
|
||||
echo "Sending webhook..."
|
||||
echo "Image: ${IMAGE_TAG}"
|
||||
echo "PR: #${PR_NUMBER}"
|
||||
|
||||
HEADERS=(-H "Content-Type: application/json")
|
||||
|
||||
if [ -n "$DEPLOY_WEBHOOK_SECRET" ]; then
|
||||
SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$DEPLOY_WEBHOOK_SECRET" | sed 's/.*= //')
|
||||
HEADERS+=(-H "X-Hub-Signature-256: sha256=$SIGNATURE")
|
||||
fi
|
||||
|
||||
HTTP_CODE=$(curl -sS --connect-timeout 10 --max-time 120 \
|
||||
-o response.txt -w "%{http_code}" \
|
||||
-X POST \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$PAYLOAD" \
|
||||
"$DEPLOY_WEBHOOK_URL") || HTTP_CODE="000"
|
||||
|
||||
echo "Response code: $HTTP_CODE"
|
||||
if [ -s response.txt ]; then
|
||||
cat response.txt
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "status=success" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Set deployment status
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ steps.image.outputs.tag }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
STATE="${{ steps.webhook.outputs.status }}"
|
||||
|
||||
if [ "$STATE" = "success" ]; then
|
||||
DESCRIPTION=$(jq -nr --arg tag "$IMAGE_TAG" --arg pr "$PR_NUMBER" \
|
||||
'"Deployed PR #\($pr) (\($tag)) to staging"')
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="success" \
|
||||
-f description="$DESCRIPTION"
|
||||
else
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="failure" \
|
||||
-f description="Staging deployment failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Comment deployment status
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ steps.image.outputs.tag }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REF_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
STATUS: ${{ steps.webhook.outputs.status }}
|
||||
run: |
|
||||
if [ "$STATUS" = "success" ]; then
|
||||
BODY=$(jq -nr --arg tag "$IMAGE_TAG" --arg sha "$REF_SHA" --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
|
||||
205
.github/workflows/deploy.yml
vendored
205
.github/workflows/deploy.yml
vendored
@@ -7,17 +7,208 @@ on:
|
||||
description: 'Docker image tag to deploy (e.g., v3.4.0, latest)'
|
||||
required: true
|
||||
default: 'latest'
|
||||
environment:
|
||||
description: 'Target environment'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- production
|
||||
- staging
|
||||
default: 'production'
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: 'Docker image tag to deploy'
|
||||
type: string
|
||||
default: 'latest'
|
||||
environment:
|
||||
description: 'Target environment'
|
||||
type: string
|
||||
default: 'staging'
|
||||
sha:
|
||||
description: 'Git commit SHA to deploy'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ inputs.environment }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
validate-inputs:
|
||||
name: Validate deployment inputs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate environment
|
||||
env:
|
||||
TARGET_ENV: ${{ inputs.environment }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$TARGET_ENV" in
|
||||
production|staging) ;;
|
||||
*)
|
||||
echo "::error::Invalid environment '$TARGET_ENV'. Expected 'production' or 'staging'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
deploy:
|
||||
name: Deploy to staging
|
||||
uses: ./.github/workflows/deploy-core.yml
|
||||
with:
|
||||
image_tag: ${{ inputs.image_tag }}
|
||||
sha: ${{ github.sha }}
|
||||
description: Deploy image ${{ inputs.image_tag }}
|
||||
secrets: inherit
|
||||
name: Deploy to ${{ inputs.environment }}
|
||||
needs: validate-inputs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: ${{ inputs.environment }}
|
||||
url: ${{ vars.DEPLOY_URL || (inputs.environment == 'production' && 'https://demo.opensourcepos.org' || 'https://dev.opensourcepos.org') }}
|
||||
deployment: false
|
||||
|
||||
steps:
|
||||
- name: Create GitHub Deployment
|
||||
id: deployment
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
TARGET_ENV: ${{ inputs.environment }}
|
||||
REF_SHA: ${{ inputs.sha || github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOYMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/deployments" \
|
||||
-X POST \
|
||||
-f ref="${REF_SHA}" \
|
||||
-f environment="${TARGET_ENV}" \
|
||||
-f description="Deploy image ${IMAGE_TAG}" \
|
||||
-F auto_merge=false \
|
||||
-F required_contexts[] \
|
||||
--jq '.id')
|
||||
|
||||
if [ -z "$DEPLOYMENT_ID" ]; then
|
||||
echo "::error::Failed to create deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "deployment_id=$DEPLOYMENT_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "Created deployment: $DEPLOYMENT_ID"
|
||||
|
||||
- name: Set deployment status to in_progress
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="in_progress" \
|
||||
-f description="Deployment in progress..." \
|
||||
-f log_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
|
||||
- name: Trigger deployment webhook
|
||||
id: webhook
|
||||
env:
|
||||
DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }}
|
||||
DEPLOY_WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }}
|
||||
DOCKER_REPO_NAME: ${{ secrets.DOCKER_REPO_NAME }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
TARGET_ENV: ${{ inputs.environment }}
|
||||
REF_SHA: ${{ inputs.sha || github.sha }}
|
||||
DEPLOYMENT_ID: ${{ steps.deployment.outputs.deployment_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$DEPLOY_WEBHOOK_URL" ]; then
|
||||
echo "::error::DEPLOY_WEBHOOK_URL secret is not configured"
|
||||
echo "Please add the DEPLOY_WEBHOOK_URL secret in your repository settings"
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_NAME="${DOCKER_REPO_NAME:-opensourcepos/opensourcepos}"
|
||||
REPO_NAMESPACE="${REPO_NAME%%/*}"
|
||||
REPO_SHORT_NAME="${REPO_NAME#*/}"
|
||||
PUSHED_AT=$(date +%s)
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--argjson pushed_at "$PUSHED_AT" \
|
||||
--arg pusher "$GITHUB_ACTOR" \
|
||||
--arg tag "$IMAGE_TAG" \
|
||||
--arg repo_name "$REPO_NAME" \
|
||||
--arg name "$REPO_SHORT_NAME" \
|
||||
--arg namespace "$REPO_NAMESPACE" \
|
||||
--arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \
|
||||
--arg deployment_id "$DEPLOYMENT_ID" \
|
||||
--arg environment "$TARGET_ENV" \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg sha "$REF_SHA" \
|
||||
--arg run_id "$GITHUB_RUN_ID" \
|
||||
--arg actor "$GITHUB_ACTOR" \
|
||||
'{
|
||||
callback_url: $callback_url,
|
||||
push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag},
|
||||
repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"},
|
||||
github_deployment: {id: $deployment_id, environment: $environment, repository: $repository, sha: $sha, run_id: $run_id, actor: $actor}
|
||||
}')
|
||||
|
||||
echo "Sending webhook..."
|
||||
echo "Image: ${IMAGE_TAG}"
|
||||
echo "Environment: ${TARGET_ENV}"
|
||||
|
||||
HEADERS=(-H "Content-Type: application/json")
|
||||
|
||||
if [ -n "$DEPLOY_WEBHOOK_SECRET" ]; then
|
||||
SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$DEPLOY_WEBHOOK_SECRET" | sed 's/.*= //')
|
||||
HEADERS+=(-H "X-Hub-Signature-256: sha256=$SIGNATURE")
|
||||
echo "Using HMAC-SHA256 signature verification"
|
||||
else
|
||||
echo "::warning::DEPLOY_WEBHOOK_SECRET not set - webhook calls will not be signed"
|
||||
echo "For security, configure DEPLOY_WEBHOOK_SECRET in your repository settings"
|
||||
fi
|
||||
|
||||
HTTP_CODE=$(curl -sS --connect-timeout 10 --max-time 120 \
|
||||
-o response.txt -w "%{http_code}" \
|
||||
-X POST \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$PAYLOAD" \
|
||||
"$DEPLOY_WEBHOOK_URL") || HTTP_CODE="000"
|
||||
|
||||
echo "Response code: $HTTP_CODE"
|
||||
if [ -s response.txt ]; then
|
||||
cat response.txt
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "status=success" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Set deployment status
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
TARGET_ENV: ${{ inputs.environment }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
STATE="${{ steps.webhook.outputs.status }}"
|
||||
|
||||
if [ "$STATE" = "success" ]; then
|
||||
DESCRIPTION=$(jq -nr --arg tag "$IMAGE_TAG" --arg env "$TARGET_ENV" \
|
||||
'"Deployed image \($tag) to \($env)"')
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="success" \
|
||||
-f description="$DESCRIPTION"
|
||||
else
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="failure" \
|
||||
-f description="Deployment failed"
|
||||
exit 1
|
||||
fi
|
||||
131
.github/workflows/install-script-test.yml
vendored
131
.github/workflows/install-script-test.yml
vendored
@@ -1,131 +0,0 @@
|
||||
name: Install Script Test
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'scripts/install-ubuntu.sh'
|
||||
- '.github/workflows/install-script-test.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'scripts/install-ubuntu.sh'
|
||||
- '.github/workflows/install-script-test.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
install-test:
|
||||
name: Test Install Script (${{ matrix.scenario }})
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- scenario: default
|
||||
db_pass: ''
|
||||
- scenario: custom-password
|
||||
db_pass: 'TestPass123!'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Make install script executable
|
||||
run: chmod +x scripts/install-ubuntu.sh
|
||||
|
||||
- name: Run install script
|
||||
env:
|
||||
DB_PASS: ${{ matrix.db_pass }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
echo "Running install script with scenario: ${{ matrix.scenario }}"
|
||||
sudo -E bash scripts/install-ubuntu.sh 2>&1 | tee install-output.log
|
||||
echo "Install completed successfully"
|
||||
|
||||
- name: Wait for services to stabilize
|
||||
run: sleep 10
|
||||
|
||||
- name: Verify Apache is running
|
||||
run: |
|
||||
echo "Checking Apache status..."
|
||||
sudo systemctl status apache2 --no-pager
|
||||
sudo systemctl is-active apache2
|
||||
|
||||
- name: Verify MariaDB is running
|
||||
run: |
|
||||
echo "Checking MariaDB status..."
|
||||
sudo systemctl status mariadb --no-pager
|
||||
sudo systemctl is-active mariadb
|
||||
|
||||
- name: Verify Apache HTTP response
|
||||
run: |
|
||||
echo "Testing HTTP response on port 80..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/)
|
||||
echo "HTTP Response Code: $HTTP_CODE"
|
||||
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then
|
||||
echo "Apache is responding correctly"
|
||||
elif [ "$HTTP_CODE" = "500" ]; then
|
||||
echo "HTTP 500 - Application error. Checking .env configuration..."
|
||||
sudo cat /var/www/ospos/.env 2>/dev/null | grep -E "database\.default\.(hostname|database|username|password)|encryption\.key|CI_ENVIRONMENT" | head -10
|
||||
sudo cat /var/www/ospos/writable/logs/*.log 2>/dev/null | tail -20 || true
|
||||
curl -s http://localhost/ | head -50
|
||||
exit 1
|
||||
else
|
||||
echo "Unexpected HTTP code: $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify OSPOS login page
|
||||
run: |
|
||||
echo "Checking OSPOS login page..."
|
||||
curl -s http://localhost/ | grep -qi "login\|password\|username" && echo "Login page content found" || {
|
||||
echo "Login page verification failed"
|
||||
curl -s http://localhost/ | head -50
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Verify database exists
|
||||
env:
|
||||
DB_PASS: ${{ matrix.db_pass != '' && matrix.db_pass || '' }}
|
||||
run: |
|
||||
echo "Verifying database..."
|
||||
|
||||
# Extract the generated password from install output if using default
|
||||
if [ -z "${{ matrix.db_pass }}" ]; then
|
||||
GENERATED_PASS=$(grep -oP 'Database Password: \K[^\s]+' install-output.log || true)
|
||||
if [ -n "$GENERATED_PASS" ]; then
|
||||
DB_PASS="$GENERATED_PASS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check database exists
|
||||
sudo mysql -u root -e "SHOW DATABASES LIKE 'ospos';" | grep -q ospos && echo "Database 'ospos' exists" || {
|
||||
echo "Database 'ospos' not found"
|
||||
sudo mysql -u root -e "SHOW DATABASES;"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check tables exist
|
||||
TABLE_COUNT=$(sudo mysql -u root ospos -e "SHOW TABLES;" | wc -l)
|
||||
echo "Found $TABLE_COUNT tables in database"
|
||||
if [ "$TABLE_COUNT" -gt 5 ]; then
|
||||
echo "Database tables verified"
|
||||
else
|
||||
echo "Not enough tables found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload install log
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: install-log-${{ matrix.scenario }}
|
||||
path: install-output.log
|
||||
retention-days: 7
|
||||
@@ -7,14 +7,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& docker-php-ext-install mysqli bcmath intl gd \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& a2enmod rewrite
|
||||
&& a2enmod rewrite \
|
||||
&& sed -i 's/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
|
||||
|
||||
RUN echo "date.timezone = \"\${PHP_TIMEZONE}\"" > /usr/local/etc/php/conf.d/timezone.ini
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=www-data:www-data . /app
|
||||
RUN chmod 750 /app/writable/logs /app/writable/uploads /app/writable/cache /app/public/uploads /app/public/uploads/item_pics \
|
||||
&& chmod 640 /app/writable/uploads/importCustomers.csv \
|
||||
RUN chmod 770 /app/writable/uploads /app/writable/logs /app/writable/cache \
|
||||
&& mkdir -p /app/public/uploads/item_pics \
|
||||
&& chown www-data:www-data /app/public/uploads/item_pics \
|
||||
&& chmod 640 /app/.env \
|
||||
&& ln -s /app/*[^public] /var/www \
|
||||
&& rm -rf /var/www/html \
|
||||
&& ln -nsf /app/public /var/www/html
|
||||
|
||||
72
INSTALL.md
72
INSTALL.md
@@ -102,73 +102,5 @@ Do **not** use below command on live deployments unless you want to tear everyth
|
||||
|
||||
## Cloud install
|
||||
|
||||
### Recommended: DigitalOcean
|
||||
|
||||
Sign up through [our referral link](https://m.do.co/c/ac38c262507b) to get a [**$100, 60-day credit**](https://m.do.co/c/ac38c262507b).
|
||||
|
||||
1. Create an Ubuntu 20.04+ or 22.04+ droplet
|
||||
2. SSH into your server: `ssh root@<your-droplet-ip>`
|
||||
3. Run the one-line installer:
|
||||
```bash
|
||||
curl -sSL https://opensourcepos.org/install | sudo bash
|
||||
```
|
||||
|
||||
The installer will:
|
||||
- Install Apache, MariaDB, PHP 8.2 and required extensions
|
||||
- Download the **latest stable release** of OSPOS from GitHub
|
||||
- Create a database with secure random password
|
||||
- Configure OSPOS and Apache
|
||||
- **Set up SSL/TLS certificates** (interactive prompt or environment variables)
|
||||
- Display login credentials after completion
|
||||
|
||||
**Interactive Mode (Recommended for first-time users):**
|
||||
|
||||
When run without environment variables, the installer will prompt you:
|
||||
1. Whether to configure SSL (recommended for production)
|
||||
2. Your domain name (e.g., `pos.example.com`)
|
||||
3. Your email for Let's Encrypt (for production SSL)
|
||||
|
||||
```bash
|
||||
curl -sSL https://opensourcepos.org/install | sudo bash
|
||||
# Script will ask:
|
||||
# - Configure SSL? (y/n)
|
||||
# - Domain name: pos.example.com
|
||||
# - Email for Let's Encrypt: admin@example.com
|
||||
```
|
||||
|
||||
**Non-Interactive Mode (for automation):**
|
||||
|
||||
```bash
|
||||
# Development (no SSL)
|
||||
curl -sSL https://opensourcepos.org/install | APACHE_SERVER_NAME=localhost sudo -E bash
|
||||
|
||||
# Production with Let's Encrypt SSL
|
||||
curl -sSL https://opensourcepos.org/install | APACHE_SERVER_NAME=pos.example.com SSL_EMAIL=admin@example.com sudo -E bash
|
||||
|
||||
# Custom database password
|
||||
curl -sSL https://opensourcepos.org/install | DB_PASS=securepassword APACHE_SERVER_NAME=pos.example.com SSL_EMAIL=admin@example.com sudo -E bash
|
||||
```
|
||||
|
||||
**Environment variables:**
|
||||
- `DB_HOST` - Database host (default: localhost)
|
||||
- `DB_NAME` - Database name (default: ospos)
|
||||
- `DB_USER` - Database user (default: ospos)
|
||||
- `DB_PASS` - Database password (default: auto-generated)
|
||||
- `MYSQL_ROOT_PASS` - MariaDB root password (default: empty/no password)
|
||||
- `OSPOS_DIR` - Installation directory (default: /var/www/ospos)
|
||||
- `OSPOS_VERSION` - OSPOS version to install (default: latest stable release)
|
||||
- `PHP_VERSION` - PHP version (default: 8.2)
|
||||
- `APACHE_SERVER_NAME` - Server hostname (default: localhost, or set interactively)
|
||||
- `SSL_EMAIL` - Email for Let's Encrypt. When set, enables production SSL with auto-renewal
|
||||
- `SSL_DOMAIN` - Alternative to `APACHE_SERVER_NAME` for SSL certificate domain
|
||||
|
||||
> **Testing:** This installer is tested with each commit via our CI workflow. A fresh Ubuntu container is spawned, the script runs to completion, and basic sanity checks verify the installation. For production deployments, we recommend testing on a staging server first. If you encounter issues, please [open an issue](https://github.com/opensourcepos/opensourcepos/issues/new?template=bug_report.yml) with your server version and error output.
|
||||
|
||||
> **Note:** If the short URL is unavailable, use the direct GitHub URL:
|
||||
> ```bash
|
||||
> curl -sSL https://raw.githubusercontent.com/opensourcepos/opensourcepos/master/scripts/install-ubuntu.sh | sudo bash
|
||||
> ```
|
||||
|
||||
For other cloud providers or manual installation, see the [detailed installation guide](https://github.com/opensourcepos/opensourcepos/wiki/Getting-Started-installations) in the wiki.
|
||||
|
||||
**Important:** Change the default password after first login!
|
||||
If you choose DigitalOcean:
|
||||
[Through this link](https://m.do.co/c/ac38c262507b), you will get a [**free $100, 60-day credit**](https://m.do.co/c/ac38c262507b). [Check the wiki](https://github.com/opensourcepos/opensourcepos/wiki/Getting-Started-installations) for further instructions on how to install the necessary components.
|
||||
|
||||
131
SECURITY.md
131
SECURITY.md
@@ -5,9 +5,8 @@
|
||||
- [Supported Versions](#supported-versions)
|
||||
- [Security Advisories](#security-advisories)
|
||||
- [Reporting a Vulnerability](#reporting-a-vulnerability)
|
||||
- [Disclosure Process](#disclosure-process)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow update -->
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
# Security Policy
|
||||
|
||||
@@ -22,116 +21,26 @@ We release patches for security vulnerabilities.
|
||||
|
||||
## Security Advisories
|
||||
|
||||
For a complete list of published and draft security advisories with CVE details, see our [GitHub Security Advisories page](https://github.com/opensourcepos/opensourcepos/security/advisories).
|
||||
The following security vulnerabilities have been published:
|
||||
|
||||
### High Severity
|
||||
|
||||
| CVE | Vulnerability | CVSS | Published | Fixed In | Credit |
|
||||
|-----|--------------|------|-----------|----------|--------|
|
||||
| [CVE-2025-68434](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-wjm4-hfwg-5w5r) | CSRF leading to Admin Creation | 8.8 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos |
|
||||
| [CVE-2025-68147](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-xgr7-7pvw-fpmh) | Stored XSS in Return Policy | 8.1 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos |
|
||||
| [CVE-2025-66924](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-gv8j-f6gq-g59m) | Stored XSS in Item Kits | 7.2 | 2026-03-04 | 3.4.2 | @hungnqdz, @omkaryepre |
|
||||
|
||||
### Medium Severity
|
||||
|
||||
| CVE | Vulnerability | CVSS | Published | Fixed In | Credit |
|
||||
|-----|--------------|------|-----------|----------|--------|
|
||||
| [CVE-2025-68658](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-32r8-8r9r-9chw) | Stored XSS in Company Name | 4.3 | 2026-01-13 | 3.4.2 | @hungnqdz |
|
||||
|
||||
For a complete list including draft advisories, see our [GitHub Security Advisories page](https://github.com/opensourcepos/opensourcepos/security/advisories).
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
**Option 1: GitHub Security Advisory (Preferred)**
|
||||
Please report (suspected) security vulnerabilities to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**.
|
||||
|
||||
1. Create a draft security advisory directly on GitHub:
|
||||
- Go to https://github.com/opensourcepos/opensourcepos/security/advisories
|
||||
- Click "New draft security advisory"
|
||||
- Fill in the vulnerability details using our [template below](#vulnerability-template)
|
||||
- Submit as **draft** (not published)
|
||||
|
||||
2. Notify us for triage:
|
||||
- Send an email to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)** with:
|
||||
- Subject: `[GHSA] Brief description of vulnerability`
|
||||
- Link to the draft advisory
|
||||
- Brief summary
|
||||
|
||||
**Option 2: Email Report**
|
||||
|
||||
Send vulnerability details to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**.
|
||||
|
||||
You will receive a response within 48 hours. Confirmed vulnerabilities will be patched within a few days depending on complexity.
|
||||
|
||||
## Disclosure Process
|
||||
|
||||
### Timeline
|
||||
|
||||
| Step | Timeline | Action |
|
||||
|------|----------|--------|
|
||||
| 1. Report received | Day 0 | We acknowledge within 48 hours |
|
||||
| 2. Triage & confirmation | Day 1-3 | We validate the vulnerability |
|
||||
| 3. Fix development | Day 3-7 | We develop and test the fix |
|
||||
| 4. Patch release | Day 7-10 | We release a security patch |
|
||||
| 5. CVE request | Day 7-14 | We request CVE from GitHub (if applicable) |
|
||||
| 6. Advisory published | Day 14 | We publish the advisory with credit |
|
||||
| 7. Public disclosure | Day 14+ | Full disclosure after patch release |
|
||||
|
||||
### CVE Process
|
||||
|
||||
**We request CVE identifiers through GitHub's security advisory system.** This is the preferred and easiest method:
|
||||
|
||||
1. After we confirm and fix the vulnerability, we'll request a CVE through GitHub
|
||||
2. GitHub coordinates with MITRE on our behalf
|
||||
3. The CVE is automatically linked to the advisory
|
||||
4. You'll be credited as the reporter in the published advisory
|
||||
|
||||
**Already have a CVE?** If you've already obtained a CVE from another source (e.g., VulDB, CVE.MITRE.ORG), please include it in your report or advisory. We'll update our advisory to reference the existing CVE.
|
||||
|
||||
### No Bug Bounty Program
|
||||
|
||||
**Important:** Open Source Point of Sale does not offer a bug bounty program.
|
||||
|
||||
- All security research and vulnerability triage is done on a **voluntary basis** in our free time
|
||||
- We do not offer monetary rewards for vulnerability reports
|
||||
- We do credit reporters in published advisories (unless anonymity is requested)
|
||||
- We greatly appreciate the security research community's efforts to help improve project security
|
||||
|
||||
### Security Best Practices for Researchers
|
||||
|
||||
- **Do not** access, modify, or delete data that doesn't belong to you
|
||||
- **Do not** perform denial of service attacks
|
||||
- **Do not** publicly disclose vulnerabilities before we've had time to fix them
|
||||
- **Do** provide sufficient information to reproduce the vulnerability
|
||||
- **Do** allow us reasonable time to fix before public disclosure
|
||||
- **Do** report through official channels (GitHub advisories or email)
|
||||
|
||||
### Vulnerability Template
|
||||
|
||||
When creating a draft advisory, please include:
|
||||
|
||||
```
|
||||
## Summary
|
||||
[Brief description of the vulnerability]
|
||||
|
||||
## Impact
|
||||
- **Confidentiality:** [High/Medium/Low - what data can be exposed]
|
||||
- **Integrity:** [High/Medium/Low - what can be modified]
|
||||
- **Availability:** [High/Medium/Low - service disruption potential]
|
||||
- **Privilege Required:** [None/Low/High - authentication level needed]
|
||||
- **CVSS v3.1:** [Score] ([Vector string])
|
||||
|
||||
## Details
|
||||
[Technical details about the vulnerability]
|
||||
|
||||
**Affected Code:**
|
||||
```php
|
||||
// Path to affected file and vulnerable code
|
||||
```
|
||||
|
||||
**Attack Vector:**
|
||||
[How an attacker can exploit this]
|
||||
|
||||
## Proof of Concept
|
||||
```bash
|
||||
# Steps to reproduce
|
||||
```
|
||||
|
||||
## Patch
|
||||
[Suggested fix or approach]
|
||||
|
||||
## Affected Versions
|
||||
- OpenSourcePOS X.Y.Z and earlier
|
||||
|
||||
## Credit
|
||||
[Your GitHub username or preferred name]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Thank you to all security researchers who have contributed to making Open Source Point of Sale more secure.** Your voluntary efforts help protect thousands of users worldwide and contribute to a safer, more trustworthy free and open-source software ecosystem. We deeply appreciate your responsible disclosure and the time you invest in improving our project.
|
||||
|
||||
If you've reported a vulnerability and would like to discuss CVE coordination or have questions about the process, please reach out to us at [jeroen@steganos.dev](mailto:jeroen@steganos.dev).
|
||||
You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
|
||||
@@ -58,9 +58,9 @@ class App extends BaseConfig
|
||||
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
|
||||
* If you want to accept multiple Hostnames, set this.
|
||||
*
|
||||
* Or via environment variable (useful for Docker/Compose):
|
||||
* ALLOWED_HOSTNAMES=example.com,www.example.com
|
||||
*
|
||||
* E.g.,
|
||||
* When your site URL ($baseURL) is 'http://example.com/', and your site
|
||||
* also accepts 'http://media.example.com/' and 'http://accounts.example.com/':
|
||||
* ['media.example.com', 'accounts.example.com']
|
||||
*
|
||||
* @var list<string>
|
||||
@@ -286,11 +286,7 @@ class App extends BaseConfig
|
||||
|
||||
// Solution for CodeIgniter 4 limitation: arrays cannot be set from .env
|
||||
// See: https://github.com/codeigniter4/CodeIgniter4/issues/7311
|
||||
// Support both: app.allowedHostnames (from .env) and ALLOWED_HOSTNAMES (from environment/Docker)
|
||||
$envAllowedHostnames = getenv('ALLOWED_HOSTNAMES');
|
||||
if ($envAllowedHostnames === false || trim($envAllowedHostnames) === '') {
|
||||
$envAllowedHostnames = getenv('app.allowedHostnames');
|
||||
}
|
||||
$envAllowedHostnames = getenv('app.allowedHostnames');
|
||||
if ($envAllowedHostnames !== false && trim($envAllowedHostnames) !== '') {
|
||||
$this->allowedHostnames = array_values(array_filter(
|
||||
array_map('trim', explode(',', $envAllowedHostnames)),
|
||||
@@ -331,8 +327,9 @@ class App extends BaseConfig
|
||||
$errorMessage =
|
||||
'Security: allowedHostnames is not configured. ' .
|
||||
'Host header injection protection is disabled. ' .
|
||||
'Set app.allowedHostnames in your .env file or ALLOWED_HOSTNAMES environment variable. ' .
|
||||
'Example: app.allowedHostnames = "example.com,www.example.com" ' .
|
||||
'Either set app.allowedHostnames in your .env file ' .
|
||||
'(e.g., app.allowedHostnames = "example.com,www.example.com") ' .
|
||||
'or configure $allowedHostnames in app/Config/App.php. ' .
|
||||
'Received Host: ' . $httpHost;
|
||||
|
||||
// Production: Fail explicitly to prevent silent security vulnerabilities
|
||||
|
||||
@@ -48,8 +48,7 @@ class OSPOS extends BaseConfig
|
||||
$this->settings = [
|
||||
'language' => 'english',
|
||||
'language_code' => 'en',
|
||||
'company' => 'Home',
|
||||
'barcode_type' => 'Code39'
|
||||
'company' => 'Home'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,9 +924,7 @@ class Config extends Secure_Controller
|
||||
public function postSaveReceipt(): ResponseInterface
|
||||
{
|
||||
$batch_save_data = [
|
||||
'receipt_template' => Sale_lib::isValidReceiptTemplate($this->request->getPost('receipt_template'))
|
||||
? $this->request->getPost('receipt_template')
|
||||
: 'receipt_default',
|
||||
'receipt_template' => $this->request->getPost('receipt_template'),
|
||||
'receipt_font_size' => $this->request->getPost('receipt_font_size', FILTER_SANITIZE_NUMBER_INT),
|
||||
'print_delay_autoreturn' => $this->request->getPost('print_delay_autoreturn', FILTER_SANITIZE_NUMBER_INT),
|
||||
'email_receipt_check_behaviour' => $this->request->getPost('email_receipt_check_behaviour'),
|
||||
|
||||
@@ -154,23 +154,8 @@ class Items extends Secure_Controller
|
||||
{
|
||||
helper('file');
|
||||
|
||||
// Security: Sanitize filename to prevent path traversal
|
||||
// Use basename() to strip directory components and prevent '../' attacks
|
||||
$pic_filename = basename(rawurldecode($pic_filename));
|
||||
$file_extension = strtolower(pathinfo($pic_filename, PATHINFO_EXTENSION));
|
||||
|
||||
// Validate file extension against system-configured allowed image types
|
||||
// Handle both legacy pipe-separated and current comma-separated formats
|
||||
// Fallback to types that GD library can process for thumbnail generation
|
||||
$allowed_types = $this->config['image_allowed_types'] ?? 'jpg,jpeg,gif,png,webp,bmp,tif,tiff';
|
||||
$allowed_extensions = strpos($allowed_types, '|') !== false
|
||||
? explode('|', $allowed_types)
|
||||
: explode(',', $allowed_types);
|
||||
|
||||
if (!in_array($file_extension, $allowed_extensions, true)) {
|
||||
return $this->response->setStatusCode(400)->setBody('Invalid file type');
|
||||
}
|
||||
|
||||
$pic_filename = rawurldecode($pic_filename);
|
||||
$file_extension = pathinfo($pic_filename, PATHINFO_EXTENSION);
|
||||
$images = glob("./uploads/item_pics/$pic_filename");
|
||||
$base_path = './uploads/item_pics/' . pathinfo($pic_filename, PATHINFO_FILENAME);
|
||||
|
||||
@@ -1055,20 +1040,14 @@ class Items extends Secure_Controller
|
||||
});
|
||||
|
||||
if (!$isFailedRow && $this->item->save_value($itemData, $itemId)) {
|
||||
if (!$this->save_tax_data($row, $itemData)) {
|
||||
$isFailedRow = true;
|
||||
}
|
||||
if (!$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId)) {
|
||||
$isFailedRow = true;
|
||||
}
|
||||
$this->save_tax_data($row, $itemData);
|
||||
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
|
||||
$csvAttributeValues = $this->extractAttributeData($row);
|
||||
if (!$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData)) {
|
||||
$isFailedRow = true;
|
||||
}
|
||||
$isFailedRow = !$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData);
|
||||
if ($isFailedRow) {
|
||||
$failedRow = $key + 2;
|
||||
$failCodes[] = $failedRow;
|
||||
log_message('error', "CSV Item import failed on line $failedRow while saving item.");
|
||||
log_message('error', "CSV Item import failed on line $failedRow while saving attributes.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1258,15 +1237,13 @@ class Items extends Secure_Controller
|
||||
* @param array $item_data
|
||||
* @param array $allowed_locations
|
||||
* @param int $employee_id
|
||||
* @return bool Returns true on success, false on failure
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
private function save_inventory_quantities(array $row, array $item_data, array $allowed_locations, int $employee_id): bool
|
||||
private function save_inventory_quantities(array $row, array $item_data, array $allowed_locations, int $employee_id): void
|
||||
{
|
||||
// Quantities & Inventory Section
|
||||
$comment = lang('Items.inventory_CSV_import_quantity');
|
||||
$is_update = (bool)$row['Id'];
|
||||
$success = true;
|
||||
|
||||
foreach ($allowed_locations as $location_id => $location_name) {
|
||||
$item_quantity_data = ['item_id' => $item_data['item_id'], 'location_id' => $location_id];
|
||||
@@ -1280,22 +1257,20 @@ class Items extends Secure_Controller
|
||||
|
||||
if (!empty($row["location_$location_name"]) || $row["location_$location_name"] === '0') {
|
||||
$item_quantity_data['quantity'] = $row["location_$location_name"];
|
||||
$success &= $this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
$this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
|
||||
$csv_data['trans_inventory'] = $row["location_$location_name"];
|
||||
$success &= (bool)$this->inventory->insert($csv_data, false);
|
||||
$this->inventory->insert($csv_data, false);
|
||||
} elseif ($is_update) {
|
||||
continue;
|
||||
return;
|
||||
} else {
|
||||
$item_quantity_data['quantity'] = 0;
|
||||
$success &= $this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
$this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
|
||||
$csv_data['trans_inventory'] = 0;
|
||||
$success &= (bool)$this->inventory->insert($csv_data, false);
|
||||
$this->inventory->insert($csv_data, false);
|
||||
}
|
||||
}
|
||||
|
||||
return (bool)$success;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1303,9 +1278,8 @@ class Items extends Secure_Controller
|
||||
*
|
||||
* @param array $row
|
||||
* @param array $item_data
|
||||
* @return bool Returns true on success, false on failure
|
||||
*/
|
||||
private function save_tax_data(array $row, array $item_data): bool
|
||||
private function save_tax_data(array $row, array $item_data): void
|
||||
{
|
||||
$items_taxes_data = [];
|
||||
|
||||
@@ -1317,11 +1291,9 @@ class Items extends Secure_Controller
|
||||
$items_taxes_data[] = ['name' => $row['Tax 2 Name'], 'percent' => $row['Tax 2 Percent']];
|
||||
}
|
||||
|
||||
if (!empty($items_taxes_data)) {
|
||||
return $this->item_taxes->save_value($items_taxes_data, $item_data['item_id']);
|
||||
if (isset($items_taxes_data)) {
|
||||
$this->item_taxes->save_value($items_taxes_data, $item_data['item_id']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1246,15 +1246,13 @@ class Reports extends Secure_Controller
|
||||
public function get_payment_type(): array
|
||||
{
|
||||
return [
|
||||
'all' => lang('Common.none_selected_text'),
|
||||
'cash' => lang('Sales.cash'),
|
||||
'due' => lang('Sales.due'),
|
||||
'check' => lang('Sales.check'),
|
||||
'credit' => lang('Sales.credit'),
|
||||
'debit' => lang('Sales.debit'),
|
||||
'bank_transfer' => lang('Sales.bank_transfer'),
|
||||
'wallet' => lang('Sales.wallet'),
|
||||
'invoices' => lang('Sales.invoice')
|
||||
'all' => lang('Common.none_selected_text'),
|
||||
'cash' => lang('Sales.cash'),
|
||||
'due' => lang('Sales.due'),
|
||||
'check' => lang('Sales.check'),
|
||||
'credit' => lang('Sales.credit'),
|
||||
'debit' => lang('Sales.debit'),
|
||||
'invoices' => lang('Sales.invoice')
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -93,8 +93,6 @@ class Sales extends Secure_Controller
|
||||
'only_check' => lang('Sales.check_filter'),
|
||||
'only_creditcard' => lang('Sales.credit_filter'),
|
||||
'only_debit' => lang('Sales.debit'),
|
||||
'only_bank_transfer'=> lang('Sales.bank_transfer'),
|
||||
'only_wallet' => lang('Sales.wallet'),
|
||||
'only_invoices' => lang('Sales.invoice_filter'),
|
||||
'selected_customer' => lang('Sales.selected_customer')
|
||||
];
|
||||
@@ -158,8 +156,6 @@ class Sales extends Secure_Controller
|
||||
'selected_customer' => false,
|
||||
'only_creditcard' => false,
|
||||
'only_debit' => false,
|
||||
'only_bank_transfer'=> false,
|
||||
'only_wallet' => false,
|
||||
'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT),
|
||||
'is_valid_receipt' => $this->sale->is_valid_receipt($search)
|
||||
];
|
||||
@@ -908,14 +904,6 @@ class Sales extends Secure_Controller
|
||||
return $this->_reload($data);
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
|
||||
// Validate receipt template to prevent path traversal
|
||||
$receipt_template = $this->config['receipt_template'] ?? '';
|
||||
if (!Sale_lib::isValidReceiptTemplate($receipt_template)) {
|
||||
$receipt_template = 'receipt_default';
|
||||
}
|
||||
$data['receipt_template_view'] = $receipt_template;
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/receipt', $data);
|
||||
}
|
||||
@@ -1171,13 +1159,6 @@ class Sales extends Secure_Controller
|
||||
}
|
||||
$data['invoice_view'] = $invoice_type;
|
||||
|
||||
// Validate receipt template to prevent path traversal
|
||||
$receipt_template = $this->config['receipt_template'] ?? '';
|
||||
if (!Sale_lib::isValidReceiptTemplate($receipt_template)) {
|
||||
$receipt_template = 'receipt_default';
|
||||
}
|
||||
$data['receipt_template_view'] = $receipt_template;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
@@ -272,9 +272,6 @@ function get_payment_options(): array
|
||||
$payments[lang('Sales.upi')] = lang('Sales.upi');
|
||||
}
|
||||
|
||||
$payments[lang('Sales.bank_transfer')] = lang('Sales.bank_transfer');
|
||||
$payments[lang('Sales.wallet')] = lang('Sales.wallet');
|
||||
|
||||
return $payments;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ return [
|
||||
"amount_due" => "Amount Due",
|
||||
"amount_tendered" => "Amount Tendered",
|
||||
"authorized_signature" => "Authorised Signature",
|
||||
"bank_transfer" => "Bank Transfer",
|
||||
"cancel_sale" => "Cancel",
|
||||
"cash" => "Cash",
|
||||
"cash_1" => "",
|
||||
@@ -224,7 +223,6 @@ return [
|
||||
"update" => "Update",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wallet" => "Wallet",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Work Order",
|
||||
"work_order_number" => "Work Order Number",
|
||||
|
||||
@@ -9,7 +9,6 @@ return [
|
||||
"amount_due" => "Amount Due",
|
||||
"amount_tendered" => "Amount Tendered",
|
||||
"authorized_signature" => "Authorized Signature",
|
||||
"bank_transfer" => "Bank Transfer",
|
||||
"cancel_sale" => "Cancel",
|
||||
"cash" => "Cash",
|
||||
"cash_1" => "",
|
||||
@@ -224,7 +223,6 @@ return [
|
||||
"update" => "Update",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wallet" => "Wallet",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Work Order",
|
||||
"work_order_number" => "Work Order Number",
|
||||
|
||||
@@ -9,7 +9,6 @@ return [
|
||||
"amount_due" => "Monto Adeudado",
|
||||
"amount_tendered" => "Cantidad Recibida",
|
||||
"authorized_signature" => "Firma Autorizada",
|
||||
"bank_transfer" => "Transferencia Bancaria",
|
||||
"cancel_sale" => "Cancelar Venta",
|
||||
"cash" => "Efectivo",
|
||||
"cash_1" => "1",
|
||||
@@ -223,7 +222,6 @@ return [
|
||||
"update" => "Editar",
|
||||
"upi" => "PIN UPI",
|
||||
"visa" => "Tarjeta Visa",
|
||||
"wallet" => "Monedero",
|
||||
"wholesale" => "Precio al por mayor",
|
||||
"work_order" => "Orden trabajo",
|
||||
"work_order_number" => "Numero Orden Trabajo",
|
||||
|
||||
@@ -9,7 +9,6 @@ return [
|
||||
"amount_due" => "Monto de adeudo",
|
||||
"amount_tendered" => "Cantidad Recibida",
|
||||
"authorized_signature" => "Firma Autorizada",
|
||||
"bank_transfer" => "Transferencia Bancaria",
|
||||
"cancel_sale" => "Cancelar",
|
||||
"cash" => "Efectivo",
|
||||
"cash_1" => "",
|
||||
@@ -223,7 +222,6 @@ return [
|
||||
"update" => "Actualizar",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wallet" => "Monedero",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Orden de trabajo",
|
||||
"work_order_number" => "Número de orden de trabajo",
|
||||
|
||||
@@ -9,7 +9,6 @@ return [
|
||||
"amount_due" => "Montant à Payer",
|
||||
"amount_tendered" => "Montant Présenté",
|
||||
"authorized_signature" => "Signature autorisée",
|
||||
"bank_transfer" => "Virement Bancaire",
|
||||
"cancel_sale" => "Annuler la Vente",
|
||||
"cash" => "Espèce",
|
||||
"cash_1" => "",
|
||||
@@ -223,7 +222,6 @@ return [
|
||||
"update" => "Éditer",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wallet" => "Portefeuille",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Commande de travail",
|
||||
"work_order_number" => "Numéro de commande",
|
||||
|
||||
@@ -108,11 +108,6 @@ class Sale_lib
|
||||
'custom_tax_invoice'
|
||||
];
|
||||
|
||||
private const ALLOWED_RECEIPT_TEMPLATES = [
|
||||
'receipt_default',
|
||||
'receipt_short'
|
||||
];
|
||||
|
||||
public function get_invoice_type_options(): array
|
||||
{
|
||||
$invoice_types = [];
|
||||
@@ -166,11 +161,6 @@ class Sale_lib
|
||||
return in_array($invoice_type, self::ALLOWED_INVOICE_TYPES, true);
|
||||
}
|
||||
|
||||
public static function isValidReceiptTemplate(string $receipt_template): bool
|
||||
{
|
||||
return in_array($receipt_template, self::ALLOWED_RECEIPT_TEMPLATES, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
|
||||
@@ -601,10 +601,6 @@ class Attribute extends Model
|
||||
*/
|
||||
public function saveAttributeLink(int $itemId, int $definitionId, int $attributeId): bool
|
||||
{
|
||||
if ($attributeId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$normalizedItemId = empty($itemId) ? null : $itemId;
|
||||
$normalizedAttributeId = empty($attributeId) ? null : $attributeId;
|
||||
|
||||
|
||||
@@ -294,9 +294,7 @@ class Receiving extends Model
|
||||
lang('Sales.check') => lang('Sales.check'),
|
||||
lang('Sales.debit') => lang('Sales.debit'),
|
||||
lang('Sales.credit') => lang('Sales.credit'),
|
||||
lang('Sales.due') => lang('Sales.due'),
|
||||
lang('Sales.bank_transfer') => lang('Sales.bank_transfer'),
|
||||
lang('Sales.wallet') => lang('Sales.wallet')
|
||||
lang('Sales.due') => lang('Sales.due')
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -33,16 +33,14 @@ class Summary_sales_taxes extends Summary_report
|
||||
* @param object $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function _where(array $inputs, object &$builder): void
|
||||
protected function _where(array $inputs, object &$builder): void // TODO: hungarian notation
|
||||
{
|
||||
$builder->where('sales.sale_status', COMPLETED);
|
||||
|
||||
if (empty($this->config['date_or_time_format'])) {
|
||||
$builder->where('DATE(sales.sale_time) >=', $inputs['start_date']);
|
||||
$builder->where('DATE(sales.sale_time) <=', $inputs['end_date']);
|
||||
if (empty($this->config['date_or_time_format'])) { // TODO: Duplicated code
|
||||
$builder->where('DATE(sales.sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
|
||||
} else {
|
||||
$builder->where('sales.sale_time >=', $inputs['start_date']);
|
||||
$builder->where('sales.sale_time <=', $inputs['end_date']);
|
||||
$builder->where('sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +53,9 @@ class Summary_sales_taxes extends Summary_report
|
||||
$builder = $this->db->table('sales_taxes');
|
||||
|
||||
if (empty($this->config['date_or_time_format'])) {
|
||||
$builder->where('DATE(sale_time) >=', $inputs['start_date']);
|
||||
$builder->where('DATE(sale_time) <=', $inputs['end_date']);
|
||||
$builder->where('DATE(sale_time) BETWEEN ' . $inputs['start_date'] . ' AND ' . $inputs['end_date']);
|
||||
} else {
|
||||
$builder->where('sale_time >=', $inputs['start_date']);
|
||||
$builder->where('sale_time <=', $inputs['end_date']);
|
||||
$builder->where('sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
}
|
||||
|
||||
$builder->select('reporting_authority, jurisdiction_name, tax_category, tax_rate, SUM(sale_tax_amount) AS tax');
|
||||
|
||||
@@ -277,14 +277,6 @@ class Sale extends Model
|
||||
$builder->like('payment_type', lang('Sales.debit'));
|
||||
}
|
||||
|
||||
if ($filters['only_bank_transfer']) {
|
||||
$builder->like('payment_type', lang('Sales.bank_transfer'));
|
||||
}
|
||||
|
||||
if ($filters['only_wallet']) {
|
||||
$builder->like('payment_type', lang('Sales.wallet'));
|
||||
}
|
||||
|
||||
$builder->groupBy('payment_type');
|
||||
|
||||
$payments = $builder->get()->getResultArray();
|
||||
@@ -1517,13 +1509,5 @@ class Sale extends Model
|
||||
if ($filters['only_check']) {
|
||||
$builder->like('payments.payment_type', lang('Sales.check'));
|
||||
}
|
||||
|
||||
if ($filters['only_bank_transfer']) {
|
||||
$builder->like('payments.payment_type', lang('Sales.bank_transfer'));
|
||||
}
|
||||
|
||||
if ($filters['only_wallet']) {
|
||||
$builder->like('payments.payment_type', lang('Sales.wallet'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
/**
|
||||
* @var int $sale_id_num
|
||||
* @var bool $print_after_sale
|
||||
* @var string $receipt_template_view
|
||||
* @var array $config
|
||||
*/
|
||||
|
||||
use App\Models\Employee;
|
||||
|
||||
$template = $receipt_template_view ?? 'receipt_default';
|
||||
|
||||
?>
|
||||
|
||||
<?= view('partial/header') ?>
|
||||
@@ -64,6 +61,6 @@ if (isset($error_message)) {
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= view('sales/' . $template) ?>
|
||||
<?= view('sales/' . $config['receipt_template']) ?>
|
||||
|
||||
<?= view('partial/footer') ?>
|
||||
|
||||
@@ -46,7 +46,6 @@ services:
|
||||
- .:/app
|
||||
environment:
|
||||
- CI_ENVIRONMENT=development
|
||||
- ALLOWED_HOSTNAMES=localhost
|
||||
- MYSQL_USERNAME=admin
|
||||
- MYSQL_PASSWORD=pointofsale
|
||||
- MYSQL_DB_NAME=ospos
|
||||
|
||||
@@ -16,7 +16,6 @@ services:
|
||||
- logs:/app/writable/logs
|
||||
environment:
|
||||
- CI_ENVIRONMENT=production
|
||||
- ALLOWED_HOSTNAMES=localhost
|
||||
- FORCE_HTTPS=false
|
||||
- PHP_TIMEZONE=UTC
|
||||
- MYSQL_USERNAME=admin
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
COLOR_RED='\033[0;31m'
|
||||
COLOR_GREEN='\033[0;32m'
|
||||
COLOR_YELLOW='\033[1;33m'
|
||||
COLOR_BLUE='\033[0;34m'
|
||||
COLOR_RESET='\033[0m'
|
||||
|
||||
echo -e "${COLOR_BLUE}╔══════════════════════════════════════════════════════════╗${COLOR_RESET}"
|
||||
echo -e "${COLOR_BLUE}║ Open Source Point of Sale - Ubuntu Installer ║${COLOR_RESET}"
|
||||
echo -e "${COLOR_BLUE}║ Version 3.4+ ║${COLOR_RESET}"
|
||||
echo -e "${COLOR_BLUE}╚══════════════════════════════════════════════════════════╝${COLOR_RESET}"
|
||||
echo ""
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${COLOR_RED}Please run this script as root or with sudo${COLOR_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
DB_HOST="${DB_HOST:-localhost}"
|
||||
DB_NAME="${DB_NAME:-ospos}"
|
||||
DB_USER="${DB_USER:-ospos}"
|
||||
DB_PASS="${DB_PASS:-$(openssl rand -base64 24)}"
|
||||
OSPOS_DIR="${OSPOS_DIR:-/var/www/ospos}"
|
||||
OSPOS_VERSION="${OSPOS_VERSION:-}"
|
||||
PHP_VERSION="${PHP_VERSION:-8.2}"
|
||||
APACHE_SERVER_NAME="${APACHE_SERVER_NAME:-}"
|
||||
SSL_EMAIL="${SSL_EMAIL:-}"
|
||||
SSL_DOMAIN="${SSL_DOMAIN:-}"
|
||||
MYSQL_ROOT_PASS="${MYSQL_ROOT_PASS:-}"
|
||||
|
||||
# Validate database variables contain only safe characters (alphanumeric, underscore, hyphen, dot)
|
||||
validate_db_vars() {
|
||||
local var_name="$1"
|
||||
local var_value="$2"
|
||||
local pattern='^[a-zA-Z0-9_\-\.]+$'
|
||||
if [[ ! "$var_value" =~ $pattern ]]; then
|
||||
echo -e "${COLOR_RED}Error: ${var_name} contains invalid characters. Only alphanumeric, underscore, hyphen, and dot are allowed.${COLOR_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate critical database variables
|
||||
validate_db_vars "DB_NAME" "$DB_NAME"
|
||||
validate_db_vars "DB_USER" "$DB_USER"
|
||||
validate_db_vars "DB_HOST" "$DB_HOST"
|
||||
|
||||
# Check if running interactively
|
||||
INTERACTIVE=false
|
||||
if [ -t 0 ]; then
|
||||
INTERACTIVE=true
|
||||
fi
|
||||
|
||||
echo -e "${COLOR_YELLOW}Configuration:${COLOR_RESET}"
|
||||
echo -e " Database Name: ${DB_NAME}"
|
||||
echo -e " Database User: ${DB_USER}"
|
||||
echo -e " Database Host: ${DB_HOST}"
|
||||
echo -e " Install Directory: ${OSPOS_DIR}"
|
||||
echo -e " PHP Version: ${PHP_VERSION}"
|
||||
if [ -n "$OSPOS_VERSION" ]; then
|
||||
echo -e " OSPOS Version: ${OSPOS_VERSION}"
|
||||
else
|
||||
echo -e " OSPOS Version: latest"
|
||||
fi
|
||||
if [ -n "$APACHE_SERVER_NAME" ]; then
|
||||
echo -e " Server Name: ${APACHE_SERVER_NAME}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [ -d "$OSPOS_DIR" ]; then
|
||||
echo -e "${COLOR_RED}Installation directory $OSPOS_DIR already exists${COLOR_RESET}"
|
||||
echo -e "${COLOR_YELLOW}Remove it or set OSPOS_DIR environment variable${COLOR_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${COLOR_GREEN}[1/9] Updating system packages...${COLOR_RESET}"
|
||||
apt-get update -qq
|
||||
|
||||
echo -e "${COLOR_GREEN}[2/9] Installing Apache, PHP, and dependencies...${COLOR_RESET}"
|
||||
# Add PHP repository for newer PHP versions if not available in default repos
|
||||
if ! apt-cache policy php${PHP_VERSION} 2>/dev/null | grep -q "Candidate:"; then
|
||||
echo -e "${COLOR_YELLOW}PHP ${PHP_VERSION} not in default repos, adding ondrej/php PPA...${COLOR_RESET}"
|
||||
apt-get install -y -qq software-properties-common
|
||||
add-apt-repository -y ppa:ondrej/php
|
||||
apt-get update -qq
|
||||
fi
|
||||
|
||||
apt-get install -y -qq \
|
||||
apache2 \
|
||||
mariadb-server \
|
||||
mariadb-client \
|
||||
php${PHP_VERSION} \
|
||||
php${PHP_VERSION}-mysql \
|
||||
php${PHP_VERSION}-gd \
|
||||
php${PHP_VERSION}-bcmath \
|
||||
php${PHP_VERSION}-intl \
|
||||
php${PHP_VERSION}-mbstring \
|
||||
php${PHP_VERSION}-curl \
|
||||
php${PHP_VERSION}-xml \
|
||||
php${PHP_VERSION}-zip \
|
||||
git \
|
||||
curl \
|
||||
unzip \
|
||||
openssl
|
||||
|
||||
echo -e "${COLOR_GREEN}[3/9] Starting MariaDB...${COLOR_RESET}"
|
||||
systemctl start mariadb
|
||||
systemctl enable mariadb
|
||||
|
||||
if [ -z "$MYSQL_ROOT_PASS" ]; then
|
||||
echo -e "${COLOR_BLUE}Securing MariaDB installation...${COLOR_RESET}"
|
||||
mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '';"
|
||||
mysql -e "FLUSH PRIVILEGES;"
|
||||
else
|
||||
mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASS}';"
|
||||
fi
|
||||
|
||||
echo -e "${COLOR_GREEN}[4/9] Creating database and user...${COLOR_RESET}"
|
||||
if [ -n "$MYSQL_ROOT_PASS" ]; then
|
||||
mysql -u root -p"${MYSQL_ROOT_PASS}" <<EOF
|
||||
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER IF NOT EXISTS '${DB_USER}'@'${DB_HOST}' IDENTIFIED BY '${DB_PASS}';
|
||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'${DB_HOST}';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
else
|
||||
mysql -u root <<EOF
|
||||
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER IF NOT EXISTS '${DB_USER}'@'${DB_HOST}' IDENTIFIED BY '${DB_PASS}';
|
||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'${DB_HOST}';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo -e "${COLOR_GREEN}[5/9] Downloading OSPOS...${COLOR_RESET}"
|
||||
mkdir -p "$(dirname "$OSPOS_DIR")"
|
||||
cd "$(dirname "$OSPOS_DIR")"
|
||||
|
||||
if [ -z "$OSPOS_VERSION" ]; then
|
||||
OSPOS_VERSION=$(curl -sS https://api.github.com/repos/opensourcepos/opensourcepos/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [ -z "$OSPOS_VERSION" ]; then
|
||||
echo -e "${COLOR_RED}Failed to get latest release version${COLOR_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${COLOR_BLUE}Downloading OSPOS version ${OSPOS_VERSION}...${COLOR_RESET}"
|
||||
ASSET_URL=$(curl -sS "https://api.github.com/repos/opensourcepos/opensourcepos/releases/tags/${OSPOS_VERSION}" | grep '"browser_download_url"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
|
||||
if [ -z "$ASSET_URL" ]; then
|
||||
echo -e "${COLOR_RED}Failed to find release asset for ${OSPOS_VERSION}${COLOR_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -sSL "$ASSET_URL" -o ospos.zip
|
||||
|
||||
if [ ! -f ospos.zip ] || [ ! -s ospos.zip ]; then
|
||||
echo -e "${COLOR_RED}Failed to download OSPOS release ${OSPOS_VERSION}${COLOR_RESET}"
|
||||
rm -f ospos.zip
|
||||
exit 1
|
||||
fi
|
||||
|
||||
unzip -q ospos.zip -d ospos-temp
|
||||
mkdir -p "${OSPOS_DIR}"
|
||||
cp -r ospos-temp/. "${OSPOS_DIR}/"
|
||||
rm -rf ospos-temp ospos.zip
|
||||
|
||||
echo -e "${COLOR_GREEN}Downloaded OSPOS ${OSPOS_VERSION}${COLOR_RESET}"
|
||||
|
||||
echo -e "${COLOR_GREEN}[6/9] Setting up OSPOS...${COLOR_RESET}"
|
||||
cd "${OSPOS_DIR}"
|
||||
|
||||
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 2>/dev/null
|
||||
|
||||
if [ -f "composer.json" ]; then
|
||||
echo -e "${COLOR_BLUE}Installing dependencies...${COLOR_RESET}"
|
||||
composer install --no-dev --optimize-autoloader --no-interaction --quiet 2>/dev/null
|
||||
fi
|
||||
|
||||
echo -e "${COLOR_GREEN}[7/9] Configuring OSPOS...${COLOR_RESET}"
|
||||
if [ -f ".env.example" ]; then
|
||||
cp .env.example .env
|
||||
fi
|
||||
|
||||
if [ -f ".env" ]; then
|
||||
# Escape special characters in password for sed
|
||||
ESCAPED_DB_PASS=$(printf '%s\n' "$DB_PASS" | sed 's/[&/\]/\\&/g')
|
||||
|
||||
sed -i "s|database\.default\.hostname = 'localhost'|database.default.hostname = '${DB_HOST}'|" .env
|
||||
sed -i "s|database\.default\.database = 'ospos'|database.default.database = '${DB_NAME}'|" .env
|
||||
sed -i "s|database\.default\.username = 'admin'|database.default.username = '${DB_USER}'|" .env
|
||||
sed -i "s|database\.default\.password = 'pointofsale'|database.default.password = '${ESCAPED_DB_PASS}'|" .env
|
||||
sed -i "s|CI_ENVIRONMENT = development|CI_ENVIRONMENT = production|" .env
|
||||
|
||||
if grep -q "encryption\.key = ''" .env; then
|
||||
ENCRYPTION_KEY=$(openssl rand -base64 32)
|
||||
ESCAPED_KEY=$(printf '%s\n' "$ENCRYPTION_KEY" | sed 's/[&/\]/\\&/g')
|
||||
sed -i "s|encryption\.key = ''|encryption.key = '${ESCAPED_KEY}'|" .env
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${COLOR_GREEN}[8/9] Importing database schema...${COLOR_RESET}"
|
||||
if [ -n "$MYSQL_ROOT_PASS" ]; then
|
||||
mysql -u root -p"${MYSQL_ROOT_PASS}" ${DB_NAME} < app/Database/database.sql
|
||||
else
|
||||
mysql -u root ${DB_NAME} < app/Database/database.sql
|
||||
fi
|
||||
|
||||
# Interactive SSL configuration
|
||||
if $INTERACTIVE && [ -z "$SSL_EMAIL" ] && [ -z "$APACHE_SERVER_NAME" ]; then
|
||||
echo ""
|
||||
echo -e "${COLOR_BLUE}╔══════════════════════════════════════════════════════════╗${COLOR_RESET}"
|
||||
echo -e "${COLOR_BLUE}║ SSL/TLS Configuration ║${COLOR_RESET}"
|
||||
echo -e "${COLOR_BLUE}╚══════════════════════════════════════════════════════════╝${COLOR_RESET}"
|
||||
echo ""
|
||||
echo -e "${COLOR_YELLOW}SSL provides secure HTTPS access to your OSPOS installation.${COLOR_RESET}"
|
||||
echo -e "${COLOR_YELLOW}For production, we recommend Let's Encrypt (free SSL certificate).${COLOR_RESET}"
|
||||
echo ""
|
||||
|
||||
read -p "Configure SSL? (y/n) [n]: " CONFIGURE_SSL
|
||||
CONFIGURE_SSL=${CONFIGURE_SSL:-n}
|
||||
|
||||
if [[ "$CONFIGURE_SSL" =~ ^[Yy]$ ]]; then
|
||||
read -p "Enter your domain name (e.g., pos.example.com): " SSL_DOMAIN
|
||||
SSL_DOMAIN=${SSL_DOMAIN:-localhost}
|
||||
APACHE_SERVER_NAME=$SSL_DOMAIN
|
||||
|
||||
read -p "Enter your email for Let's Encrypt notifications: " SSL_EMAIL
|
||||
|
||||
if [ -z "$SSL_EMAIL" ]; then
|
||||
echo -e "${COLOR_YELLOW}No email provided. Using self-signed certificate (not recommended for production).${COLOR_RESET}"
|
||||
SSL_TYPE="self-signed"
|
||||
else
|
||||
SSL_TYPE="letsencrypt"
|
||||
fi
|
||||
else
|
||||
APACHE_SERVER_NAME="localhost"
|
||||
SSL_TYPE="none"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set default server name if not provided
|
||||
if [ -z "$APACHE_SERVER_NAME" ]; then
|
||||
APACHE_SERVER_NAME="localhost"
|
||||
fi
|
||||
|
||||
# If SSL_EMAIL is set without SSL_DOMAIN, use APACHE_SERVER_NAME
|
||||
if [ -n "$SSL_EMAIL" ] && [ -z "$SSL_DOMAIN" ] && [ "$APACHE_SERVER_NAME" != "localhost" ]; then
|
||||
SSL_DOMAIN="$APACHE_SERVER_NAME"
|
||||
fi
|
||||
|
||||
echo -e "${COLOR_GREEN}[9/9] Configuring Apache...${COLOR_RESET}"
|
||||
cat > /etc/apache2/sites-available/ospos.conf <<EOF
|
||||
<VirtualHost *:80>
|
||||
ServerName ${APACHE_SERVER_NAME}
|
||||
DocumentRoot ${OSPOS_DIR}/public
|
||||
|
||||
<Directory ${OSPOS_DIR}/public>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog \${APACHE_LOG_DIR}/ospos_error.log
|
||||
CustomLog \${APACHE_LOG_DIR}/ospos_access.log combined
|
||||
</VirtualHost>
|
||||
EOF
|
||||
|
||||
a2enmod rewrite
|
||||
a2dissite 000-default.conf
|
||||
a2ensite ospos.conf
|
||||
|
||||
chown -R www-data:www-data "${OSPOS_DIR}"
|
||||
chmod -R 750 "${OSPOS_DIR}/writable"
|
||||
|
||||
systemctl restart apache2
|
||||
systemctl enable apache2
|
||||
|
||||
# Configure SSL if requested
|
||||
if [ -n "$SSL_EMAIL" ] && [ -n "$SSL_DOMAIN" ]; then
|
||||
# Let's Encrypt SSL
|
||||
echo -e "${COLOR_BLUE}Installing Certbot for Let's Encrypt...${COLOR_RESET}"
|
||||
apt-get install -y -qq certbot python3-certbot-apache
|
||||
|
||||
echo -e "${COLOR_BLUE}Obtaining SSL certificate for ${SSL_DOMAIN}...${COLOR_RESET}"
|
||||
certbot --apache -d ${SSL_DOMAIN} --non-interactive --agree-tos --email ${SSL_EMAIL} --redirect
|
||||
|
||||
echo -e "${COLOR_BLUE}Setting up auto-renewal...${COLOR_RESET}"
|
||||
systemctl enable certbot.timer
|
||||
systemctl start certbot.timer
|
||||
|
||||
PROTOCOL="https"
|
||||
FINAL_URL="https://${SSL_DOMAIN}/"
|
||||
elif [ -n "$SSL_DOMAIN" ]; then
|
||||
# Self-signed SSL
|
||||
echo -e "${COLOR_BLUE}Generating self-signed SSL certificate...${COLOR_RESET}"
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout /etc/ssl/private/ospos-selfsigned.key \
|
||||
-out /etc/ssl/certs/ospos-selfsigned.crt \
|
||||
-subj "/C=US/ST=State/L=City/O=Organization/CN=${SSL_DOMAIN}" 2>/dev/null
|
||||
|
||||
cat > /etc/apache2/sites-available/ospos-ssl.conf <<EOF
|
||||
<VirtualHost *:443>
|
||||
ServerName ${SSL_DOMAIN}
|
||||
DocumentRoot ${OSPOS_DIR}/public
|
||||
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/ssl/certs/ospos-selfsigned.crt
|
||||
SSLCertificateKeyFile /etc/ssl/private/ospos-selfsigned.key
|
||||
|
||||
<Directory ${OSPOS_DIR}/public>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog \${APACHE_LOG_DIR}/ospos_ssl_error.log
|
||||
CustomLog \${APACHE_LOG_DIR}/ospos_ssl_access.log combined
|
||||
</VirtualHost>
|
||||
EOF
|
||||
|
||||
a2enmod ssl
|
||||
a2ensite ospos-ssl.conf
|
||||
|
||||
cat > /etc/apache2/sites-available/ospos.conf <<EOF
|
||||
<VirtualHost *:80>
|
||||
ServerName ${SSL_DOMAIN}
|
||||
Redirect permanent / https://${SSL_DOMAIN}/
|
||||
</VirtualHost>
|
||||
EOF
|
||||
|
||||
a2dissite ospos.conf
|
||||
a2ensite ospos.conf
|
||||
|
||||
PROTOCOL="https"
|
||||
FINAL_URL="https://${SSL_DOMAIN}/"
|
||||
|
||||
echo -e "${COLOR_YELLOW}Note: Your browser will show a security warning for self-signed${COLOR_RESET}"
|
||||
echo -e "${COLOR_YELLOW} certificates. For production, re-run with an email for Let's Encrypt.${COLOR_RESET}"
|
||||
else
|
||||
PROTOCOL="http"
|
||||
FINAL_URL="http://${APACHE_SERVER_NAME}/"
|
||||
fi
|
||||
|
||||
systemctl restart apache2
|
||||
|
||||
# Configure allowed hostnames
|
||||
if [ -f "${OSPOS_DIR}/.env" ]; then
|
||||
sed -i "s|app\.allowedHostnames = ''|app.allowedHostnames = '${APACHE_SERVER_NAME}'|" ${OSPOS_DIR}/.env
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${COLOR_GREEN}╔══════════════════════════════════════════════════════════╗${COLOR_RESET}"
|
||||
echo -e "${COLOR_GREEN}║ Installation Complete! ║${COLOR_RESET}"
|
||||
echo -e "${COLOR_GREEN}╚══════════════════════════════════════════════════════════╝${COLOR_RESET}"
|
||||
echo ""
|
||||
echo -e "${COLOR_YELLOW}Database Credentials:${COLOR_RESET}"
|
||||
echo -e " Database: ${DB_NAME}"
|
||||
echo -e " Username: ${DB_USER}"
|
||||
echo -e " Password: ${DB_PASS}"
|
||||
echo ""
|
||||
echo -e "${COLOR_YELLOW}Login Credentials:${COLOR_RESET}"
|
||||
echo -e " URL: ${FINAL_URL}"
|
||||
if [ -n "$SSL_EMAIL" ]; then
|
||||
echo -e " SSL: Let's Encrypt (auto-renewal enabled)"
|
||||
elif [ -n "$SSL_DOMAIN" ]; then
|
||||
echo -e " SSL: Self-signed certificate"
|
||||
else
|
||||
echo -e " SSL: Not configured (HTTP only)"
|
||||
fi
|
||||
echo -e " Username: admin"
|
||||
echo -e " Password: pointofsale"
|
||||
echo ""
|
||||
echo -e "${COLOR_RED}IMPORTANT: Change the default password after first login!${COLOR_RESET}"
|
||||
echo ""
|
||||
echo -e "${COLOR_BLUE}Configuration file: ${OSPOS_DIR}/.env${COLOR_RESET}"
|
||||
echo ""
|
||||
@@ -18,7 +18,6 @@ class AppTest extends CIUnitTestCase
|
||||
// Clean up environment
|
||||
putenv('CI_ENVIRONMENT');
|
||||
putenv('app.allowedHostnames');
|
||||
putenv('ALLOWED_HOSTNAMES');
|
||||
unset($_SERVER['HTTP_HOST']);
|
||||
}
|
||||
|
||||
@@ -282,106 +281,4 @@ class AppTest extends CIUnitTestCase
|
||||
putenv('app.allowedHostnames');
|
||||
putenv('CI_ENVIRONMENT');
|
||||
}
|
||||
|
||||
public function testAllowedHostnamesEnvVarParsedAsCommaSeparated(): void
|
||||
{
|
||||
// Set ALLOWED_HOSTNAMES environment variable
|
||||
putenv('ALLOWED_HOSTNAMES=example.com,www.example.com,demo.example.com');
|
||||
|
||||
$_SERVER['HTTP_HOST'] = 'www.example.com';
|
||||
$_SERVER['SCRIPT_NAME'] = '/index.php';
|
||||
$_SERVER['HTTPS'] = null;
|
||||
|
||||
$app = new App();
|
||||
|
||||
// Constructor should parse comma-separated values
|
||||
$this->assertEquals(['example.com', 'www.example.com', 'demo.example.com'], $app->allowedHostnames);
|
||||
$this->assertStringContainsString('www.example.com', $app->baseURL);
|
||||
|
||||
// Clean up
|
||||
putenv('ALLOWED_HOSTNAMES');
|
||||
}
|
||||
|
||||
public function testAllowedHostnamesEnvVarTakesPrecedenceOverDotEnv(): void
|
||||
{
|
||||
// Set both environment variables
|
||||
putenv('ALLOWED_HOSTNAMES=allowed1.com,allowed2.com');
|
||||
putenv('app.allowedHostnames=dotenv1.com,dotenv2.com');
|
||||
|
||||
$_SERVER['HTTP_HOST'] = 'allowed1.com';
|
||||
$_SERVER['SCRIPT_NAME'] = '/index.php';
|
||||
$_SERVER['HTTPS'] = null;
|
||||
|
||||
$app = new App();
|
||||
|
||||
// ALLOWED_HOSTNAMES should take precedence
|
||||
$this->assertEquals(['allowed1.com', 'allowed2.com'], $app->allowedHostnames);
|
||||
$this->assertStringContainsString('allowed1.com', $app->baseURL);
|
||||
|
||||
// Clean up
|
||||
putenv('ALLOWED_HOSTNAMES');
|
||||
putenv('app.allowedHostnames');
|
||||
}
|
||||
|
||||
public function testAllowedHostnamesEnvVarFallsBackToDotEnv(): void
|
||||
{
|
||||
// Only set app.allowedHostnames, not ALLOWED_HOSTNAMES
|
||||
putenv('app.allowedHostnames=dotenv1.com,dotenv2.com');
|
||||
|
||||
$_SERVER['HTTP_HOST'] = 'dotenv1.com';
|
||||
$_SERVER['SCRIPT_NAME'] = '/index.php';
|
||||
$_SERVER['HTTPS'] = null;
|
||||
|
||||
$app = new App();
|
||||
|
||||
// Should fall back to app.allowedHostnames
|
||||
$this->assertEquals(['dotenv1.com', 'dotenv2.com'], $app->allowedHostnames);
|
||||
$this->assertStringContainsString('dotenv1.com', $app->baseURL);
|
||||
|
||||
// Clean up
|
||||
putenv('app.allowedHostnames');
|
||||
}
|
||||
|
||||
public function testAllowedHostnamesEnvVarTrimmedWhitespace(): void
|
||||
{
|
||||
// Set environment variable with whitespace
|
||||
putenv('ALLOWED_HOSTNAMES= example.com , www.example.com , demo.example.com ');
|
||||
|
||||
$_SERVER['HTTP_HOST'] = 'example.com';
|
||||
$_SERVER['SCRIPT_NAME'] = '/index.php';
|
||||
$_SERVER['HTTPS'] = null;
|
||||
|
||||
$app = new App();
|
||||
|
||||
// Values should be trimmed
|
||||
$this->assertEquals(['example.com', 'www.example.com', 'demo.example.com'], $app->allowedHostnames);
|
||||
|
||||
// Clean up
|
||||
putenv('ALLOWED_HOSTNAMES');
|
||||
}
|
||||
|
||||
public function testAllowedHostnamesEnvVarFiltersEmptyEntries(): void
|
||||
{
|
||||
// Trailing comma should not produce empty entry
|
||||
putenv('ALLOWED_HOSTNAMES=example.com,');
|
||||
$_SERVER['HTTP_HOST'] = 'example.com';
|
||||
$_SERVER['SCRIPT_NAME'] = '/index.php';
|
||||
$_SERVER['HTTPS'] = null;
|
||||
|
||||
$app = new App();
|
||||
$this->assertEquals(['example.com'], $app->allowedHostnames);
|
||||
|
||||
// Clean up
|
||||
putenv('ALLOWED_HOSTNAMES');
|
||||
|
||||
// Whitespace-only entry should be filtered
|
||||
putenv('ALLOWED_HOSTNAMES=example.com, ,www.example.com');
|
||||
$_SERVER['HTTP_HOST'] = 'example.com';
|
||||
|
||||
$app = new App();
|
||||
$this->assertEquals(['example.com', 'www.example.com'], $app->allowedHostnames);
|
||||
|
||||
// Clean up
|
||||
putenv('ALLOWED_HOSTNAMES');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user