fix production trial issue
Build & Push / Pipeline Tests (push) Successful in 1m49s
Test / Type Check (all packages) (push) Successful in 55s
Build & Push / Build & Push Docker Image (push) Failing after 5m31s
Test / API Unit Tests (push) Successful in 1m20s
Test / Homepage Unit Tests (push) Successful in 48s
Test / Carplace Unit Tests (push) Successful in 45s
Test / Admin Unit Tests (push) Successful in 44s
Test / Dashboard Unit Tests (push) Successful in 46s
Test / API Integration Tests (push) Successful in 1m8s
Build & Push / Pipeline Tests (push) Successful in 1m49s
Test / Type Check (all packages) (push) Successful in 55s
Build & Push / Build & Push Docker Image (push) Failing after 5m31s
Test / API Unit Tests (push) Successful in 1m20s
Test / Homepage Unit Tests (push) Successful in 48s
Test / Carplace Unit Tests (push) Successful in 45s
Test / Admin Unit Tests (push) Successful in 44s
Test / Dashboard Unit Tests (push) Successful in 46s
Test / API Integration Tests (push) Successful in 1m8s
This commit is contained in:
@@ -447,6 +447,95 @@ jobs:
|
||||
ENV_DOCKER_PRODUCTION_RAW_B64="$(printf '%s' "$ENV_DOCKER_PRODUCTION" | base64 | tr -d '\n')"
|
||||
STRIPE_API_KEY_B64="$(printf '%s' "$STRIPE_API_KEY" | base64 | tr -d '\n')"
|
||||
STRIPE_WEBHOOK_SECRET_B64="$(printf '%s' "$STRIPE_WEBHOOK_SECRET" | base64 | tr -d '\n')"
|
||||
validate_stripe_secret_inputs() {
|
||||
local env_file stripe_api_key stripe_webhook_secret
|
||||
|
||||
env_file="$(mktemp)"
|
||||
trap 'rm -f "$env_file"' EXIT
|
||||
|
||||
if [ -n "$ENV_DOCKER_PRODUCTION_B64_CLEAN" ]; then
|
||||
if ! printf '%s' "$ENV_DOCKER_PRODUCTION_B64_CLEAN" | base64 -d > "$env_file"; then
|
||||
echo "::error::ENV_DOCKER_PRODUCTION_B64 is not valid base64"
|
||||
exit 1
|
||||
fi
|
||||
elif [ -n "$ENV_DOCKER_PRODUCTION" ]; then
|
||||
printf '%s' "$ENV_DOCKER_PRODUCTION" > "$env_file"
|
||||
else
|
||||
: > "$env_file"
|
||||
fi
|
||||
|
||||
read_env_key() {
|
||||
local key="$1"
|
||||
awk -F= -v key="$key" '
|
||||
/^[[:space:]]*#/ || index($0, "=") == 0 {
|
||||
next
|
||||
}
|
||||
{
|
||||
name = $1
|
||||
sub(/^[[:space:]]*export[[:space:]]+/, "", name)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
|
||||
if (name == key) {
|
||||
value = substr($0, index($0, "=") + 1)
|
||||
}
|
||||
}
|
||||
END {
|
||||
print value
|
||||
}
|
||||
' "$env_file"
|
||||
}
|
||||
|
||||
normalize_secret() {
|
||||
local value="$1"
|
||||
value="${value%$'\r'}"
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
if [[ "$value" == \"*\" && "$value" == *\" ]]; then
|
||||
value="${value#\"}"
|
||||
value="${value%\"}"
|
||||
elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
|
||||
value="${value#\'}"
|
||||
value="${value%\'}"
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
stripe_api_key="$(normalize_secret "${STRIPE_API_KEY:-$(read_env_key STRIPE_API_KEY)}")"
|
||||
stripe_webhook_secret="$(normalize_secret "${STRIPE_WEBHOOK_SECRET:-$(read_env_key STRIPE_WEBHOOK_SECRET)}")"
|
||||
|
||||
case "$stripe_api_key" in
|
||||
sk_live_*|rk_live_*) ;;
|
||||
"")
|
||||
echo "::error::STRIPE_API_KEY is missing. Add a live sk_live_ or restricted rk_live_ key to the STRIPE_API_KEY secret, ENV_DOCKER_PRODUCTION, or ENV_DOCKER_PRODUCTION_B64."
|
||||
exit 1
|
||||
;;
|
||||
placeholder|replace-with-*|*changeme*|*change-me*)
|
||||
echo "::error::STRIPE_API_KEY is still a placeholder. Replace it with a live sk_live_ or restricted rk_live_ key in Gitea Actions secrets."
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "::error::STRIPE_API_KEY must start with sk_live_ or rk_live_ for production billing."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$stripe_webhook_secret" in
|
||||
whsec_*) ;;
|
||||
"")
|
||||
echo "::error::STRIPE_WEBHOOK_SECRET is missing. Add the Stripe webhook signing secret to the STRIPE_WEBHOOK_SECRET secret, ENV_DOCKER_PRODUCTION, or ENV_DOCKER_PRODUCTION_B64."
|
||||
exit 1
|
||||
;;
|
||||
placeholder|replace-with-*|*changeme*|*change-me*)
|
||||
echo "::error::STRIPE_WEBHOOK_SECRET is still a placeholder. Replace it with the Stripe webhook signing secret in Gitea Actions secrets."
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "::error::STRIPE_WEBHOOK_SECRET must start with whsec_."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
validate_stripe_secret_inputs
|
||||
REGISTRY_USERNAME="${REGISTRY_USERNAME:-${REGISTRY_USER:-}}"
|
||||
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-${REGISTRY_TOKEN:-}}"
|
||||
REGISTRY_PASSWORD_B64="$(printf '%s' "$REGISTRY_PASSWORD" | base64 | tr -d '\n')"
|
||||
|
||||
@@ -375,4 +375,55 @@ describe('menu.service', () => {
|
||||
'settings',
|
||||
])
|
||||
})
|
||||
|
||||
it('adds baseline routes when a full-access trial menu only exposes a small recovery menu', async () => {
|
||||
vi.mocked(prisma.employee.findUniqueOrThrow).mockResolvedValue({
|
||||
id: 'employee_1',
|
||||
role: 'OWNER',
|
||||
isActive: true,
|
||||
companyId: 'company_1',
|
||||
} as never)
|
||||
|
||||
vi.mocked(prisma.company.findUniqueOrThrow).mockResolvedValue({
|
||||
id: 'company_1',
|
||||
name: 'Atlas Cars',
|
||||
status: 'TRIALING',
|
||||
subscription: { plan: 'STARTER', status: 'TRIALING' },
|
||||
} as never)
|
||||
|
||||
vi.mocked(prisma.menuItem.findMany).mockResolvedValue([
|
||||
{
|
||||
id: 'item_notifications',
|
||||
systemKey: 'notifications',
|
||||
label: 'Notifications',
|
||||
itemType: 'INTERNAL_PAGE',
|
||||
routeOrUrl: '/notifications',
|
||||
icon: 'Bell',
|
||||
parentId: null,
|
||||
openInNewTab: false,
|
||||
isRequired: false,
|
||||
isActive: true,
|
||||
displayOrder: 120,
|
||||
roleVisibilities: [{ role: 'OWNER' }],
|
||||
subscriptionAssignments: [{ plan: 'STARTER', displayOrder: 120, isActive: true }],
|
||||
companyAssignments: [],
|
||||
},
|
||||
] as never)
|
||||
|
||||
const result = await getEmployeeMenu('employee_1')
|
||||
|
||||
expect(result.subscriptionStatus).toBe('TRIALING')
|
||||
expect(result.subscriptionAccessLevel).toBe('full')
|
||||
expect(result.items.map((menuItem) => menuItem.systemKey)).toEqual([
|
||||
'dashboard',
|
||||
'reservations',
|
||||
'contracts',
|
||||
'fleet',
|
||||
'customers',
|
||||
'reports',
|
||||
'billing',
|
||||
'settings',
|
||||
'notifications',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -621,6 +621,46 @@ function buildBaselineEmployeeMenu(role: EmployeeRole) {
|
||||
}))
|
||||
}
|
||||
|
||||
function routeKey(routeOrUrl: string | null) {
|
||||
return routeOrUrl === '/' ? '/' : routeOrUrl?.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
function hasOnlyRecoveryOrUtilityRoutes(items: ReturnType<typeof buildMenuTree>) {
|
||||
const internalRoutes = items
|
||||
.filter((item) => item.itemType === 'INTERNAL_PAGE')
|
||||
.map((item) => routeKey(item.routeOrUrl))
|
||||
.filter((route): route is string => Boolean(route))
|
||||
|
||||
if (internalRoutes.length === 0) return false
|
||||
|
||||
return internalRoutes.every((route) => (
|
||||
route === '/' ||
|
||||
route === '/notifications' ||
|
||||
MENU_RECOVERY_ROUTES.has(route)
|
||||
))
|
||||
}
|
||||
|
||||
function mergeWithBaselineEmployeeMenu(items: ReturnType<typeof buildMenuTree>, role: EmployeeRole) {
|
||||
const merged = [...items]
|
||||
const seenRoutes = new Set(
|
||||
merged
|
||||
.filter((item) => item.itemType === 'INTERNAL_PAGE')
|
||||
.map((item) => routeKey(item.routeOrUrl))
|
||||
.filter(Boolean),
|
||||
)
|
||||
const seenSystemKeys = new Set(merged.map((item) => item.systemKey).filter(Boolean))
|
||||
|
||||
for (const baselineItem of buildBaselineEmployeeMenu(role)) {
|
||||
const key = routeKey(baselineItem.routeOrUrl)
|
||||
if (seenRoutes.has(key) || seenSystemKeys.has(baselineItem.systemKey)) continue
|
||||
merged.push(baselineItem)
|
||||
seenRoutes.add(key)
|
||||
seenSystemKeys.add(baselineItem.systemKey)
|
||||
}
|
||||
|
||||
return sortByDisplayOrder(merged)
|
||||
}
|
||||
|
||||
export async function previewCompanyMenu(input: MenuPreviewInput) {
|
||||
const context = await getMenuEvaluationContext(input.companyId, input.role)
|
||||
return {
|
||||
@@ -677,6 +717,8 @@ export async function getEmployeeMenu(employeeId: string) {
|
||||
const resolvedItems =
|
||||
context.subscriptionAccessLevel === 'full' && employee.isActive && !hasFeatureMenuRoute(menuItems)
|
||||
? buildBaselineEmployeeMenu(employee.role)
|
||||
: context.subscriptionAccessLevel === 'full' && employee.isActive && hasOnlyRecoveryOrUtilityRoutes(menuItems)
|
||||
? mergeWithBaselineEmployeeMenu(menuItems, employee.role)
|
||||
: menuItems
|
||||
|
||||
return {
|
||||
|
||||
@@ -161,6 +161,8 @@ Open `.env.docker.production` and fill in every value. The minimum required secr
|
||||
| `ACME_EMAIL` | Your email for Let's Encrypt notifications |
|
||||
| `RESEND_API_KEY` | Resend API key (or configure SMTP vars instead) |
|
||||
| `PGMANAGE_DOMAIN` | Hostname for pgManage, e.g. `pgmanage.rentaldrivego.ma` |
|
||||
| `STRIPE_API_KEY` | Live Stripe secret key or restricted key for production billing (`sk_live_` or `rk_live_`) |
|
||||
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret for `/api/v1/subscriptions/webhooks/stripe` (`whsec_`) |
|
||||
|
||||
For Gitea Actions deploys, either store the completed production env file as the raw `ENV_DOCKER_PRODUCTION` secret or as the base64-encoded `ENV_DOCKER_PRODUCTION_B64` secret:
|
||||
|
||||
@@ -170,6 +172,8 @@ base64 < .env.docker.production | tr -d '\n'
|
||||
|
||||
Paste that single-line output into `ENV_DOCKER_PRODUCTION_B64` when using the base64 option. During deploy, the workflow writes the env file to `/opt/rentaldrivego/.env.docker.production` on the VPS with `600` permissions before running `scripts/docker-prod-deploy.sh`. If neither production env secret is set, the workflow reuses `/opt/rentaldrivego/.env.docker.production` when it already exists on the VPS.
|
||||
|
||||
You can also store `STRIPE_API_KEY` and `STRIPE_WEBHOOK_SECRET` as separate Gitea Actions secrets. When those secrets are present, the deploy workflow overwrites the Stripe values from `ENV_DOCKER_PRODUCTION`/`ENV_DOCKER_PRODUCTION_B64` before deploying. This is useful when the production env file secret still contains placeholders for billing secrets.
|
||||
|
||||
Production now derives `DATABASE_URL` inside the app container from `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD` when `DATABASE_URL_FROM_POSTGRES=true`. That avoids Prisma auth failures when the database password contains reserved URL characters such as `@`, `:`, or `/`.
|
||||
|
||||
The example file uses `rentaldrivego.ma` for the carplace and public site. The dashboard and admin panel are routed under that same host at `/dashboard` and `/admin`.
|
||||
|
||||
Reference in New Issue
Block a user