Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
af05d6b
Refactor pm_only dependency to admin_only
prabinoid Nov 3, 2025
dd1cec2
Add validator id in task json
prabinoid Nov 6, 2025
032ad4b
save comment input on session storage to persist on reload or page na…
suzit-10 Nov 11, 2025
30db9b0
return empty string if secondary hashtag is undefined/null
suzit-10 Nov 17, 2025
67583e8
Remove composite fk on project teams and add id pk
prabinoid Nov 20, 2025
470b136
provide access to `/manage/teams` for edit access for team managers
suzit-10 Nov 24, 2025
07497e3
Token decode error exception handled
prabinoid Nov 26, 2025
d457b69
fetch list of teams where the user is assigned as team manager
suzit-10 Nov 27, 2025
fee840e
update edit team permission
suzit-10 Nov 27, 2025
681e1e7
adapt test cases for updating the team edit permission logic
suzit-10 Nov 28, 2025
93764a7
Merge pull request #7075 from hotosm/fix/6684-persist-unposted-comment
ramyaragupathy Dec 2, 2025
b9c7d80
Merge pull request #7064 from hotosm/refactor/pm-dependency
ramyaragupathy Dec 2, 2025
a27c658
Merge pull request #7069 from hotosm/feat/tasks-validator-id
ramyaragupathy Dec 2, 2025
52a8af1
Merge pull request #7079 from hotosm/fix/auth-exception
ramyaragupathy Dec 2, 2025
9aa68c0
Update role by ensuring both team id and current role are matched wit…
suzit-10 Dec 3, 2025
67ca25e
remove comment and use optional chaining
suzit-10 Nov 28, 2025
48261f9
Refactor user data fetching to async/await with parallel requests
suzit-10 Nov 28, 2025
6987050
Merge pull request #7099 from hotosm/fix/7091-follow-up-update-team-e…
ramyaragupathy Dec 4, 2025
b0261ac
Merge pull request #7092 from hotosm/fix/7091-unable-to-edit-team
ramyaragupathy Dec 4, 2025
c6259ba
Merge pull request #7101 from hotosm/fix/7100-updates-all-with-same-r…
ramyaragupathy Dec 4, 2025
c8971f4
Merge pull request #7083 from hotosm/fix/7082-listing-undefined-hashtag
ramyaragupathy Dec 4, 2025
6849193
Merge pull request #7090 from hotosm/fix/project-creation-permission
ramyaragupathy Dec 4, 2025
69d8f86
Merge pull request #7102 from hotosm/develop
ramyaragupathy Dec 4, 2025
c8dada3
connsider the team id and role for selection to remove role
suzit-10 Dec 5, 2025
e45ba8e
Filter out roles and show only remaining roles in team options
suzit-10 Dec 5, 2025
8d241da
Merge pull request #7104 from hotosm/fix/7100-updates-all-with-same-r…
ramyaragupathy Dec 5, 2025
1a91104
Filter out roles and show only remaining roles in team options
suzit-10 Dec 5, 2025
cc4ed75
Merge pull request #7106 from hotosm/fix/7100-updates-all-with-same-r…
nischalstha9 Dec 5, 2025
24f92c2
Merge pull request #7105 from hotosm/develop
nischalstha9 Dec 5, 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
8 changes: 4 additions & 4 deletions backend/api/issues/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from backend.models.dtos.mapping_issues_dto import MappingIssueCategoryDTO
from backend.models.dtos.user_dto import AuthUserDTO
from backend.services.mapping_issues_service import MappingIssueCategoryService
from backend.services.users.authentication_service import pm_only
from backend.services.users.authentication_service import admin_only

router = APIRouter(
prefix="/tasks",
Expand Down Expand Up @@ -52,7 +52,7 @@ async def get_issue(category_id: int, db: Database = Depends(get_db)):
async def patch_issue(
request: Request,
category_id: int,
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
data: MappingIssueCategoryDTO = Body(...),
):
Expand Down Expand Up @@ -121,7 +121,7 @@ async def patch_issue(
async def delete_issue(
request: Request,
category_id: int,
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
Expand Down Expand Up @@ -200,7 +200,7 @@ async def get_issues_categories(
@router.post("/issues/categories/", response_model=MappingIssueCategoryDTO)
async def post_issues_categories(
request: Request,
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
data: dict = Body(...),
):
Expand Down
8 changes: 4 additions & 4 deletions backend/api/licenses/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from backend.models.dtos.licenses_dto import LicenseDTO
from backend.models.dtos.user_dto import AuthUserDTO
from backend.services.license_service import LicenseService
from backend.services.users.authentication_service import pm_only
from backend.services.users.authentication_service import admin_only

router = APIRouter(
prefix="/licenses",
Expand All @@ -19,7 +19,7 @@
async def post_license(
license_dto: LicenseDTO,
db: Database = Depends(get_db),
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
):
"""
Creates a new mapping license
Expand Down Expand Up @@ -100,7 +100,7 @@ async def patch_license(
license_dto: LicenseDTO,
license_id: int,
db: Database = Depends(get_db),
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
):
"""
Update a specified mapping license
Expand Down Expand Up @@ -155,7 +155,7 @@ async def patch_license(
async def delete_license(
license_id: int,
db: Database = Depends(get_db),
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
):
"""
Delete a specified mapping license
Expand Down
8 changes: 4 additions & 4 deletions backend/api/mapping_badges/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
)
from backend.models.dtos.user_dto import AuthUserDTO
from backend.services.mapping_badges import MappingBadgeService
from backend.services.users.authentication_service import pm_only
from backend.services.users.authentication_service import admin_only

router = APIRouter(
prefix="/badges",
Expand Down Expand Up @@ -39,7 +39,7 @@ async def get_mapping_badges(
async def create_mapping_badge(
data: MappingBadgeCreateDTO,
db: Database = Depends(get_db),
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
) -> MappingBadgeDTO:
"""
Creates a new MappingBadge
Expand Down Expand Up @@ -73,7 +73,7 @@ async def update_mapping_badge(
data: MappingBadgeUpdateDTO,
badge_id: int,
db: Database = Depends(get_db),
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
) -> MappingBadgeDTO:
"""
Updates a mapping badge
Expand All @@ -92,7 +92,7 @@ async def update_mapping_badge(
async def delete_mapping_badge(
badge_id: int,
db: Database = Depends(get_db),
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
):
"""
Deletes a mapping badge
Expand Down
8 changes: 4 additions & 4 deletions backend/api/mapping_levels/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
)
from backend.models.dtos.user_dto import AuthUserDTO
from backend.services.mapping_levels import MappingLevelService
from backend.services.users.authentication_service import pm_only
from backend.services.users.authentication_service import admin_only

router = APIRouter(
prefix="/levels",
Expand Down Expand Up @@ -36,7 +36,7 @@ async def get_mapping_levels(
async def create_mapping_level(
data: MappingLevelCreateDTO,
db: Database = Depends(get_db),
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
):
"""
Create a new mapping level
Expand Down Expand Up @@ -70,7 +70,7 @@ async def update_mapping_level(
data: MappingLevelUpdateDTO,
level_id: int,
db: Database = Depends(get_db),
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
):
"""
Update a given mapping level
Expand All @@ -89,7 +89,7 @@ async def update_mapping_level(
async def delete_mapping_level(
level_id: int,
db: Database = Depends(get_db),
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
):
"""
Delete the specified mapping level
Expand Down
12 changes: 6 additions & 6 deletions backend/api/users/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from backend.models.dtos.user_dto import AuthUserDTO, UserDTO, UserRegisterEmailDTO
from backend.services.interests_service import InterestService
from backend.services.messaging.message_service import MessageService
from backend.services.users.authentication_service import login_required, pm_only
from backend.services.users.authentication_service import login_required, admin_only
from backend.services.users.user_service import UserService, UserServiceError

router = APIRouter(
Expand Down Expand Up @@ -117,7 +117,7 @@ async def set_mapping_level(
request: Request,
username,
level,
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
Expand Down Expand Up @@ -173,7 +173,7 @@ async def set_user_role(
request: Request,
username: str,
role: str,
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
Expand Down Expand Up @@ -228,7 +228,7 @@ async def set_user_role(
async def update_stats(
request: Request,
username: str,
_: AuthUserDTO = Depends(pm_only),
_: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
Expand All @@ -255,7 +255,7 @@ async def update_stats(
async def approve_level(
request: Request,
username: str,
voter: AuthUserDTO = Depends(pm_only),
voter: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
Expand All @@ -282,7 +282,7 @@ async def set_user_is_expert(
request: Request,
user_name,
is_expert,
user: AuthUserDTO = Depends(pm_only),
user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
Expand Down
26 changes: 15 additions & 11 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from pyinstrument import Profiler
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from starlette.middleware.authentication import AuthenticationMiddleware

from backend.config import settings
from backend.db import db_connection
from backend.exceptions import BadRequest, Conflict, Forbidden, NotFound, Unauthorized
Expand Down Expand Up @@ -55,16 +54,21 @@ async def lifespan(app):
# Custom exception handler for invalid token and logout.
@_app.exception_handler(HTTPException)
async def custom_http_exception_handler(request: Request, exc: HTTPException):
if exc.status_code == 401 and "InvalidToken" in exc.detail.get("SubCode", ""):
return JSONResponse(
content={
"Error": exc.detail["Error"],
"SubCode": exc.detail["SubCode"],
},
status_code=exc.status_code,
headers={"WWW-Authenticate": "Bearer"},
)

try:
if exc.status_code == 401 and "InvalidToken" in exc.detail.get(
"SubCode", ""
):
return JSONResponse(
content={
"Error": exc.detail["Error"],
"SubCode": exc.detail["SubCode"],
},
status_code=exc.status_code,
headers={"WWW-Authenticate": "Bearer"},
)
except Exception as e:
logging.debug(f"Exception while handling custom HTTPException: {e}")
pass
if isinstance(exc.detail, dict) and "error" in exc.detail:
error_response = exc.detail
else:
Expand Down
11 changes: 9 additions & 2 deletions backend/models/postgis/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
orm,
select,
update,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.hybrid import hybrid_property
Expand Down Expand Up @@ -96,10 +97,16 @@

class ProjectTeams(Base):
__tablename__ = "project_teams"
team_id = Column(Integer, ForeignKey("teams.id"), primary_key=True)
project_id = Column(Integer, ForeignKey("projects.id"), primary_key=True)

id = Column(BigInteger, primary_key=True, autoincrement=True)
team_id = Column(Integer, ForeignKey("teams.id"), nullable=False)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
role = Column(Integer, nullable=False)

__table_args__ = (
UniqueConstraint("team_id", "project_id", "role", name="uq_project_team_role"),
)

project = relationship(
"Project", backref=backref("teams", cascade="all, delete-orphan")
)
Expand Down
4 changes: 3 additions & 1 deletion backend/models/postgis/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -1396,7 +1396,8 @@ async def get_tasks_as_geojson_feature_collection(
t.task_status,
ST_AsGeoJSON(t.geometry) AS geojson,
t.locked_by,
t.mapped_by
t.mapped_by,
t.validated_by
FROM tasks t
WHERE t.project_id = :project_id
"""
Expand Down Expand Up @@ -1449,6 +1450,7 @@ async def get_tasks_as_geojson_feature_collection(
taskStatus=TaskStatus(row["task_status"]).name,
lockedBy=row["locked_by"],
mappedBy=row["mapped_by"],
validatedBy=row["validated_by"],
)
feature = geojson.Feature(
geometry=task_geometry, properties=task_properties
Expand Down
21 changes: 13 additions & 8 deletions backend/services/users/authentication_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,18 @@ def verify_token(token):
class TokenAuthBackend(AuthenticationBackend):
async def authenticate(self, conn):
if "authorization" not in conn.headers:
return
return None

auth = conn.headers["authorization"]
try:
scheme, credentials = auth.split()
if scheme.lower() != "token":
return
return None
try:
decoded_token = base64.b64decode(credentials).decode("ascii")
except UnicodeDecodeError:
logger.debug("Unable to decode token")
return False
return None
except (ValueError, UnicodeDecodeError, binascii.Error):
raise AuthenticationError("Invalid auth credentials")

Expand All @@ -90,7 +90,7 @@ async def authenticate(self, conn):
)
if not valid_token:
logger.debug("Token not valid.")
return
return None
tm.authenticated_user_id = user_id
return AuthCredentials(["authenticated"]), SimpleUser(user_id)

Expand Down Expand Up @@ -251,7 +251,6 @@ async def login_required(
raise AuthenticationError("Invalid auth credentials")
valid_token, user_id = AuthenticationService.is_valid_token(decoded_token, 604800)
if not valid_token:
logger.debug("Token not valid")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"Error": "Token is expired or invalid", "SubCode": "InvalidToken"},
Expand All @@ -275,17 +274,23 @@ async def login_required_optional(
decoded_token = base64.b64decode(credentials).decode("ascii")
except UnicodeDecodeError:
logger.debug("Unable to decode token")
raise HTTPException(status_code=401, detail="Invalid token")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"Error": "Token is expired or invalid",
"SubCode": "InvalidToken",
},
headers={"WWW-Authenticate": "Bearer"},
)
except (ValueError, UnicodeDecodeError, binascii.Error):
raise AuthenticationError("Invalid auth credentials")
valid_token, user_id = AuthenticationService.is_valid_token(decoded_token, 604800)
if not valid_token:
logger.debug("Token not valid")
return None
return AuthUserDTO(id=user_id)


async def pm_only(
async def admin_only(
Authorization: str = Security(APIKeyHeader(name="Authorization")),
db: Database = Depends(get_db),
):
Expand Down
22 changes: 20 additions & 2 deletions frontend/src/components/comments/commentInput.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useEffect, useState } from 'react';
import { useRef, useEffect, useState, useCallback } from 'react';
import { useSelector } from 'react-redux';
import MDEditor from '@uiw/react-md-editor';
import Tribute from 'tributejs';
Expand All @@ -21,6 +21,7 @@ import { CurrentUserAvatar } from '../user/avatar';
const maxFileSize = 1 * 1024 * 1024; // 1MB

function CommentInputField({
sessionkey,
comment,
setComment,
contributors,
Expand Down Expand Up @@ -130,6 +131,23 @@ function CommentInputField({
});
};

useEffect(() => {
if (!sessionkey) return;
const commenEvent = sessionStorage.getItem(sessionkey);
if (commenEvent) {
setComment(commenEvent);
}
}, [sessionkey, setComment]);

const onCommentChange = useCallback(
(e) => {
setComment(e);
if (!sessionkey) return;
sessionStorage.setItem(sessionkey, e);
},
[sessionkey, setComment],
);

return (
<div {...getRootProps()}>
{isShowTabNavs && (
Expand Down Expand Up @@ -165,7 +183,7 @@ function CommentInputField({
extraCommands={[]}
height={200}
value={comment}
onChange={setComment}
onChange={onCommentChange}
textareaProps={{
...getInputProps(),
spellCheck: 'true',
Expand Down
Loading
Loading