diff --git a/server/mergin/sync/public_api.yaml b/server/mergin/sync/public_api.yaml index 21a5a8b0..50c20c10 100644 --- a/server/mergin/sync/public_api.yaml +++ b/server/mergin/sync/public_api.yaml @@ -977,7 +977,7 @@ components: UnsupportedMediaType: description: Payload format is in an unsupported format. ConflictResp: - description: Request could not be processed becuase of conflict in resources + description: Request could not be processed because of conflict in resources UnprocessableEntity: description: Request was correct and yet server could not process it ProjectsLimitHitResp: diff --git a/server/mergin/sync/public_api_v2.yaml b/server/mergin/sync/public_api_v2.yaml index 0eae3ce7..4973696a 100644 --- a/server/mergin/sync/public_api_v2.yaml +++ b/server/mergin/sync/public_api_v2.yaml @@ -10,22 +10,73 @@ tags: description: Mergin project paths: /projects/{id}: + parameters: + - $ref: "#/components/parameters/ProjectId" delete: tags: - project summary: Delete project immediately operationId: delete_project_now - parameters: - - $ref: "#/components/parameters/ProjectId" responses: "204": $ref: "#/components/responses/NoContent" "400": $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + x-openapi-router-controller: mergin.sync.public_api_v2_controller + patch: + tags: + - project + summary: Update project + operationId: update_project + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + example: survey + responses: + "204": + $ref: "#/components/responses/NoContent" + "400": + description: Invalid project name + content: + application/json: + schema: + type: object + required: + - code + - detail + properties: + code: + type: string + enum: + - InvalidProjectName + example: InvalidProjectName + detail: + type: string + enum: + - "Entered project name is invalid" + example: "Entered project name is invalid" + "401": $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" + "409": + $ref: "#/components/responses/Conflict" x-openapi-router-controller: mergin.sync.public_api_v2_controller /projects/{id}/scheduleDelete: post: @@ -40,8 +91,10 @@ paths: $ref: "#/components/responses/NoContent" "400": $ref: "#/components/responses/BadRequest" - "403": + "401": $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" x-openapi-router-controller: mergin.sync.public_api_v2_controller @@ -50,11 +103,15 @@ components: NoContent: description: No content BadRequest: - description: Invalid request. + description: Invalid request Unauthorized: description: Authentication information is missing or invalid + Forbidden: + description: Invalid token NotFound: description: Not found + Conflict: + description: Conflict parameters: ProjectId: name: id diff --git a/server/mergin/sync/public_api_v2_controller.py b/server/mergin/sync/public_api_v2_controller.py index 78ddadc4..5f16138f 100644 --- a/server/mergin/sync/public_api_v2_controller.py +++ b/server/mergin/sync/public_api_v2_controller.py @@ -4,14 +4,15 @@ from datetime import datetime -from connexion import NoContent -from flask import abort +from connexion import NoContent, request +from flask import abort, jsonify from flask_login import current_user from mergin import db from mergin.auth import auth_required from mergin.sync.models import Project from mergin.sync.permissions import ProjectPermissions, require_project_by_uuid +from mergin.sync.utils import is_name_allowed @auth_required @@ -43,3 +44,32 @@ def delete_project_now(id_): db.session.commit() return NoContent, 204 + + +@auth_required +def update_project(id_): + """Rename project + + :param id_: Project_id + :type id_: str + """ + project = require_project_by_uuid(id_, ProjectPermissions.Update) + new_name = request.json["name"] + + if not is_name_allowed(new_name): + return ( + jsonify( + code="InvalidProjectName", detail="Entered project name is invalid" + ), + 400, + ) + new_name_exists = Project.query.filter_by( + workspace_id=project.workspace_id, name=new_name + ).first() + if new_name_exists: + abort(409, "Name already exist within workspace") + + project.name = new_name.strip() + db.session.commit() + + return NoContent, 204 diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index 3101b225..2eb3621a 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -14,7 +14,13 @@ from ..auth.tasks import prune_removed_users from .. import db from ..sync.models import Project -from . import test_workspace_id, json_headers, DEFAULT_USER, test_workspace_name +from . import ( + test_workspace_id, + json_headers, + DEFAULT_USER, + test_workspace_name, + test_project, +) from .utils import add_user, login_as_admin, login @@ -718,3 +724,19 @@ def test_admin_login(client, data, expected): add_user("user", "user") resp = client.post("/app/admin/login", data=json.dumps(data), headers=json_headers) assert resp.status_code == expected + + +def test_update_project_v2(client): + project = Project.query.filter_by( + workspace_id=test_workspace_id, name=test_project + ).first() + data = {"name": "new_project_name"} + resp = client.patch(f"v2/projects/{project.id}", json=data) + assert resp.status_code == 401 + user = add_user("test", "test") + login(client, "test", "test") + resp = client.patch(f"v2/projects/{project.id}", json=data) + assert resp.status_code == 403 + login_as_admin(client) + resp = client.patch(f"v2/projects/{project.id}", json=data) + assert resp.status_code == 204 diff --git a/server/mergin/tests/test_public_api_v2.py b/server/mergin/tests/test_public_api_v2.py index 2213ced4..6694ea5e 100644 --- a/server/mergin/tests/test_public_api_v2.py +++ b/server/mergin/tests/test_public_api_v2.py @@ -37,3 +37,25 @@ def test_delete_after_schedule(client): assert response.status_code == 204 response = client.delete(f"v2/projects/{project.id}") assert response.status_code == 204 + + +def test_rename_project(client): + project = Project.query.filter_by( + workspace_id=test_workspace_id, name=test_project + ).first() + data = {"name": "new_project_name"} + response = client.patch(f"v2/projects/{project.id}", json=data) + assert response.status_code == 204 + assert project.name == "new_project_name" + # name already exists + response = client.patch(f"v2/projects/{project.id}", json=data) + assert response.status_code == 409 + # invalid project name + response = client.patch(f"v2/projects/{project.id}", json={"name": ".new_name"}) + assert response.status_code == 400 + assert response.json["code"] == "InvalidProjectName" + response = client.patch( + f"v2/projects/{project.id}", json={"name": " new_project_name"} + ) + assert response.status_code == 400 + assert response.json["code"] == "InvalidProjectName"