diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..e5b85bb7 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,42 @@ +name: Build images +on: + workflow_dispatch: + release: + types: [published] + +env: + AWS_REGION: eu-west-1 + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_ACCESS_PASSWORD }} + aws-region: ${{ env.AWS_REGION }} + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + - name: Build, tag, and push CE images to Amazon ECR + env: + BUILD_HASH: ${{ github.sha }} + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + REPOSITORY: mergin/mergin-ce + run: | + IMAGE_TAG="build-$GITHUB_SHA" + echo "IMAGE_TAG=$IMAGE_TAG" >> "$GITHUB_ENV" + cd server + docker build -f Dockerfile . --build-arg BUILD_HASH=${BUILD_HASH} -t $ECR_REGISTRY/$REPOSITORY-back:$IMAGE_TAG; + cd ../web-app + docker build -f Dockerfile . -t $ECR_REGISTRY/$REPOSITORY-front:$IMAGE_TAG + docker push $ECR_REGISTRY/$REPOSITORY-back:$IMAGE_TAG + docker push $ECR_REGISTRY/$REPOSITORY-front:$IMAGE_TAG + - name: Build info + run: | + echo "IMAGE_TAG=$IMAGE_TAG" + echo "GITHUB_SHA=$GITHUB_SHA" + echo "TARGET_BRANCH=${{ github.ref_name }}" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..bb7d5a39 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,40 @@ +name: Build and publish public images +on: + workflow_dispatch: + +env: + REGISTRY: docker.io + REPOSITORY: lutraconsulting/merginmaps + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_LOGIN }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build, tag, and push CE images to Dockerhub + env: + BUILD_HASH: ${{ github.sha }} + IMAGE_TAG: ${{ github.ref_name }} + run: | + if [[ $GITHUB_REF_TYPE != "tag" ]]; then + echo "Not a tag, exiting" + exit 1 + fi + echo "IMAGE_TAG=$IMAGE_TAG" >> "$GITHUB_ENV" + cd server + docker build -f Dockerfile . --build-arg BUILD_HASH=${BUILD_HASH} -t $REGISTRY/$REPOSITORY-backend:$IMAGE_TAG; + cd ../web-app + docker build -f Dockerfile . -t $REGISTRY/$REPOSITORY-frontend:$IMAGE_TAG + docker push $REGISTRY/$REPOSITORY-backend:$IMAGE_TAG + docker push $REGISTRY/$REPOSITORY-frontend:$IMAGE_TAG + - name: Build info + run: | + echo "IMAGE_TAG=$IMAGE_TAG" + echo "GITHUB_SHA=$GITHUB_SHA" diff --git a/docker-compose.yml b/docker-compose.yml index 3c4162bf..fd19df78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,13 +15,13 @@ services: volumes: - ./mergin_db:/var/lib/postgresql/data redis: - image: redis + image: redis:6.2.17 container_name: merginmaps-redis restart: always networks: - merginmaps server-gunicorn: - image: lutraconsulting/merginmaps-backend:2024.2.2 + image: lutraconsulting/merginmaps-backend:2025.2.2 container_name: merginmaps-server restart: always user: 901:999 @@ -37,7 +37,7 @@ services: networks: - merginmaps celery-beat: - image: lutraconsulting/merginmaps-backend:2024.2.2 + image: lutraconsulting/merginmaps-backend:2025.2.2 container_name: celery-beat restart: always env_file: @@ -54,7 +54,7 @@ services: networks: - merginmaps celery-worker: - image: lutraconsulting/merginmaps-backend:2024.2.2 + image: lutraconsulting/merginmaps-backend:2025.2.2 container_name: celery-worker restart: always user: 901:999 @@ -74,7 +74,7 @@ services: networks: - merginmaps web: - image: lutraconsulting/merginmaps-frontend:2024.2.2 + image: lutraconsulting/merginmaps-frontend:2025.2.2 container_name: merginmaps-web restart: always depends_on: diff --git a/server/mergin/sync/interfaces.py b/server/mergin/sync/interfaces.py index aa916b38..bb2e9843 100644 --- a/server/mergin/sync/interfaces.py +++ b/server/mergin/sync/interfaces.py @@ -188,6 +188,12 @@ def get_push_permission(self, changes: dict): """ pass + def get_email_receivers(self, project): + """ + Return list of members who should receive email notifications about project changes + """ + pass + class WorkspaceRole(Enum): GUEST = "guest" diff --git a/server/mergin/sync/private_api_controller.py b/server/mergin/sync/private_api_controller.py index 00d0a13d..643c274c 100644 --- a/server/mergin/sync/private_api_controller.py +++ b/server/mergin/sync/private_api_controller.py @@ -17,8 +17,6 @@ AccessRequest, ProjectRole, RequestStatus, - ProjectVersion, - ProjectUser, ) from .schemas import ( ProjectListSchema, @@ -64,16 +62,7 @@ def create_project_access_request(namespace, project_name): # noqa: E501 db.session.add(access_request) db.session.commit() # notify project owners - owners = ( - User.query.join(UserProfile, ProjectUser) - .filter( - ProjectUser.project_id == project.id, - ProjectUser.role == ProjectRole.OWNER.value, - User.verified_email, - UserProfile.receive_notifications, - ) - .all() - ) + owners = current_app.project_handler.get_email_receivers(project) for owner in owners: email_data = { "subject": "Project access requested", diff --git a/server/mergin/sync/project_handler.py b/server/mergin/sync/project_handler.py index 8bf17af8..8299935a 100644 --- a/server/mergin/sync/project_handler.py +++ b/server/mergin/sync/project_handler.py @@ -1,7 +1,30 @@ +from .models import ProjectRole, ProjectUser, Project from .interfaces import AbstractProjectHandler from .permissions import ProjectPermissions +from sqlalchemy import or_, and_ +from typing import List +from ..auth.models import User, UserProfile class ProjectHandler(AbstractProjectHandler): def get_push_permission(self, changes: dict): return ProjectPermissions.Upload + + def get_email_receivers(self, project: Project) -> List[User]: + return ( + User.query.join(UserProfile) + .outerjoin(ProjectUser, ProjectUser.user_id == User.id) + .filter( + or_( + and_( + ProjectUser.project_id == project.id, + ProjectUser.role == ProjectRole.OWNER.value, + ), + User.is_admin, + ), + User.active, + User.verified_email, + UserProfile.receive_notifications, + ) + .all() + ) diff --git a/server/mergin/tests/test_project_handler.py b/server/mergin/tests/test_project_handler.py index d44bc5f5..76040ca8 100644 --- a/server/mergin/tests/test_project_handler.py +++ b/server/mergin/tests/test_project_handler.py @@ -1,8 +1,53 @@ +from ..sync.models import Project, ProjectRole +from .utils import add_user, create_project, create_workspace from ..sync.project_handler import ProjectHandler from ..sync.permissions import ProjectPermissions +from ..auth.models import User +from ..app import db -def test_project_handler(): + +def test_project_permissions(client): project_handler = ProjectHandler() project_permission = project_handler.get_push_permission(None) assert project_permission == ProjectPermissions.Upload + + +def test_email_receivers(client): + project_handler = ProjectHandler() + # test project email receivers (owners and super admins) + workspace = create_workspace() + user = add_user() + project = create_project("test_project", workspace, user) + project.set_role(user.id, ProjectRole.READER) + db.session.commit() + receivers = project_handler.get_email_receivers(project) + assert len(receivers) == 1 + + project.set_role(user.id, ProjectRole.OWNER) + db.session.commit() + receivers = project_handler.get_email_receivers(project) + assert len(receivers) == 2 + + user.verified_email = False + db.session.commit() + receivers = project_handler.get_email_receivers(project) + assert len(receivers) == 1 + + user.verified_email = True + user.profile.receive_notifications = False + db.session.commit() + receivers = project_handler.get_email_receivers(project) + assert len(receivers) == 1 + + user.profile.receive_notifications = True + user.active = False + db.session.commit() + receivers = project_handler.get_email_receivers(project) + assert len(receivers) == 1 + + admin = User.query.filter(User.username == "mergin").first() + admin.is_admin = False + db.session.commit() + receivers = project_handler.get_email_receivers(project) + assert len(receivers) == 0 diff --git a/server/mergin/tests/utils.py b/server/mergin/tests/utils.py index 0d4d4f9c..0f768c6e 100644 --- a/server/mergin/tests/utils.py +++ b/server/mergin/tests/utils.py @@ -28,7 +28,7 @@ CHUNK_SIZE = 1024 -def add_user(username="random", password="random", is_admin=False): +def add_user(username="random", password="random", is_admin=False) -> User: """Helper function to create not-privileged user. Associated user workspace is created with db hook. diff --git a/server/mergin/version.py b/server/mergin/version.py index acd1cb0b..efbe6d52 100644 --- a/server/mergin/version.py +++ b/server/mergin/version.py @@ -4,4 +4,4 @@ def get_version(): - return "2025.2.1" + return "2025.2.2" diff --git a/server/setup.py b/server/setup.py index 16252a16..fb67ae9a 100644 --- a/server/setup.py +++ b/server/setup.py @@ -6,7 +6,7 @@ setup( name="mergin", - version="2025.2.1", + version="2025.2.2", url="https://github.com/MerginMaps/mergin", license="AGPL-3.0-only", author="Lutra Consulting Limited",