Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
fabf1a4
Fix: counts just for active projects and users
MarcelGeo Dec 11, 2024
a95e5a6
Api impl: added editors to stats + server usage for admin
MarcelGeo Dec 12, 2024
3bf9a09
Fe impl: show card with editors count
MarcelGeo Dec 12, 2024
ee8632c
Do not count admins
MarcelGeo Dec 12, 2024
e505598
Fix storage usage for no files
harminius Dec 12, 2024
c509977
Revert admin handling
MarcelGeo Dec 12, 2024
d3598a3
Add fallback 0 size for no files in storage server usage
harminius Dec 13, 2024
6cd9639
Add editor limit hit blocking
varmar05 Dec 13, 2024
8a518af
Adjust projectApi
harminius Dec 13, 2024
1d4c45a
Address commnets @varmar05
MarcelGeo Dec 16, 2024
3c2c186
Fix removed filtering based just on deleted_ and add tests
MarcelGeo Dec 16, 2024
98f0e5f
Merge pull request #342 from MerginMaps/update-statistics-counts
MarcelGeo Dec 16, 2024
e7dfd74
Merge pull request #343 from MerginMaps/editor_limit_blocking
MarcelGeo Dec 16, 2024
c2aedea
Update project access api to v2 endpoint
harminius Dec 17, 2024
df13893
Merge pull request #344 from MerginMaps/fe_uses_v2_endpoints
MarcelGeo Dec 17, 2024
7d6b336
Added method for error handling which could be reused in other applic…
MarcelGeo Dec 18, 2024
14b95ab
Use v2 api to get project collaborators and their project permission
harminius Dec 18, 2024
ec42840
Keep original interfaces
harminius Dec 18, 2024
dfb6ad7
Added project_role to access
MarcelGeo Dec 18, 2024
4f3c23d
Cleanup of none ProjecRole
MarcelGeo Dec 18, 2024
67f146e
Updated:
MarcelGeo Dec 18, 2024
13e68b4
Fix tests for project_role
MarcelGeo Dec 18, 2024
f1f09b9
Merge remote-tracking branch 'origin/use-v2-update-project-access' in…
harminius Dec 19, 2024
e61f7b5
Address comments @varmar05 - renaming attributes
MarcelGeo Dec 19, 2024
6ba9811
Use v2 api in collaborators tab
harminius Dec 20, 2024
7f73a32
Add a collaborators property to the project store
harminius Dec 20, 2024
549ce73
Merge remote-tracking branch 'origin/use-v2-update-project-access' in…
harminius Dec 20, 2024
e84681e
Merge pull request #349 from MerginMaps/use-v2-update-project-access
MarcelGeo Dec 20, 2024
8389be3
Cleanup unused project API
harminius Dec 20, 2024
bbe7720
Separate collaborators method in Project store
harminius Dec 20, 2024
c13f1b2
update project public flag
harminius Dec 20, 2024
6b1ac37
Fix tests by rm
harminius Dec 20, 2024
f80c82b
Merge pull request #348 from MerginMaps/fe_get_project_collaborators
MarcelGeo Jan 7, 2025
4e600e0
Use week unit when it is more than 14 days
harminius Jan 8, 2025
c168c49
Fix CSRF issue for public endpoint
varmar05 Jan 9, 2025
73d45c5
Fix collaborators API interface according to specification
varmar05 Jan 9, 2025
61313dc
black
varmar05 Jan 9, 2025
ed334ea
Merge pull request #351 from MerginMaps/show_inv_expiration_in_days_#…
MarcelGeo Jan 9, 2025
de92c24
Adsut currency calculation to accept number of digits
MarcelGeo Jan 9, 2025
32270a8
Merge pull request #353 from MerginMaps/gh#2697-adjust-currency
MarcelGeo Jan 9, 2025
8f6a611
Merge pull request #352 from MerginMaps/fix_v2_apis
MarcelGeo Jan 10, 2025
1b12240
Fix username autogeneration from email
varmar05 Jan 13, 2025
475239f
Merge pull request #354 from MerginMaps/fix_generate_username
MarcelGeo Jan 13, 2025
2eb7cf0
Remove monthl contributors card from overview
MarcelGeo Jan 14, 2025
40ad6f4
Merge pull request #355 from MerginMaps/remove-contributors-admin-ove…
MarcelGeo Jan 14, 2025
659a8c9
Fix: public switch
MarcelGeo Jan 15, 2025
06520e1
Merge pull request #356 from MerginMaps/fix-public-switch
MarcelGeo Jan 15, 2025
a7bd2fe
Remove default message to habe unique error message for users which a…
MarcelGeo Jan 15, 2025
a7196ad
Fix: Update interface for update user to make is_admin and is_active …
MarcelGeo Jan 16, 2025
ce515e3
Merge pull request #357 from MerginMaps/gh#2711-not-found-message-col…
MarcelGeo Jan 16, 2025
3d6d3c3
Merge pull request #358 from MerginMaps/fix-admin-deactivate-account
MarcelGeo Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/mergin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def custom_protect():
_get_csrf_token = csrf._get_csrf_token

def get_csrf_token():
if request.path.startswith("/v1/"):
if request.path.startswith("/v1/") or request.path.startswith("/v2/"):
for header_name in app.app.config["WTF_CSRF_HEADERS"]:
csrf_token = request.headers.get(header_name)
if csrf_token:
Expand Down
6 changes: 3 additions & 3 deletions server/mergin/auth/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ components:
type: object
properties:
active_monthly_contributors:
type: array
type: integer
description: count of users who made a project change last months
items:
type: integer
Expand All @@ -934,9 +934,9 @@ components:
description: total number of projects
example: 12
storage:
type: string
type: number
description: projest files size in bytes
example: 1024 kB
example: 1024
users:
type: integer
description: count of registered accounts
Expand Down
18 changes: 9 additions & 9 deletions server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,9 @@ def create_user():
username = request.json.get(
"username", User.generate_username(request.json["email"])
)
form = UserRegistrationForm()

# in public endpoint we want to disable form csrf - for browser clients endpoint is protected anyway
form = UserRegistrationForm(meta={"csrf": False})
form.confirm.data = form.password.data
form.username.data = username
if not form.validate():
Expand Down Expand Up @@ -545,15 +547,13 @@ def create_user():
@auth_required(permissions=["admin"])
def get_server_usage():
data = {
"active_monthly_contributors": [
current_app.ws_handler.monthly_contributors_count(),
current_app.ws_handler.monthly_contributors_count(month_offset=1),
current_app.ws_handler.monthly_contributors_count(month_offset=2),
current_app.ws_handler.monthly_contributors_count(month_offset=3),
],
"projects": Project.query.count(),
"active_monthly_contributors": current_app.ws_handler.monthly_contributors_count(),
"projects": Project.query.filter(Project.removed_at.is_(None)).count(),
"storage": files_size(),
"users": User.query.count(),
"users": User.query.filter(
is_(User.username.ilike("deleted_%"), False),
).count(),
"workspaces": current_app.ws_handler.workspace_count(),
"editors": current_app.ws_handler.server_editors_count(),
}
return data, 200
5 changes: 5 additions & 0 deletions server/mergin/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import datetime
from typing import List, Optional
import bcrypt
import re
from flask import current_app, request
from sqlalchemy import or_, func, text

Expand Down Expand Up @@ -196,6 +197,10 @@ def generate_username(cls, email: str) -> Optional[str]:
if not "@" in email:
return
username = email.split("@")[0].strip().lower()
# remove forbidden chars
username = re.sub(
r"[\@\#\$\%\^\&\*\(\)\{\}\[\]\?\'\"`,;\:\+\=\~\\\/\|\<\>]", "", username
)
# check if we already do not have existing usernames
suffix = db.session.execute(
text(
Expand Down
8 changes: 6 additions & 2 deletions server/mergin/stats/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import logging
from flask import current_app
from sqlalchemy.sql.operators import is_

from .models import MerginInfo
from ..celery import celery
Expand Down Expand Up @@ -60,12 +61,15 @@ def send_statistics():
"url": current_app.config["MERGIN_BASE_URL"],
"contact_email": current_app.config["CONTACT_EMAIL"],
"licence": current_app.config["SERVER_TYPE"],
"projects_count": Project.query.count(),
"users_count": User.query.count(),
"projects_count": Project.query.filter(Project.removed_at.is_(None)).count(),
"users_count": User.query.filter(
is_(User.username.ilike("deleted_%"), False)
).count(),
"workspaces_count": current_app.ws_handler.workspace_count(),
"last_change": str(last_change_item.updated) + "Z" if last_change_item else "",
"server_version": current_app.config["VERSION"],
"monthly_contributors": current_app.ws_handler.monthly_contributors_count(),
"editors": current_app.ws_handler.server_editors_count(),
}

try:
Expand Down
7 changes: 7 additions & 0 deletions server/mergin/sync/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ def monthly_contributors_count():
"""
pass

@staticmethod
def server_editors_count():
"""
Return number of workspace editors in current server instance
"""
pass


class AbstractProjectHandler(ABC):
@abstractmethod
Expand Down
7 changes: 6 additions & 1 deletion server/mergin/sync/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ def unset_role(self, user_id: int) -> None:

def get_member(self, user_id: int) -> Optional[ProjectMember]:
"""Get project member"""
from .permissions import ProjectPermissions

member = self._member(user_id)
if member:
return ProjectMember(
Expand All @@ -291,6 +293,7 @@ def get_member(self, user_id: int) -> Optional[ProjectMember]:
email=member.user.email,
project_role=ProjectRole(member.role),
workspace_role=self.workspace.get_user_role(member.user),
role=ProjectPermissions.get_user_project_role(self, member.user),
)

def members_by_role(self, role: ProjectRole) -> List[int]:
Expand Down Expand Up @@ -350,6 +353,7 @@ class ProjectMember:
username: str
workspace_role: WorkspaceRole
project_role: Optional[ProjectRole]
role: ProjectRole


@dataclass
Expand All @@ -359,7 +363,8 @@ class ProjectAccessDetail:
role: str
username: str
name: Optional[str]
project_permission: str
workspace_role: str
project_role: Optional[ProjectRole]
type: str


Expand Down
74 changes: 39 additions & 35 deletions server/mergin/sync/private_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ paths:
required: true
schema:
type: string
# // Kept for EE (collaborators + invitation) access, TODO: remove when a separate invitation endpoint is implemented
get:
tags:
- project
Expand All @@ -350,34 +351,29 @@ paths:
"404":
$ref: "#/components/responses/NotFoundResp"
x-openapi-router-controller: mergin.sync.private_api_controller
/project/{id}/public:
parameters:
- name: id
in: path
description: Project uuid
required: true
schema:
type: string
patch:
summary: Update direct project access (sharing)
operationId: update_project_access
summary: Update public project flag
operationId: update_project_public_flag
requestBody:
description: Request data
required: true
content:
application/json:
schema:
type: object
properties:
user_id:
type: integer
public:
type: boolean
nullable: true
role:
type: string
enum:
- owner
- writer
- editor
- reader
- none
example: writer
responses:
"200":
$ref: "#/components/schemas/ProjectAccessUpdated"
"204":
description: OK
"400":
$ref: "#/components/responses/BadStatusResp"
"401":
Expand Down Expand Up @@ -551,8 +547,7 @@ components:
- id
- type
- email
- project_permission
- role
- workspace_role
properties:
id:
description: User/Invitation (uu)id
Expand All @@ -569,16 +564,9 @@ components:
type: string
format: email
example: john.doe@example.com
role:
workspace_role:
description: Workspace role
type: string
enum:
- owner
- admin
- writer
- editor
- reader
- guest
$ref: "#/components/schemas/WorkspaceRole"
username:
description: Present only for type `member`
type: string
Expand All @@ -587,13 +575,13 @@ components:
description: Present only for type `member`
type: string
example: John Doe
project_permission:
type: string
enum:
- owner
- writer
- editor
- reader
role:
description: Project role defined as combination of project and workspace roles
$ref: "#/components/schemas/ProjectRole"
project_role:
nullable: true
description: Project role defined in database, not calculated version
$ref: "#/components/schemas/ProjectRole"
invitation:
description: Present only for type `invitation`
type: object
Expand Down Expand Up @@ -658,3 +646,19 @@ components:
items:
type: integer
example: [1]
WorkspaceRole:
type: string
enum:
- owner
- admin
- writer
- editor
- reader
- guest
ProjectRole:
type: string
enum:
- owner
- writer
- editor
- reader
21 changes: 4 additions & 17 deletions server/mergin/sync/private_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
AdminProjectSchema,
ProjectAccessSchema,
ProjectAccessDetailSchema,
ProjectVersionListSchema,
)
from .permissions import (
require_project_by_uuid,
Expand Down Expand Up @@ -304,28 +303,16 @@ def unsubscribe_project(id): # pylint: disable=W0612


@auth_required
def update_project_access(id: str):
"""Modify shared project access
def update_project_public_flag(id: str):
"""Modify the project's public flag

:param id: Project uuid
"""
project = require_project_by_uuid(id, ProjectPermissions.Update)

if "public" in request.json:
project.public = request.json["public"]

if "user_id" in request.json and "role" in request.json:
user = User.query.filter_by(
id=request.json["user_id"], active=True
).first_or_404("User does not exist")

if request.json["role"] == "none":
project.unset_role(user.id)
else:
project.set_role(user.id, ProjectRole(request.json["role"]))
project_access_granted.send(project, user_id=user.id)
project.public = request.json.get("public", False)
db.session.commit()
return ProjectAccessSchema().dump(project), 200
return NoContent, 204


@auth_required
Expand Down
17 changes: 17 additions & 0 deletions server/mergin/sync/public_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,23 @@ components:
detail: Maximum number of people in this workspace is reached. Please upgrade your subscription to add more people (UsersLimitHit)
rejected_emails: [ rejected@example.com ]
users_quota: 6
EditorsLimitHit:
allOf:
- $ref: '#/components/schemas/CustomError'
type: object
properties:
rejected_emails:
nullable: true
type: array
items:
type: string
editors_quota:
type: integer
example:
code: EditorsLimitHit
detail: Maximum number of editors in this workspace is reached. Please upgrade your subscription to add more (EditorsLimitHit)
rejected_emails: [ rejected@example.com ]
editors_quota: 6
UpdateProjectAccessError:
allOf:
- $ref: '#/components/schemas/CustomError'
Expand Down
23 changes: 23 additions & 0 deletions server/mergin/sync/public_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,11 +680,34 @@ def update_project(namespace, project_name): # noqa: E501 # pylint: disable=W0
"""
project = require_project(namespace, project_name, ProjectPermissions.Update)
parsed_access = parse_project_access_update_request(request.json.get("access", {}))

# get current status for easier rollback
modified_user_ids = []
for role in list(ProjectRole.__reversed__()):
modified_user_ids.extend(parsed_access.get(role, []))
current_permissions_map = {
user_id: project.get_role(user_id) for user_id in modified_user_ids
}

# get set of modified user_ids and possible (custom) errors
id_diffs, error = current_app.ws_handler.update_project_members(
project, parsed_access
)

# revert back rejected changes
if error and hasattr(error, "rejected_emails"):
rejected_users = (
db.session.query(User.id)
.filter(User.email.in_(error.rejected_emails))
.all()
)
for user in rejected_users:
if current_permissions_map[user.id] is None:
project.unset_role(user.id)
else:
project.set_role(user.id, current_permissions_map[user.id])
db.session.commit()

if not id_diffs and error:
# nothing was done but there are errors
return jsonify(error.to_dict()), 422
Expand Down
Loading
Loading