Compare commits

...

44 Commits

Author SHA1 Message Date
bc5f5e051f Merge branch 'release/2.21.0'
All checks were successful
Build and Push Docker Image / build_web_app__check_image_version (push) Successful in 13s
Build and Push Docker Image / build_web_app (push) Successful in 6m22s
2026-01-09 18:38:11 +01:00
528c433fce (ver) web-app: version bump 2026-01-09 18:34:59 +01:00
50238b4e90 Merge branch 'feature/gitea-workflow' into develop 2026-01-09 18:33:09 +01:00
5773156222 Add Gitea CI/CD workflows for automated Docker builds
Implements automated Docker image building and publishing to registry:

- build.yml: Main workflow that builds and pushes Docker images to registry
  - Triggers on push to master branch
  - Only builds when image with current version doesn't exist
  - Uses Docker BuildKit with layer caching for faster builds
  - Tags images with both version number and 'latest'

- check_image_version.yml: Reusable workflow to verify image existence
  - Reads version from package.json
  - Uses lightweight manifest inspection (no image download)
  - Returns image_exists and version as outputs

- check_package_version.yml: Reusable workflow to detect version changes
  - Compares version between commits
  - Handles edge cases (first commit, missing package.json)
  - Includes validation for version extraction failures

All workflows include proper error handling and clear logging.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 18:32:48 +01:00
a3ec20544c (refactor) Move generateShareId to locationActions and apply to LocationCard
- Moved generateShareId from shareChecksum.ts to locationActions.ts as a server action
- Updated LocationCard to use shareID with checksum for proof of payment download link
- Replaced Link with AsyncLink to handle async shareID generation
- Commented out debug console.log in Pdf417Barcode

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 18:18:17 +01:00
e318523887 (bugfix) Fix proof of payment download URL to use shareID with checksum
Fixed bug where proof of payment download links used raw locationID instead
of shareID (locationID + checksum), causing link validation to fail. Added
AsyncLink component to handle async shareID generation gracefully.

Changes:
- BillEditForm: Generate shareID using generateShareId server action
- BillEditForm: Use AsyncLink to prevent broken links during async load
- AsyncLink: New reusable component for links that need async data
- Updated download URL from locationID-billID to shareID-billID format

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 18:16:53 +01:00
37f617683e (refactor + bugfix) Improve data structure and handle empty database edge cases
Refactored months data structure from object to array for better performance
and cleaner iteration. Fixed crash when availableYears array is empty by
adding proper guards and fallback to current year.

Changes:
- MonthLocationList: Changed months prop from object to array type
- HomePage: Refactored reduce logic to build array instead of object
- HomePage: Added empty database handling in year selection logic
- HomePage: Added early returns for invalid year params in empty DB

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 17:36:10 +01:00
1ca55ae820 Merge branch 'master' into develop 2026-01-05 16:08:17 +01:00
0b8c8ae6c4 Merge branch 'master' into develop 2026-01-05 15:57:31 +01:00
Knee Cola
7a526c5a85 Merge branch 'feature/email-confirm-unsubscribe' into develop 2025-12-29 23:31:25 +01:00
Knee Cola
f42366c00b feat: add git diff command to permissions in settings 2025-12-29 23:31:09 +01:00
Knee Cola
4dc2df4a12 security: add server-side validation for email status transitions
Implement strict validation to prevent unauthorized email status changes:
- Force status to Unverified when email address changes
- Only allow client to reset status to Unverified (via reset button)
- Block client from upgrading status (Unverified→Verified, etc.)
- All status upgrades must happen server-side via verification links

This prevents attackers from:
- Submitting new emails with fake "verified" status
- Bypassing email verification by modifying client requests
- Escalating email status without proper verification flow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 23:21:49 +01:00
Knee Cola
fe98a63594 feat: add persistence for tenant email status field
- Add tenantEmailStatus hidden field to LocationEditForm
- Update locationActions to persist email status across all scopes
- Add reset button for unsubscribed email status
- Improve email status display with new/modified indicators
- Update translations for email status messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 23:05:57 +01:00
Knee Cola
b20d68405c refactor: improve email status display and messaging
LocationCard:
- Include email status in card info section display condition
- Remove emoji suffixes (icons already convey status visually)

LocationEditForm:
- Enable autoBillFwd and rentDueNotification toggles
- Only show email status when displayed email matches saved email
- Show unverified status when email is changed or for new emails
- Remove emoji suffixes from status messages
- Add left margin to status display

Messages (EN/HR):
- More descriptive email status messages in both languages
- LocationCard: "tenant email not verified" vs "Email not verified"
- LocationEditForm: Clearer explanations like "this e-mail address
  will need to be verified by the tenant"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 21:58:02 +01:00
Knee Cola
fea0f48cec fix: include tenantEmail and tenantEmailStatus in fetchAllLocations projection
Added tenantEmail and tenantEmailStatus fields to the MongoDB projection
in fetchAllLocations() so LocationCard can display email status indicators.

Previously these fields were always undefined in LocationCard because they
weren't included in the aggregation pipeline's $project stage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 21:11:07 +01:00
Knee Cola
db9c57472d feat: add email status check to verify page
Security Enhancement:
- Server-side validation of email status before allowing verification
- Only allow verifying emails in VerificationPending state
- Show "Action not possible" message for invalid states
- Extract and validate share-id on server side
- Return 404 for invalid share-ids or missing tenant emails

Implementation:
- Convert page.tsx to async server component
- Fetch location and check tenantEmailStatus
- Pass isPending prop to client component
- Add bilingual "not-allowed" translations (same as unsubscribe page)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 20:54:14 +01:00
Knee Cola
ff6f8890c5 refactor: simplify unsubscribe "not-allowed" message
Make error message more generic and less specific:
- Change title from "Action Not Allowed" to "Action not possible"
- Simplify message to cover broader error cases
- Fix typo: "performe" → "performed"
- Apply same changes to Croatian version

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 20:44:58 +01:00
Knee Cola
5d1602df7f feat: add email verification check to unsubscribe page
Security Enhancement:
- Server-side validation of email status before allowing unsubscribe
- Only allow unsubscribing from verified emails
- Show "Action Not Allowed" message for unverified/unsubscribed emails
- Extract and validate share-id on server side
- Return 404 for invalid share-ids or missing tenant emails

Implementation:
- Convert page.tsx to async server component
- Fetch location and check tenantEmailStatus
- Pass isVerified prop to client component
- Add bilingual "not-allowed" translations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 20:33:16 +01:00
Knee Cola
bc7b28e6e9 refactor: improve email verification/unsubscribe UI and messaging
UI Improvements:
- Add spacing (mb-3) to card titles
- Increase heading font size (text-lg) for better hierarchy

Content Updates:
- Rebrand from "Evidencija Režija" to "rezije.app"
- Clarify success message: "subscribed to receive notifications"
- Improve opt-out description wording
- Fix Croatian grammar and phrasing
- Update unsubscribe page title for clarity

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 20:26:35 +01:00
Knee Cola
6eee14d0c3 feat: add email sending test script
Add sent-mail-tester.mjs for testing email functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 19:49:28 +01:00
Knee Cola
db92d157c5 feat: create email-server-worker workspace
Initialize workspace for email server worker service
- Polls MongoDB for email status changes
- Sends verification and notification emails
- Updates email statuses
- Runs as standalone background worker

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 19:47:13 +01:00
Knee Cola
1666394435 feat: add mailgun-webhook to VSCode workspace
Add new mailgun-webhook folder to workspace configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 19:06:22 +01:00
Knee Cola
8eb4aec3b7 fix: remove unused currentLocale variable in LocationCard
Remove unused import to resolve TypeScript warning [6133]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 18:51:45 +01:00
Knee Cola
c81c182806 feat: create mailgun-webhook workspace
Initialize empty workspace for Mailgun webhook handler service
- Processes email verification and status updates
- Communicates with web-app via shared MongoDB
- Handles Mailgun webhook events

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 18:51:04 +01:00
Knee Cola
92ecd7f18e feat: add email status indicators to LocationEditForm
- Display all email statuses (Unverified, VerificationPending, Verified, Unsubscribed)
- Show appropriate icons and colors for each status
- Add bilingual translations for status labels
- Use UTF-8 emojis (⚠️   ✉️) alongside Heroicons
- Position indicator before tenantEmail-error div

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 18:35:27 +01:00
Knee Cola
8bed381f36 feat: add email status indicators to LocationCard
- Display email status when not Verified
- Show appropriate icons and colors for each status
- Add bilingual translations for status labels
- Use UTF-8 emojis (⚠️  ✉️) alongside Heroicons

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 18:33:37 +01:00
Knee Cola
0f06394984 feat: implement email unsubscribe page
- Create /email/unsubscribe/[id] route with page and component
- Add share-id validation and 404 on invalid links
- Add bilingual translations (English/Croatian)
- Implement unsubscribe UI with success/error states
- Call unsubscribeTenantEmail server action on button click

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 18:32:04 +01:00
Knee Cola
2bc7bcdc1e feat: implement email verification page
- Create /email/verify/[id] route with page and component
- Add share-id validation and 404 on invalid links
- Add bilingual translations (English/Croatian)
- Implement verification UI with success/error states
- Call verifyTenantEmail server action on button click

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 18:16:44 +01:00
Knee Cola
c1d3026f4b feat: implement email verification and unsubscribe DB logic
- Export EmailStatus enum from db-types.ts
- Add verifyTenantEmail server action
- Add unsubscribeTenantEmail server action
- Both actions update current and all subsequent matching locations
- Match criteria: userId, name, tenantEmail, yearMonth >= current
- Share-id validation using existing shareChecksum utilities

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 18:13:41 +01:00
Knee Cola
a51476d82b docs: clarify email verification implementation details
- Added DB integration details for external email system
- Clarified share-id validation (404 on invalid)
- Enhanced subsequent matching to include tenantEmail
- Specified exact UI placement for email status indicators
- Fixed typo: EmailStatus.Verifies → EmailStatus.Verified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 18:01:10 +01:00
Knee Cola
1cafe33386 (prompt) written prompt for implementing e-mail confirmation and unsubscribe 2025-12-29 17:37:25 +01:00
Knee Cola
7a0370da9b (refactor) db-types: removed unused e-mail statuses 2025-12-29 17:36:51 +01:00
Knee Cola
ddf83fe0e5 (refactor) InfoBox: setting max width 2025-12-29 13:33:09 +01:00
Knee Cola
52662e0fb3 feat: add email status tracking for tenant emails
Add EmailStatus enum and tracking fields to BillingLocation to support
email delivery monitoring (bounces, complaints, unsubscribes).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 10:41:46 +01:00
Knee Cola
96abb5fffe (refactor) moving sign-in button to more prominent place 2025-12-27 09:36:52 +01:00
Knee Cola
d031d84781 Merge branch 'feature/going-to-monorepo' into develop 2025-12-27 09:22:32 +01:00
Knee Cola
e8164596b9 refactor: move VS Code config and debug compose to web-app workspace
Update launch.json and tasks.json to use multi-root workspace folder syntax for correct path resolution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 10:25:39 +01:00
Knee Cola
36accc3b30 refactor: create docker-stack workspace for deployment configs
Created dedicated workspace for Docker deployment configurations and scripts.
Improves organization by grouping all deployment-related files together.

## New Structure
- docker-stack/: Docker Compose files and deployment scripts
  - docker-compose-standalone.yaml
  - docker-compose-swarm.yml
  - docker-compose-debug.yml
  - deploy-standalone.sh
  - deploy-swarm.sh
  - README.md (deployment documentation)
  - package.json

## Changes
- Moved all docker-compose YAML files to docker-stack/
- Moved deploy scripts to docker-stack/
- Updated VS Code workspace to include docker-stack
- Updated documentation (README, CLAUDE.md)

## Deployment Workflow
1. Build: `cd web-app && ./build.sh 2.20.0`
2. Deploy: `cd docker-stack && ./deploy-standalone.sh 2.20.0`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-25 13:21:46 +01:00
Knee Cola
b4424edd4e refactor: move Docker build files to web-app directory
Moved Dockerfile, build.sh, and .dockerignore to web-app/ for better
project encapsulation. Each project now contains its own build configuration.

## Changes
- Moved Dockerfile to web-app/ and simplified paths
- Moved build.sh to web-app/
- Moved .dockerignore to web-app/
- Updated Dockerfile to work from web-app/ context (no workspace references)
- Updated documentation for new build workflow

## Build Workflow
- Build: Run from web-app/ directory (`cd web-app && ./build.sh 2.20.0`)
- Deploy: Run from repository root (`./deploy-standalone.sh 2.20.0`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-25 12:31:15 +01:00
Knee Cola
362a49e3cd refactor: simplify to independent multi-project structure
Removed npm workspace configuration in favor of simple multi-project repository.
Each project (web-app, housekeeping) is now completely self-contained.

## Changes
- Removed root package.json and package-lock.json
- Added VS Code workspace file for better project organization
- Updated documentation to reflect independent project structure
- Each project manages its own dependencies without workspace linking

## Structure
- web-app/: Self-contained Next.js application
- housekeeping/: Self-contained DB maintenance scripts
- No workspace management or dependency sharing
- Monorepo is purely for Git organization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-25 12:25:46 +01:00
Knee Cola
8685ac36b8 refactor: make workspaces fully self-contained
Removed shared dependencies from root package.json. Each workspace now
manages its own dependencies independently.

- Removed prettier from root devDependencies
- Removed prettier scripts from root (available in web-app workspace)
- Root package.json now only manages workspace configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-25 12:19:13 +01:00
Knee Cola
57dcebd640 refactor: convert repository to monorepo with npm workspaces
Restructured the repository into a monorepo to better organize application code
and maintenance scripts.

## Workspace Structure
- web-app: Next.js application (all app code moved from root)
- housekeeping: Database backup and maintenance scripts

## Key Changes
- Moved all application code to web-app/ using git mv
- Moved database scripts to housekeeping/ workspace
- Updated Dockerfile for monorepo build process
- Updated docker-compose files (volume paths: ./web-app/etc/hosts/)
- Updated .gitignore for workspace-level node_modules
- Updated documentation (README.md, CLAUDE.md, CHANGELOG.md)

## Migration Impact
- Root package.json now manages workspaces
- Build commands delegate to web-app workspace
- All file history preserved via git mv
- Docker build process updated for workspace structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-25 12:13:04 +01:00
Knee Cola
321267a848 Merge branch 'hotfix/2.20.1' into develop 2025-12-24 23:55:58 +01:00
Knee Cola
3f007cba4a Merge branch 'release/2.20.0' into develop 2025-12-24 23:43:24 +01:00
192 changed files with 1606 additions and 155 deletions

View File

@@ -20,7 +20,11 @@
"mcp__context7__resolve-library-id",
"mcp__context7__get-library-docs",
"mcp__serena__create_text_file",
"Bash(curl:*)"
"Bash(curl:*)",
"Bash(git mv:*)",
"Bash(rmdir:*)",
"Bash(mkdir:*)",
"Bash(git diff:*)"
]
},
"enableAllProjectMcpServers": true,

View File

@@ -0,0 +1,58 @@
name: Build and Push Docker Image
on:
push:
branches:
- master
jobs:
# Verifies if Docker image with current version already exists in registry
# This prevents rebuilding the same version but allows pulls and version changes
# to always trigger new builds. Uses lightweight manifest inspect (no download)
build_web_app__check_image_version:
uses: ./.gitea/workflows/check_image_version.yml
with:
workspacePath: './web-app'
imageName: 'utility-bills-tracker'
registryUrl: 'registry.budakova.org'
registryUsername: ${{ vars.PROFILE_REGISTRY_USERNAME }}
registryNamespace: 'knee-cola'
secrets:
registryToken: ${{ secrets.PROFILE_REGISTRY_TOKEN }}
# Builds and pushes Docker image to registry if:
# - Image with current version doesn't exist in registry
# This prevents rebuilding the same version unnecessarily
build_web_app:
needs: [build_web_app__check_image_version]
if: needs.build_web_app__check_image_version.outputs.image_exists == 'false'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Registry
# Gitea automatically provides these secrets:
# - `vars.REGISTRY_USERNAME` - defined as action variable in **repo settings**
# - `secrets.REGISTRY_TOKEN` - defined as action secret in **repo settings**
# created in user settings as personal access token with `write:packages` scope
# - `vars.PROFILE_REGISTRY_USERNAME` - defined as action variable in **profile settings**
# - `secrets.PROFILE_REGISTRY_TOKEN` - defined as action secret in **profile settings**
# created in user settings as personal access token with `write:packages` scope
run: |
echo "${{ secrets.PROFILE_REGISTRY_TOKEN }}" | docker login registry.budakova.org -u "${{ vars.PROFILE_REGISTRY_USERNAME }}" --password-stdin
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./web-app
push: true
tags: |
registry.budakova.org/knee-cola/utility-bills-tracker:${{ needs.build_web_app__check_image_version.outputs.version }}
registry.budakova.org/knee-cola/utility-bills-tracker:latest
cache-from: type=registry,ref=registry.budakova.org/knee-cola/utility-bills-tracker:buildcache
cache-to: type=registry,ref=registry.budakova.org/knee-cola/utility-bills-tracker:buildcache,mode=max

View File

@@ -0,0 +1,94 @@
name: Check Image Version
on:
workflow_call:
inputs:
workspacePath:
description: 'Path relative to repo root where package.json is located'
required: false
type: string
default: '.'
imageName:
description: 'Docker image name without registry FQDN or username'
required: true
type: string
registryUrl:
description: 'Docker registry URL (e.g., registry.budakova.org)'
required: false
type: string
default: 'registry.budakova.org'
registryUsername:
description: 'Docker registry username'
required: true
type: string
registryNamespace:
description: 'Docker registry namespace/organization (e.g., knee-cola)'
required: true
type: string
secrets:
registryToken:
description: 'Registry access token'
required: true
outputs:
image_exists:
description: 'Whether the image exists in the registry'
value: ${{ jobs.check_image.outputs.image_exists }}
version:
description: 'Current version from package.json'
value: ${{ jobs.check_image.outputs.version }}
jobs:
check_image:
runs-on: ubuntu-latest
outputs:
image_exists: ${{ steps.manifest-check.outputs.image_exists }}
version: ${{ steps.version-read.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Read version from package.json
id: version-read
run: |
WORKSPACE_PATH="${{ inputs.workspacePath }}"
# Clean up path - remove trailing slash if present
WORKSPACE_PATH="${WORKSPACE_PATH%/}"
# Handle root directory case
if [ "$WORKSPACE_PATH" = "." ]; then
PACKAGE_JSON_PATH="package.json"
else
PACKAGE_JSON_PATH="${WORKSPACE_PATH}/package.json"
fi
VERSION=$(node -p "try { require('./${PACKAGE_JSON_PATH}').version } catch(e) { console.error('Error reading version:', e.message); process.exit(1) }") || {
echo "Error: Failed to read version from ${PACKAGE_JSON_PATH}"
exit 1
}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Checking for image version: $VERSION"
- name: Login to Registry
run: |
echo "${{ secrets.registryToken }}" | docker login ${{ inputs.registryUrl }} -u "${{ inputs.registryUsername }}" --password-stdin
- name: Check if image exists in registry
id: manifest-check
run: |
VERSION=${{ steps.version-read.outputs.version }}
IMAGE="${{ inputs.registryUrl }}/${{ inputs.registryNamespace }}/${{ inputs.imageName }}:${VERSION}"
echo "Checking manifest for image: $IMAGE"
if docker manifest inspect "$IMAGE" &>/dev/null; then
echo "Image exists in registry"
echo "image_exists=true" >> $GITHUB_OUTPUT
else
echo "Image does not exist in registry"
echo "image_exists=false" >> $GITHUB_OUTPUT
fi
- name: Summary
run: |
echo "Version: ${{ steps.version-read.outputs.version }}"
echo "Image exists: ${{ steps.manifest-check.outputs.image_exists }}"

View File

@@ -0,0 +1,82 @@
name: Check Package Version
on:
workflow_call:
inputs:
workspacePath:
description: 'Path relative to repo root where package.json is located'
required: false
type: string
default: '.'
outputs:
version_changed:
description: 'Whether the version changed from the previous commit'
value: ${{ jobs.check_version.outputs.version_changed }}
version:
description: 'Current version from package.json'
value: ${{ jobs.check_version.outputs.version }}
jobs:
check_version:
runs-on: ubuntu-latest
outputs:
version_changed: ${{ steps.version-check.outputs.version_changed }}
version: ${{ steps.version-check.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if version changed
id: version-check
run: |
WORKSPACE_PATH="${{ inputs.workspacePath }}"
# Clean up path - remove trailing slash if present
WORKSPACE_PATH="${WORKSPACE_PATH%/}"
# Handle root directory case
if [ "$WORKSPACE_PATH" = "." ]; then
PACKAGE_JSON_PATH="package.json"
else
PACKAGE_JSON_PATH="${WORKSPACE_PATH}/package.json"
fi
# Get current version
CURRENT_VERSION=$(node -p "require('./${PACKAGE_JSON_PATH}').version")
echo "Current version: $CURRENT_VERSION"
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
# Check if HEAD~1 exists (handle first commit)
if ! git rev-parse HEAD~1 &>/dev/null; then
echo "First commit detected, running workflow"
echo "version_changed=true" >> $GITHUB_OUTPUT
exit 0
fi
# Check if package.json exists in previous commit
if ! git show HEAD~1:${PACKAGE_JSON_PATH} &>/dev/null; then
echo "package.json doesn't exist in previous commit"
echo "version_changed=true" >> $GITHUB_OUTPUT
exit 0
fi
# Extract previous version using grep/sed (safer than node for old file)
PREVIOUS_VERSION=$(git show HEAD~1:${PACKAGE_JSON_PATH} | grep '"version"' | head -1 | sed -E 's/.*"version"\s*:\s*"([^"]+)".*/\1/')
echo "Previous version: $PREVIOUS_VERSION"
# Validate extraction
if [ -z "$PREVIOUS_VERSION" ]; then
echo "Warning: Could not extract previous version, assuming changed"
echo "version_changed=true" >> $GITHUB_OUTPUT
exit 0
fi
# Compare versions
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "Version changed: $PREVIOUS_VERSION -> $CURRENT_VERSION"
echo "version_changed=true" >> $GITHUB_OUTPUT
else
echo "Version unchanged: $CURRENT_VERSION"
echo "version_changed=false" >> $GITHUB_OUTPUT
fi

6
.gitignore vendored
View File

@@ -1,14 +1,16 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
# next.js (in web-app workspace)
web-app/.next/
web-app/out/
/.next/
/out/

View File

@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- **Repository Structure**: Converted to multi-project monorepo
- `web-app/` - Main Next.js application (formerly root directory)
- `housekeeping/` - Database backup and maintenance scripts
- **Docker Configuration**: Updated Dockerfile and docker-compose files for new directory structure
- **Documentation**: Updated README.md and CLAUDE.md to reflect new structure
### Migration Notes
- All application code moved to `web-app/` directory using `git mv` to preserve history
- All database backup scripts moved to `housekeeping/` directory
- Each project is self-contained with its own package.json and dependencies
- Docker builds install dependencies from `web-app/` directory
- Volume mounts in docker-compose updated to reference `web-app/etc/hosts/`
- `.gitignore` updated to handle `node_modules` at any directory level
- No workspace management - each project is completely independent
## [2.17.0] - 2025-12-21
### Changed

View File

@@ -2,20 +2,41 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Structure
This is a multi-project repository containing:
- **web-app/**: Next.js 14 utility bills tracking application
- **docker-stack/**: Docker Compose configurations and deployment scripts
- **housekeeping/**: Database backup and maintenance scripts
Each project is self-contained with its own dependencies.
## Development Commands
- `npm run dev` - Start development server (Next.js)
- `npm run build` - Build production version
All commands should be run from within the respective project directory.
**Web App** (`cd web-app`):
- `npm install` - Install dependencies
- `npm run dev` - Start development server
- `npm run build` - Build production version
- `npm start` - Start production server
- `npm run prettier` - Format code with Prettier
- `npm run prettier:check` - Check code formatting
- `npm run prettier` - Format code
- `npm run seed` - Seed database with initial data
**Housekeeping** (`cd housekeeping`):
- `./db-backup--standalone.sh` - Run standalone database backup
- `./db-backup--swarm.sh` - Run swarm database backup
- `./db-dump--standalone.sh` - Run standalone database dump
- See housekeeping/README.md for more details
## Deployment Commands
- `./build.sh` - Build Docker image for deployment
- `./deploy.sh` - Deploy Docker service to production
- `./debug-deploy.sh` - Deploy with debug configuration
**Building Docker Image** (`cd web-app`):
- `./build.sh <version>` - Build Docker image
**Deploying** (`cd docker-stack`):
- `./deploy-standalone.sh <version>` - Deploy with docker-compose (standalone)
- `./deploy-swarm.sh <version>` - Deploy with Docker Swarm
## Architecture Overview
@@ -31,25 +52,26 @@ This is a Next.js 14 utility bills tracking application ("Evidencija Režija") w
### Core Architecture Patterns
**Multi-user Data Isolation**: All database operations use the `withUser` higher-order function from `app/lib/auth.ts:102` to automatically inject authenticated user ID into queries, ensuring data isolation between users.
**Multi-user Data Isolation**: All database operations use the `withUser` higher-order function from `web-app/app/lib/auth.ts:102` to automatically inject authenticated user ID into queries, ensuring data isolation between users.
**Server Actions Pattern**: Form handling uses Next.js Server Actions with Zod validation. Actions are defined in `app/lib/actions/` and follow the pattern:
**Server Actions Pattern**: Form handling uses Next.js Server Actions with Zod validation. Actions are defined in `web-app/app/lib/actions/` and follow the pattern:
```typescript
export const actionName = withUser(async (user: AuthenticatedUser, ...args) => {
// Server action implementation with automatic user context
});
```
**Internationalization**: Uses next-intl with locale-based routing. Messages are in `messages/` directory. The middleware handles both auth and i18n routing.
**Internationalization**: Uses next-intl with locale-based routing. Messages are in `web-app/messages/` directory. The middleware handles both auth and i18n routing.
### Key Files & Responsibilities
- `middleware.ts` - Handles authentication and i18n routing, defines public pages
- `app/lib/auth.ts` - NextAuth configuration, `withUser` HOF for user context
- `app/lib/dbClient.ts` - MongoDB connection with development/production handling
- `app/lib/actions/` - Server actions for data mutations (locations, bills, months)
- `app/i18n.ts` - Internationalization configuration (Croatian default)
- `next.config.js` - Standalone build config with `serverActions.allowedOrigins` for Docker deployment
- `web-app/middleware.ts` - Handles authentication and i18n routing, defines public pages
- `web-app/app/lib/auth.ts` - NextAuth configuration, `withUser` HOF for user context
- `web-app/app/lib/dbClient.ts` - MongoDB connection with development/production handling
- `web-app/app/lib/actions/` - Server actions for data mutations (locations, bills, months)
- `web-app/app/i18n.ts` - Internationalization configuration (Croatian default)
- `web-app/next.config.js` - Standalone build config with `serverActions.allowedOrigins` for Docker deployment
- `housekeeping/` - Database backup and maintenance scripts
### Database Schema
- **Collections**: Locations, Bills, Months (year-month periods)

View File

@@ -17,46 +17,74 @@ Each location record is marked with a user ID.
All the actions user `withUser` to fetch user ID, which is then used in all the DB operations.
# Repository Structure
This repository contains multiple independent projects:
- **web-app/**: Next.js application for tracking utility bills
- **docker-stack/**: Docker Compose configurations and deployment scripts
- **housekeeping/**: Database backup and maintenance scripts
Each project is self-contained with its own dependencies and configuration.
## Working with Projects
```bash
# Web app
cd web-app
npm install
npm run dev
# Deploy with Docker
cd docker-stack
./deploy-standalone.sh 2.20.0
# Housekeeping scripts
cd housekeeping
./db-backup--standalone.sh
```
# Database Backup & Restore
The project includes multiple backup strategies for different deployment scenarios and requirements.
All backup scripts are located in the `housekeeping/` workspace.
## Backup Scripts Overview
### Standalone Docker Deployments
**Online Backups (No Downtime):**
- `db-dump--standalone.sh` - Creates online backup of the 'utility-bills' database using mongodump
- `housekeeping/db-dump--standalone.sh` - Creates online backup of the 'utility-bills' database using mongodump
- Database stays running during backup
- Only backs up the database content, not the full volume
- Output: `./mongo-backup/utility-bills-dump-YYYY-MM-DD_HH-MM.tar.gz`
- Default retention: 7 backups (configurable via `KEEP` env var)
- Usage: `./db-dump--standalone.sh` or `KEEP=10 ./db-dump--standalone.sh`
- Usage: `cd housekeeping && ./db-dump--standalone.sh` or `KEEP=10 ./db-dump--standalone.sh`
- `db-restore-from-dump--standalone.sh` - Restores from mongodump archives
- `housekeeping/db-restore-from-dump--standalone.sh` - Restores from mongodump archives
- Database stays running during restore
- **WARNING**: Drops existing collections before restore
- Usage: `./db-restore-from-dump--standalone.sh utility-bills-dump-2025-11-26_14-30.tar.gz`
- Usage: `cd housekeeping && ./db-restore-from-dump--standalone.sh utility-bills-dump-2025-11-26_14-30.tar.gz`
**Offline Backups (With Downtime):**
- `db-backup--standalone.sh` - Creates offline backup of the complete mongo-volume directory
- `housekeeping/db-backup--standalone.sh` - Creates offline backup of the complete mongo-volume directory
- Database container is stopped during backup for consistency
- Backs up the entire MongoDB data directory
- Output: `./mongo-backup/mongo-volume-backup-YYYY-MM-DD-HH-MM.tar.gz`
- Default retention: 7 backups (configurable via `KEEP` env var)
- Usage: `./db-backup--standalone.sh` or `KEEP=2 ./db-backup--standalone.sh`
- Usage: `cd housekeeping && ./db-backup--standalone.sh` or `KEEP=2 ./db-backup--standalone.sh`
### Docker Swarm Deployments
- `db-backup--swarm.sh` - Creates offline backup by scaling down the MongoDB service
- `housekeeping/db-backup--swarm.sh` - Creates offline backup by scaling down the MongoDB service
- Service is scaled to 0 during backup
- Output: `./mongo-backup/mongo-volume-backup-YYYY-MM-DD-HH-MM.tar.gz`
- Usage: `./db-backup--swarm.sh`
- Usage: `cd housekeeping && ./db-backup--swarm.sh`
- `db-restore-from-backup--swarm.sh` - Restores volume backup by scaling down the service
- `housekeeping/db-restore-from-backup--swarm.sh` - Restores volume backup by scaling down the service
- Service is scaled to 0 during restore
- Optional `--pre-backup` flag for safety backup before restore
- Usage: `./db-restore-from-backup--swarm.sh mongo-volume-backup-2025-11-26-14-30.tar.gz`
- Usage: `cd housekeeping && ./db-restore-from-backup--swarm.sh mongo-volume-backup-2025-11-26-14-30.tar.gz`
## Automated Backup Schedule
@@ -64,10 +92,10 @@ Backups run automatically via cron at 04:00 every day:
```cron
# Sunday: Full volume backup (offline), keep 2 backups
0 4 * * 0 cd /home/knee-cola/web-pro/evidencija-rezija && KEEP=2 ./db-backup--standalone.sh
0 4 * * 0 cd /home/knee-cola/web-pro/evidencija-rezija/housekeeping && KEEP=2 ./db-backup--standalone.sh
# Monday-Saturday: Database dump (online), keep 6 backups
0 4 * * 1-6 cd /home/knee-cola/web-pro/evidencija-rezija && KEEP=6 ./db-dump--standalone.sh
0 4 * * 1-6 cd /home/knee-cola/web-pro/evidencija-rezija/housekeeping && KEEP=6 ./db-dump--standalone.sh
```
**Backup Strategy:**
@@ -89,21 +117,32 @@ All backups are stored in `./mongo-backup/`:
This directory is excluded from git via `.gitignore`.
# Deploying
The deployment is done via Docker:
* build docker image
* deploy Docker service
## Building Docker image
Run the following command:
The deployment is done via Docker.
## Building Docker Image
From the `web-app/` directory:
```bash
build.sh
cd web-app
./build.sh 2.20.0
```
The image will be stored in the local Docker instance.
## Deploying Docker service
Run the following command:
## Deploying Docker Service
From the `docker-stack/` directory:
```bash
deploy.sh
cd docker-stack
# Standalone deployment
./deploy-standalone.sh 2.20.0
# Or Swarm deployment
./deploy-swarm.sh 2.20.0
```
# Implementation details

46
docker-stack/README.md Normal file
View File

@@ -0,0 +1,46 @@
# Docker Stack
Docker Compose configurations and deployment scripts for the Evidencija Režija application.
## Files
### Docker Compose Configurations
- `docker-compose-standalone.yaml` - Standalone deployment with docker-compose
- `docker-compose-swarm.yml` - Docker Swarm deployment
- `docker-compose-debug.yml` - Debug/development deployment
### Deployment Scripts
- `deploy-standalone.sh` - Deploy standalone configuration
- `deploy-swarm.sh` - Deploy swarm configuration
## Usage
### Deploying Standalone
```bash
cd docker-stack
./deploy-standalone.sh 2.20.0
```
### Deploying to Swarm
```bash
cd docker-stack
./deploy-swarm.sh 2.20.0
```
## Prerequisites
- Docker image must be built first: `cd ../web-app && ./build.sh 2.20.0`
- MongoDB data directory: `../mongo-volume/`
- MongoDB backup directory: `../mongo-backup/`
## Configuration
All compose files reference:
- Web app image: `utility-bills-tracker:${IMAGE_VERSION}`
- Volume mounts: `../web-app/etc/hosts/`
- MongoDB data: `../mongo-volume/`
- MongoDB backups: `../mongo-backup/`

View File

@@ -18,7 +18,7 @@ services:
- traefik-network
- util-bills-mongo-network
volumes:
- ./etc/hosts/:/etc/hosts
- ./web-app/etc/hosts/:/etc/hosts
environment:
MONGODB_URI: mongodb://rezije.app:w4z4piJBgCdAm4tpawqB@mongo:27017/utility-bills
GOOGLE_ID: 355397364527-adjrokm6hromcaaar0qfhk050mfr35ou.apps.googleusercontent.com

View File

@@ -18,7 +18,7 @@ services:
- traefik-network
- util-bills-mongo-network
volumes:
- ./etc/hosts/:/etc/hosts
- ./web-app/etc/hosts/:/etc/hosts
environment:
MONGODB_URI: mongodb://rezije.app:w4z4piJBgCdAm4tpawqB@mongo:27017/utility-bills
GOOGLE_ID: 355397364527-adjrokm6hromcaaar0qfhk050mfr35ou.apps.googleusercontent.com

10
docker-stack/package.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "docker-stack",
"version": "2.20.0",
"private": true,
"description": "Docker deployment configurations and scripts",
"scripts": {
"deploy:standalone": "./deploy-standalone.sh",
"deploy:swarm": "./deploy-swarm.sh"
}
}

View File

@@ -0,0 +1,27 @@
# Email Server Worker
This workspace contains the email server worker service for the Evidencija Režija tenant notification system.
## Purpose
This service manages email operations by:
- Polling MongoDB for email status changes
- Detecting unverified tenant emails (EmailStatus.Unverified)
- Sending verification emails to tenants
- Updating email status to VerificationPending
- Sending scheduled notifications (rent due, utility bills)
## Architecture
This is a standalone background worker service that:
- Runs independently from the Next.js web-app
- Communicates via the shared MongoDB database
- Integrates with email service provider (e.g., Mailgun, SendGrid)
## Setup
TBD
## Environment Variables
TBD

View File

@@ -0,0 +1,27 @@
import FormData from "form-data"; // form-data v4.0.1
import Mailgun from "mailgun.js"; // mailgun.js v11.1.0
async function sendSimpleMessage() {
const mailgun = new Mailgun(FormData);
const mg = mailgun.client({
username: "api",
key: process.env.API_KEY || "f581edcac21ec14d086ef25e36f04432-e61ae8dd-e207f22b",
// When you have an EU-domain, you must specify the endpoint:
url: "https://api.eu.mailgun.net"
});
try {
console.log("Sending email...");
const data = await mg.messages.create("rezije.app", {
from: "Mailgun Sandbox <support@rezije.app>",
to: ["Nikola Derezic <nikola.derezic@gmail.com>"],
subject: "Hello Nikola Derezic",
text: "Congratulations Nikola Derezic, you just sent an email with Mailgun! You are truly awesome!",
});
console.log(data); // logs response data
} catch (error) {
console.log(error); //logs any error
}
}
sendSimpleMessage();

View File

@@ -0,0 +1,51 @@
{
"folders": [
{
"name": "🌐 web-app",
"path": "web-app"
},
{
"name": "🐳 docker-stack",
"path": "docker-stack"
},
{
"name": "🔧 housekeeping",
"path": "housekeeping"
},
{
"name": "📧 mailgun-webhook",
"path": "mailgun-webhook"
},
{
"name": "⚙️ email-server-worker",
"path": "email-server-worker"
},
{
"name": "📦 root",
"path": "."
}
],
"settings": {
"files.exclude": {
"**/node_modules": true,
"**/.next": true,
"**/.git": false
},
"search.exclude": {
"**/node_modules": true,
"**/.next": true,
"**/package-lock.json": true
},
"typescript.tsdk": "web-app/node_modules/typescript/lib",
"eslint.workingDirectories": [
"web-app"
]
},
"extensions": {
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}
}

20
housekeeping/README.md Normal file
View File

@@ -0,0 +1,20 @@
# Housekeeping
Database backup and maintenance scripts for the Evidencija Režija application.
## Scripts
- `db-backup--standalone.sh` - Backup database in standalone deployment
- `db-backup--swarm.sh` - Backup database in Docker Swarm deployment
- `db-dump--standalone.sh` - Dump database in standalone deployment
- `db-restore-from-dump--standalone.sh` - Restore from dump in standalone deployment
- `db-restore-from-backup--swarm.sh` - Restore from backup in Docker Swarm deployment
## Usage
From the housekeeping directory:
```bash
cd housekeeping
./db-backup--standalone.sh
```

13
housekeeping/package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "housekeeping",
"version": "2.20.0",
"private": true,
"description": "Database backup and maintenance scripts",
"scripts": {
"backup:standalone": "./db-backup--standalone.sh",
"backup:swarm": "./db-backup--swarm.sh",
"dump:standalone": "./db-dump--standalone.sh",
"restore:standalone": "./db-restore-from-dump--standalone.sh",
"restore:swarm": "./db-restore-from-backup--swarm.sh"
}
}

23
mailgun-webhook/README.md Normal file
View File

@@ -0,0 +1,23 @@
# Mailgun Webhook Handler
This workspace contains the Mailgun webhook handler service for processing email events related to the Evidencija Režija tenant notification system.
## Purpose
This service handles email verification and status updates by:
- Detecting new tenant email addresses (EmailStatus.Unverified)
- Sending verification emails via Mailgun
- Updating email status to VerificationPending
- Processing webhook events from Mailgun (bounces, complaints, etc.)
## Architecture
This is a separate system from the Next.js web-app that communicates via the shared MongoDB database.
## Setup
TBD
## Environment Variables
TBD

View File

@@ -0,0 +1,83 @@
# Context
App users (landlord) can assign `tenantEmail` to a `BillingLocation`.
This is a e-mail address will be used to notify the tenant when the rent is due and/or the utility bills are due.
## E-mail verification
To prevent missuse and ensure that the e-mail is correct, before an e-mail address can be used by the automatic notification system, the tenant needs to verifies that he/she accepts to receive notifications.
This verification is done via a link sent to the tenant in a verification-request e-mail, which is sent to the tenant automatically when the landloard (app user) assigns this e-mail address to a BillingLocation.
Sending of this verification-request e-mail is handled by a system separate from NextJS app in `web-app` workspace. It detects newly assigned addresses from their status bein equal `EmailStatus.Unverified`. The two systems don't talk to each other at all - what's holding them together is the DB.
### Implementation details
Verification link points to the NextJS app in `web-app` workspace at path `/email/verify/[share-id]` (share-id is calculated using `generateShareId` from `/home/kneecola/projects/evidencija-rezija/web-app/app/lib/shareChecksum.ts`).
The web page served at this path cerifies if the [share-id] is correct and if not it shows 404 page.
The web page served at this path contains an text explanation and "Verify e-mail" button.
The text includes the following information:
* what the web app is about - very short into
* why the e-mail was sent = because the landloard of the property `BillingLocation.name` configured the rent (`BillingLocation.rentDueNotification`) and/or utility bills (`BillingLocation.billFwdStrategy`) to be delivered to that e-mail address
* what will hapen if he/she clicks on the "Verify e-mail" button = they will be receiving rent due (`BillingLocation.rentDueNotification`) or utility bills due (`BillingLocation.billFwdStrategy`) notification or both - 2x a month - depending on the config set by the landloard
* opt-out infomation (they can ignore this e-mail, but can also opt-out at any moment)
If the user clicks the button "Verify e-mail" this triggers update of `BillingLocation.tenantEmailStatus`.
Here's the expected stats flow:
* landloard/app user assigns an an new address to `BillingLocation` -> `BillingLocation.tenantEmailStatus` is set to `EmailStatus.Unverified`
* an automated system detects that a new address was set (as indicated by `EmailStatus.Unverified` status), it then sets verification-email -> `BillingLocation.tenantEmailStatus` is set to `EmailStatus.VerificationPending`
* tenant click the link from the verification-requets e-mail -> `BillingLocation.tenantEmailStatus` is set to `EmailStatus.Verified`
**Note:** status is updated for the `BillingLocation` inidcated by locationID (decoded from `[share-id]` param), and all subsequent (later in time) matching `BillingLocation` records (matched by comparing `BillingLocation.name`, `BillingLocation.userId` and `BillingLocation.tenantEmail`). Similar pattern is already implemented in `updateOrAddLocation` fn in `locationAction.ts` (`updateScope === "subsequent"`).
## E-mail unsubscribe
Tenant can out-out from receiving e-mail notifications at any time via an `unsubscribe` link included at the end of every mail sent to the tenant.
### Implementation details
Verification link points to the NextJS app in `web-app` workspace at path `/email/unsubscribe/[share-id]` (share-id is calculated using `generateShareId` from `/home/kneecola/projects/evidencija-rezija/web-app/app/lib/shareChecksum.ts` ... search of examples of how this function is used).
The web page served at this path contains an text explanation and "Confirm unsubscribe" button.
The text includes the following information:
* what the web app is about - very short into
* why are they receiveing e-mails from this page = because their landlord for property `BillingLocation.name` has configured the app to deliver rent due or utility bills due notification or both to that address
* what will hapen if they click on "Confirm unsubscribe" = they will no longer receive rent due / utility bull due reminders
E-mail address's verification status is tracked via `BillingLocation.tenantEmailStatus`, which is set to `EmailStatus.Unsubscribed`.
**Note:** status is updated for the `BillingLocation` inidcated by locationID (decoded from `[share-id]` param), and all subsequent (later in time) matching `BillingLocation` records (matched by comparing `BillingLocation.name`, `BillingLocation.userId` and `BillingLocation.tenantEmail`). Similar pattern is already implemented in `updateOrAddLocation` fn in `locationAction.ts` (`updateScope === "subsequent"`).
## E-mail status in `LocationCard.tsx`
If the e-mail is not in `EmailStatus.Verified` state for a given location, then this will be indicated in `LocationCard.tsx` as a sibling of `total-payed-label` block (`<div className="flex ml-1">`)
## E-mail status in `LocationEditForm.tsx`
Current e-mail status will be indicated as a sibling of:
```
{/* Email status indicator should go here */}
<div id="tenantEmail-error" aria-live="polite" aria-atomic="true">
...
```
Use appropriate utf-8 icon for each status.
# Logical Units of work
Work will be split in logical units of work:
* implement e-mail verification DB logic
* implement e-mail verification page
* create text both in croatian (hr.json) and english (en.json)
* implement share-id verification (see /home/kneecola/projects/evidencija-rezija/web-app/app/[locale]/share/proof-of-payment/per-bill/[id]/route.tsx)
* implement e-mail unsubscribe DB logic
* implement e-mail unsubscribe page
* create text both in croatian (hr.json) and english (en.json)
* implement share-id verification (see /home/kneecola/projects/evidencija-rezija/web-app/app/[locale]/share/proof-of-payment/per-bill/[id]/route.tsx)
* add email status to `LocationCard.tsx`
* add email status to `LocationEditForm.tsx`
Each logical unit of work will be commited separatley.

View File

View File

@@ -8,7 +8,7 @@
"name": "Debug",
"type": "node",
"request": "launch",
"envFile": "${workspaceFolder}/.env",
"envFile": "${workspaceFolder:🌐 web-app}/.env",
"env": {
"USE_MOCK_AUTH": "true",
"MOCK_USER_ID": "109754742613069927799",
@@ -19,8 +19,8 @@
"dev" // this is `dev` from `npm run dev`
],
"runtimeExecutable": "npm",
"cwd": "${workspaceFolder}/",
"localRoot": "${workspaceFolder}/",
"cwd": "${workspaceFolder:🌐 web-app}/",
"localRoot": "${workspaceFolder:🌐 web-app}/",
"remoteRoot": "/app/",
"skipFiles": [
"<node_internals>/**",

View File

@@ -13,7 +13,7 @@
"build": true,
},
"files": [
"${workspaceFolder}/docker-compose-debug.yml"
"${workspaceFolder:🌐 web-app}/docker-compose-debug.yml"
],
"isBackground": true,
"problemMatcher": "Terminal will be reused by tasks, press any key to close it"
@@ -25,7 +25,7 @@
"dockerCompose": {
"down": {},
"files": [
"${workspaceFolder}/docker-compose-debug.yml"
"${workspaceFolder:🌐 web-app}/docker-compose-debug.yml"
],
}
},

View File

@@ -12,16 +12,16 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app
# package.json and package-lock.json
# Copy package files
COPY ./package.json ./package-lock.json ./
# installing dependencies
# Install dependencies
RUN npm i && npm cache clean --force
# copy all the soruce code
# Copy application source code
COPY . .
# building app
# Build application
RUN npm run build
#-----------------------------------------

View File

@@ -0,0 +1,117 @@
'use client';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { unsubscribeTenantEmail } from '@/app/lib/actions/emailActions';
import { CheckCircleIcon } from '@heroicons/react/24/outline';
interface EmailUnsubscribePageProps {
shareId: string;
isVerified: boolean;
}
export default function EmailUnsubscribePage({ shareId, isVerified }: EmailUnsubscribePageProps) {
const t = useTranslations('email-unsubscribe-page');
const [isUnsubscribing, setIsUnsubscribing] = useState(false);
const [isUnsubscribed, setIsUnsubscribed] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleUnsubscribe = async () => {
setIsUnsubscribing(true);
setError(null);
try {
const result = await unsubscribeTenantEmail(shareId);
if (result.success) {
setIsUnsubscribed(true);
} else {
setError(result.message || t('error.unknown'));
}
} catch (err) {
setError(t('error.unknown'));
} finally {
setIsUnsubscribing(false);
}
};
if (isUnsubscribed) {
return (
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
<div className="card-body">
<div className="flex justify-center mb-4">
<CheckCircleIcon className="h-16 w-16 text-success" />
</div>
<h2 className="card-title text-center justify-center text-success">
{t('success.title')}
</h2>
<p className="text-center">{t('success.message')}</p>
</div>
</div>
);
}
if (error) {
return (
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
<div className="card-body">
<h2 className="card-title text-error">{t('error.title')}</h2>
<p>{error}</p>
</div>
</div>
);
}
if (!isVerified) {
return (
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
<div className="card-body">
<h2 className="card-title text-warning">{t('not-allowed.title')}</h2>
<p>{t('not-allowed.message')}</p>
</div>
</div>
);
}
return (
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
<div className="card-body">
<h2 className="card-title mb-3">{t('title')}</h2>
<div className="space-y-4">
<div>
<h3 className="font-semibold text-lg">{t('about.title')}</h3>
<p>{t('about.description')}</p>
</div>
<div>
<h3 className="font-semibold text-lg">{t('why.title')}</h3>
<p>{t('why.description')}</p>
</div>
<div>
<h3 className="font-semibold text-lg">{t('what-happens.title')}</h3>
<p>{t('what-happens.description')}</p>
</div>
</div>
<div className="card-actions justify-center mt-6">
<button
className="btn btn-error"
onClick={handleUnsubscribe}
disabled={isUnsubscribing}
>
{isUnsubscribing ? (
<>
<span className="loading loading-spinner"></span>
{t('button.unsubscribing')}
</>
) : (
t('button.unsubscribe')
)}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,45 @@
import { Suspense } from 'react';
import EmailUnsubscribePage from './EmailUnsubscribePage';
import { Main } from '@/app/ui/Main';
import { getDbClient } from '@/app/lib/dbClient';
import { BillingLocation, EmailStatus } from '@/app/lib/db-types';
import { extractShareId, validateShareChecksum } from '@/app/lib/shareChecksum';
import { notFound } from 'next/navigation';
export default async function Page({ params: { id } }: { params: { id: string } }) {
// Extract and validate share ID
const extracted = extractShareId(id);
if (!extracted) {
notFound();
}
const { locationId, checksum } = extracted;
// Validate checksum
if (!validateShareChecksum(locationId, checksum)) {
notFound();
}
// Fetch location to check email status
const dbClient = await getDbClient();
const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne(
{ _id: locationId },
{ projection: { tenantEmail: 1, tenantEmailStatus: 1 } }
);
if (!location || !location.tenantEmail) {
notFound();
}
// Check if email is verified
const isVerified = location.tenantEmailStatus === EmailStatus.Verified;
return (
<Main>
<Suspense fallback={<div className="text-center p-8">Loading...</div>}>
<EmailUnsubscribePage shareId={id} isVerified={isVerified} />
</Suspense>
</Main>
);
}

View File

@@ -0,0 +1,122 @@
'use client';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { verifyTenantEmail } from '@/app/lib/actions/emailActions';
import { CheckCircleIcon } from '@heroicons/react/24/outline';
interface EmailVerifyPageProps {
shareId: string;
isPending: boolean;
}
export default function EmailVerifyPage({ shareId, isPending }: EmailVerifyPageProps) {
const t = useTranslations('email-verify-page');
const [isVerifying, setIsVerifying] = useState(false);
const [isVerified, setIsVerified] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleVerify = async () => {
setIsVerifying(true);
setError(null);
try {
const result = await verifyTenantEmail(shareId);
if (result.success) {
setIsVerified(true);
} else {
setError(result.message || t('error.unknown'));
}
} catch (err) {
setError(t('error.unknown'));
} finally {
setIsVerifying(false);
}
};
if (isVerified) {
return (
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
<div className="card-body">
<div className="flex justify-center mb-4">
<CheckCircleIcon className="h-16 w-16 text-success" />
</div>
<h2 className="card-title text-center justify-center text-success">
{t('success.title')}
</h2>
<p className="text-center">{t('success.message')}</p>
</div>
</div>
);
}
if (error) {
return (
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
<div className="card-body">
<h2 className="card-title text-error">{t('error.title')}</h2>
<p>{error}</p>
</div>
</div>
);
}
if (!isPending) {
return (
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
<div className="card-body">
<h2 className="card-title text-warning">{t('not-allowed.title')}</h2>
<p>{t('not-allowed.message')}</p>
</div>
</div>
);
}
return (
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
<div className="card-body">
<h2 className="card-title mb-3">{t('title')}</h2>
<div className="space-y-4">
<div>
<h3 className="font-semibold text-lg">{t('about.title')}</h3>
<p>{t('about.description')}</p>
</div>
<div>
<h3 className="font-semibold text-lg">{t('why.title')}</h3>
<p>{t('why.description')}</p>
</div>
<div>
<h3 className="font-semibold text-lg">{t('what-happens.title')}</h3>
<p>{t('what-happens.description')}</p>
</div>
<div>
<h3 className="font-semibold text-lg">{t('opt-out.title')}</h3>
<p>{t('opt-out.description')}</p>
</div>
</div>
<div className="card-actions justify-center mt-6">
<button
className="btn btn-primary"
onClick={handleVerify}
disabled={isVerifying}
>
{isVerifying ? (
<>
<span className="loading loading-spinner"></span>
{t('button.verifying')}
</>
) : (
t('button.verify')
)}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,45 @@
import { Suspense } from 'react';
import EmailVerifyPage from './EmailVerifyPage';
import { Main } from '@/app/ui/Main';
import { getDbClient } from '@/app/lib/dbClient';
import { BillingLocation, EmailStatus } from '@/app/lib/db-types';
import { extractShareId, validateShareChecksum } from '@/app/lib/shareChecksum';
import { notFound } from 'next/navigation';
export default async function Page({ params: { id } }: { params: { id: string } }) {
// Extract and validate share ID
const extracted = extractShareId(id);
if (!extracted) {
notFound();
}
const { locationId, checksum } = extracted;
// Validate checksum
if (!validateShareChecksum(locationId, checksum)) {
notFound();
}
// Fetch location to check email status
const dbClient = await getDbClient();
const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne(
{ _id: locationId },
{ projection: { tenantEmail: 1, tenantEmailStatus: 1 } }
);
if (!location || !location.tenantEmail) {
notFound();
}
// Check if email is pending verification
const isPending = location.tenantEmailStatus === EmailStatus.VerificationPending;
return (
<Main>
<Suspense fallback={<div className="text-center p-8">Loading...</div>}>
<EmailVerifyPage shareId={id} isPending={isPending} />
</Suspense>
</Main>
);
}

View File

@@ -9,6 +9,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import { paragraphFormatFactory } from '../lib/paragraphFormatFactory';
import { getAuthProviders } from '../lib/getProviders';
import { EnterOrSignInButton } from '../ui/EnterOrSignInButton';
import Link from 'next/link';
const h1ClassName = "text-3xl font-bold max-w-[38rem] mx-auto text-neutral-50";
const h2ClassName = h1ClassName + " mt-8";
@@ -33,11 +34,10 @@ const Page: FC = async () => {
<h1 className={h1ClassName}>
{t.rich("main-card.title", paragraphFormat)}
</h1>
{t.rich("main-card.text", paragraphFormat)}
<Image src={t("main-card.image-url")} alt={t("main-card.image-alt")} className="m-auto mt-0" width={400} height={300} />
<EnterOrSignInButton session={session} locale={locale} providers={providers} />
{t.rich("main-card.text", paragraphFormat)}
<Image src={t("main-card.image-url")} alt={t("main-card.image-alt")} className="m-auto mt-0" width={400} height={300} />
{
// Google will refuse OAuth signin if it's inside a webview (i.e. Facebook)
insideWebeview &&

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,175 @@
'use server';
import { getDbClient } from '../dbClient';
import { BillingLocation, EmailStatus } from '../db-types';
import { extractShareId, validateShareChecksum } from '../shareChecksum';
import { revalidatePath } from 'next/cache';
export type EmailActionResult = {
success: boolean;
message?: string;
};
/**
* Verify tenant email address
* Updates the email status to Verified for the location and all subsequent matching locations
*
* @param shareId - The share ID from the verification link (locationId + checksum)
* @returns Result indicating success or failure
*/
export async function verifyTenantEmail(shareId: string): Promise<EmailActionResult> {
// Extract and validate share ID
const extracted = extractShareId(shareId);
if (!extracted) {
return {
success: false,
message: 'Invalid verification link'
};
}
const { locationId, checksum } = extracted;
// Validate checksum
if (!validateShareChecksum(locationId, checksum)) {
return {
success: false,
message: 'Invalid verification link'
};
}
// Get database client
const dbClient = await getDbClient();
// Fetch the location to get userId, name, tenantEmail, and yearMonth
const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne(
{ _id: locationId },
{ projection: { userId: 1, name: 1, tenantEmail: 1, yearMonth: 1 } }
);
if (!location) {
return {
success: false,
message: 'Location not found'
};
}
if (!location.tenantEmail) {
return {
success: false,
message: 'No tenant email configured for this location'
};
}
// Update current and all subsequent matching locations
// Match by: userId, name, tenantEmail, and yearMonth >= current
const result = await dbClient.collection<BillingLocation>("lokacije").updateMany(
{
userId: location.userId,
name: location.name,
tenantEmail: location.tenantEmail,
$or: [
{ "yearMonth.year": { $gt: location.yearMonth.year } },
{
"yearMonth.year": location.yearMonth.year,
"yearMonth.month": { $gte: location.yearMonth.month }
}
]
},
{
$set: {
tenantEmailStatus: EmailStatus.Verified
}
}
);
// Revalidate paths to refresh UI
revalidatePath('/[locale]', 'layout');
return {
success: true,
message: `Email verified successfully (${result.modifiedCount} location(s) updated)`
};
}
/**
* Unsubscribe tenant from email notifications
* Updates the email status to Unsubscribed for the location and all subsequent matching locations
*
* @param shareId - The share ID from the unsubscribe link (locationId + checksum)
* @returns Result indicating success or failure
*/
export async function unsubscribeTenantEmail(shareId: string): Promise<EmailActionResult> {
// Extract and validate share ID
const extracted = extractShareId(shareId);
if (!extracted) {
return {
success: false,
message: 'Invalid unsubscribe link'
};
}
const { locationId, checksum } = extracted;
// Validate checksum
if (!validateShareChecksum(locationId, checksum)) {
return {
success: false,
message: 'Invalid unsubscribe link'
};
}
// Get database client
const dbClient = await getDbClient();
// Fetch the location to get userId, name, tenantEmail, and yearMonth
const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne(
{ _id: locationId },
{ projection: { userId: 1, name: 1, tenantEmail: 1, yearMonth: 1 } }
);
if (!location) {
return {
success: false,
message: 'Location not found'
};
}
if (!location.tenantEmail) {
return {
success: false,
message: 'No tenant email configured for this location'
};
}
// Update current and all subsequent matching locations
// Match by: userId, name, tenantEmail, and yearMonth >= current
const result = await dbClient.collection<BillingLocation>("lokacije").updateMany(
{
userId: location.userId,
name: location.name,
tenantEmail: location.tenantEmail,
$or: [
{ "yearMonth.year": { $gt: location.yearMonth.year } },
{
"yearMonth.year": location.yearMonth.year,
"yearMonth.month": { $gte: location.yearMonth.month }
}
]
},
{
$set: {
tenantEmailStatus: EmailStatus.Unsubscribed
}
}
);
// Revalidate paths to refresh UI
revalidatePath('/[locale]', 'layout');
return {
success: true,
message: `Unsubscribed successfully (${result.modifiedCount} location(s) updated)`
};
}

View File

@@ -2,7 +2,7 @@
import { z } from 'zod';
import { getDbClient } from '../dbClient';
import { BillingLocation, FileAttachment, YearMonth } from '../db-types';
import { BillingLocation, FileAttachment, YearMonth, EmailStatus } from '../db-types';
import { ObjectId } from 'mongodb';
import { withUser } from '@/app/lib/auth';
import { AuthenticatedUser } from '../types/next-auth';
@@ -10,7 +10,7 @@ import { gotoHomeWithMessage } from './navigationActions';
import { unstable_noStore, revalidatePath } from 'next/cache';
import { IntlTemplateFn } from '@/app/i18n';
import { getTranslations, getLocale } from "next-intl/server";
import { generateShareId, extractShareId, validateShareChecksum } from '../shareChecksum';
import { extractShareId, validateShareChecksum, generateShareChecksum } from '../shareChecksum';
import { validatePdfFile } from '../validators/pdfValidator';
import { checkUploadRateLimit } from '../uploadRateLimiter';
@@ -22,6 +22,7 @@ export type State = {
tenantTown?: string[];
autoBillFwd?: string[];
tenantEmail?: string[];
tenantEmailStatus?: string[];
billFwdStrategy?: string[];
rentDueNotification?: string[];
rentDueDay?: string[];
@@ -44,6 +45,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({
tenantTown: z.string().max(27).optional().nullable(),
autoBillFwd: z.boolean().optional().nullable(),
tenantEmail: z.string().email(t("tenant-email-invalid")).optional().or(z.literal("")).nullable(),
tenantEmailStatus: z.enum([EmailStatus.Unverified, EmailStatus.VerificationPending, EmailStatus.Verified, EmailStatus.Unsubscribed]).optional().nullable(),
billFwdStrategy: z.enum(["when-payed", "when-attached"]).optional().nullable(),
rentDueNotification: z.boolean().optional().nullable(),
rentDueDay: z.coerce.number().min(1).max(31).optional().nullable(),
@@ -122,6 +124,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown: formData.get('tenantTown') || null,
autoBillFwd: formData.get('autoBillFwd') === 'on',
tenantEmail: formData.get('tenantEmail') || null,
tenantEmailStatus: formData.get('tenantEmailStatus') as "unverified" | "verification-pending" | "verified" | "unsubscribed" | undefined,
billFwdStrategy: formData.get('billFwdStrategy') as "when-payed" | "when-attached" | undefined,
rentDueNotification: formData.get('rentDueNotification') === 'on',
rentDueDay: formData.get('rentDueDay') || null,
@@ -147,6 +150,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown,
autoBillFwd,
tenantEmail,
tenantEmailStatus,
billFwdStrategy,
rentDueNotification,
rentDueDay,
@@ -172,6 +176,20 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
};
}
// SECURITY: Validate email status transitions
// - If email changed: force to Unverified (prevents spoofing verified status)
// - If email unchanged: only allow client to reset to Unverified (via reset button)
// All other status transitions (Unverified→VerificationPending, VerificationPending→Verified)
// must happen server-side through other mechanisms (email verification links, etc.)
const emailHasChanged = currentLocation.tenantEmail !== (tenantEmail || null);
const clientWantsToReset = tenantEmailStatus === EmailStatus.Unverified;
const finalEmailStatus = emailHasChanged
? EmailStatus.Unverified // Email changed: force reset
: clientWantsToReset
? EmailStatus.Unverified // Client initiated reset: allow it
: (currentLocation.tenantEmailStatus || EmailStatus.Unverified); // Otherwise: keep current status
// Handle different update scopes
if (updateScope === "current" || !updateScope) {
// Update only the current location (default behavior)
@@ -190,6 +208,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown: tenantTown || null,
autoBillFwd: autoBillFwd || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: finalEmailStatus,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotification: rentDueNotification || false,
rentDueDay: rentDueDay || null,
@@ -221,6 +240,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown: tenantTown || null,
autoBillFwd: autoBillFwd || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: finalEmailStatus,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotification: rentDueNotification || false,
rentDueDay: rentDueDay || null,
@@ -245,6 +265,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown: tenantTown || null,
autoBillFwd: autoBillFwd || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: finalEmailStatus,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotification: rentDueNotification || false,
rentDueDay: rentDueDay || null,
@@ -268,6 +289,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown: tenantTown || null,
autoBillFwd: autoBillFwd || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotification: rentDueNotification || false,
rentDueDay: rentDueDay || null,
@@ -343,6 +365,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
tenantTown: tenantTown || null,
autoBillFwd: autoBillFwd || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotification: rentDueNotification || false,
rentDueDay: rentDueDay || null,
@@ -446,6 +469,8 @@ export const fetchAllLocations = withUser(async (user:AuthenticatedUser, year:nu
"bills.payedAmount": 1,
"bills.proofOfPayment.uploadedAt": 1,
"seenByTenantAt": 1,
"tenantEmail": 1,
"tenantEmailStatus": 1,
// "bills.attachment": 0,
// "bills.notes": 0,
// "bills.hub3aText": 1,
@@ -740,6 +765,18 @@ export const uploadUtilBillsProofOfPayment = async (
}
}
/**
* Generate combined location ID with checksum appended
* @param locationId - The MongoDB location ID (24 chars)
* @returns Combined ID: locationId + checksum (40 chars total)
*/
export async function generateShareId(locationId: string): Promise<string> {
const checksum = generateShareChecksum(locationId);
return locationId + checksum;
}
/**
* Generate/activate share link for location
* Called when owner clicks "Share" button
@@ -775,7 +812,7 @@ export const generateShareLink = withUser(
);
// Generate combined share ID (locationId + checksum)
const shareId = generateShareId(locationId);
const shareId = await generateShareId(locationId);
// Build share URL
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';

View File

@@ -1,3 +1,4 @@
import { unsubscribe } from "diagnostics_channel";
export interface FileAttachment {
fileName: string;
@@ -35,6 +36,17 @@ export interface UserSettings {
ownerRevolutProfileName?: string | null;
};
export enum EmailStatus {
/** Email is not yet verified - recipient has not yet confirmed their email address */
Unverified = "unverified",
/** Email is not yet verified - a verification request has been sent */
VerificationPending = "verification-pending",
/** Email is verified and is in good standing: emails are being successfully delivered */
Verified = "verified",
/** Recepient has unsubscribed from receiving emails via link - no further emails will be sent */
Unsubscribed = "unsubscribed"
}
/** bill object in the form returned by MongoDB */
export interface BillingLocation {
_id: string;
@@ -67,6 +79,8 @@ export interface BillingLocation {
autoBillFwd?: boolean | null;
/** (optional) tenant email */
tenantEmail?: string | null;
/** (optional) tenant email status */
tenantEmailStatus?: EmailStatus | null;
/** (optional) bill forwarding strategy */
billFwdStrategy?: "when-payed" | "when-attached" | null;
/** (optional) whether to automatically send rent notification */

Some files were not shown because too many files have changed in this diff Show More