Skip to content

feat: Azure groups#41

Merged
lorenzocorallo merged 3 commits into
mainfrom
azure-groups
Jul 21, 2026
Merged

feat: Azure groups#41
lorenzocorallo merged 3 commits into
mainfrom
azure-groups

Conversation

@lorenzocorallo

Copy link
Copy Markdown
Member

No description provided.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@lorenzocorallo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42c38245-2e9e-4589-be97-cd0799f15774

📥 Commits

Reviewing files that changed from the base of the PR and between 9d2ba07 and 80044f5.

📒 Files selected for processing (2)
  • package.json
  • package/package.json

Walkthrough

Adds Azure group retrieval and membership management through Graph helpers, tRPC procedures, and development HTTP routes. It also changes latest-guide query handling and logs the fetched result.

Changes

Azure group management

Layer / File(s) Summary
Group data contracts
src/azure/functions/groups.ts, src/azure/types.ts
Defines Graph group input fields and the normalized ParsedGroup output shape.
Microsoft Graph group operations
src/azure/functions/groups.ts
Fetches and normalizes groups, and adds retryable member insertion and removal operations.
Group API entry points
src/routers/azure/groups.ts, src/routers/azure/index.ts, src/server.ts
Exposes validated tRPC procedures and development-only HTTP routes for group retrieval and membership changes.

Guide query adjustment

Layer / File(s) Summary
Latest guide result handling
src/routers/web/guides_matricole.ts
Stores the query result array, logs it, and returns its first item or null.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant TRPCRouter
  participant HonoServer
  participant AzureGroupHelpers
  participant MicrosoftGraph
  Client->>TRPCRouter: Request group operation
  TRPCRouter->>AzureGroupHelpers: Validate and delegate request
  Client->>HonoServer: Call development test route
  HonoServer->>AzureGroupHelpers: Invoke group operation
  AzureGroupHelpers->>MicrosoftGraph: Fetch groups or mutate membership
  MicrosoftGraph-->>AzureGroupHelpers: Return Graph response
  AzureGroupHelpers-->>TRPCRouter: ParsedGroup[] or boolean
  AzureGroupHelpers-->>HonoServer: Groups or operation status
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding Azure group APIs and related router/endpoints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lorenzocorallo
lorenzocorallo merged commit fe27ead into main Jul 21, 2026
1 of 2 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
src/routers/azure/groups.ts (1)

10-10: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Validate Azure Object IDs as UUIDs to prevent path traversal.

Since groupId and userId are interpolated directly into the Microsoft Graph API URL path without sanitization, using a generic string validator allows potential path injection. Azure Object IDs are standard UUIDs, so they should be strictly validated using Zod 4's new standalone z.uuid() schema.

  • src/routers/azure/groups.ts#L10-L10: Replace groupId: z.string(), userId: z.string() with groupId: z.uuid(), userId: z.uuid().
  • src/routers/azure/groups.ts#L15-L15: Apply the same strict UUID validation.
  • src/server.ts#L100-L100: Replace groupId: z.string(), userId: z.string() with groupId: z.uuid(), userId: z.uuid().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routers/azure/groups.ts` at line 10, Replace the generic string
validators for groupId and userId with strict z.uuid() validation in
src/routers/azure/groups.ts at lines 10 and 15, and in src/server.ts at line
100. Apply the same UUID schema consistently to all affected input definitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/azure/functions/groups.ts`:
- Around line 31-35: Await the asynchronous withRetry calls in both
addGroupMember (src/azure/functions/groups.ts lines 31-35) and removeGroupMember
(src/azure/functions/groups.ts line 45), so retries complete before returning
and failures reach each method’s local catch block.
- Line 21: Update the members mapping in the group transformation to handle
g.members being absent by using optional chaining with an empty-array fallback
before calling map, while preserving the existing displayName normalization.
- Around line 13-16: Update the group-fetching flow around the Graph client
request so it follows every `@odata.nextLink` page for both groups and each
group’s expanded members, rather than returning only the initial value. Preserve
the existing Group[] result shape and selected fields while aggregating all
pages before returning.

In `@src/routers/azure/groups.ts`:
- Around line 6-18: Replace publicProcedure with protectedProcedure or the
existing admin-only wrapper for getAll, addMember, and removeMember in the Azure
group router, ensuring all group reads and membership mutations require an
authenticated session.

In `@src/routers/web/guides_matricole.ts`:
- Line 34: Remove the raw console.log call in the public procedure; do not log
complete database rows or sensitive fields. If diagnostic logging is required,
use the existing logger at debug level and include only an explicit allowlist of
non-sensitive fields.

In `@src/server.ts`:
- Line 92: Update both production-disabled route guards in src/server.ts at
lines 92-92 and 102-102 to return c.notFound() instead of c.status(500),
preserving the existing NODE_ENV checks.

---

Nitpick comments:
In `@src/routers/azure/groups.ts`:
- Line 10: Replace the generic string validators for groupId and userId with
strict z.uuid() validation in src/routers/azure/groups.ts at lines 10 and 15,
and in src/server.ts at line 100. Apply the same UUID schema consistently to all
affected input definitions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f9493e70-7dd7-4f18-931a-6292899d4864

📥 Commits

Reviewing files that changed from the base of the PR and between 7a9caff and 9d2ba07.

📒 Files selected for processing (6)
  • src/azure/functions/groups.ts
  • src/azure/types.ts
  • src/routers/azure/groups.ts
  • src/routers/azure/index.ts
  • src/routers/web/guides_matricole.ts
  • src/server.ts

Comment on lines +13 to +16
const res: Group[] = await client
.api("/groups?$select=id,displayName,mailNickname,mailEnabled&$expand=members($select=id,displayName)")
.get()
.then((r) => r.value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Look for existing Graph API pagination utilities.
rg -i 'nextLink|PageIterator|paginate' src/

Repository: PoliNetworkOrg/backend

Length of output: 160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== groups.ts outline =="
ast-grep outline src/azure/functions/groups.ts --view expanded || true

echo "\n== groups.ts excerpt =="
wc -l src/azure/functions/groups.ts
sed -n '1,220p' src/azure/functions/groups.ts

echo "\n== Graph pagination searches =="
rg -n "`@odata`\.nextLink|PageIterator|paginate|nextLink" src/azure || true

echo "\n== Graph client usages =="
rg -n "\.api\\(\"/groups|graph" src/azure/functions src/azure || true

Repository: PoliNetworkOrg/backend

Length of output: 4364


Handle Graph pagination for groups and members. This only reads the first value; later /groups pages and expanded members pages from @odata.nextLink will be dropped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/azure/functions/groups.ts` around lines 13 - 16, Update the
group-fetching flow around the Graph client request so it follows every
`@odata.nextLink` page for both groups and each group’s expanded members, rather
than returning only the initial value. Preserve the existing Group[] result
shape and selected fields while aggregating all pages before returning.

...g,
displayName: displayName ?? "",
mailAddress: mailEnabled ? `${mailNickname}@polinetwork.org` : null,
members: g.members.map((m) => ({ ...m, displayName: m.displayName ?? "" })),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Safeguard against missing members array.

If a group has no members, the Graph API may omit the members property entirely despite the $expand directive. Calling .map directly on an undefined value will crash the application. Use optional chaining with a fallback array.

🛡️ Proposed defensive fallback
-      members: g.members.map((m) => ({ ...m, displayName: m.displayName ?? "" })),
+      members: g.members?.map((m) => ({ ...m, displayName: m.displayName ?? "" })) ?? [],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
members: g.members.map((m) => ({ ...m, displayName: m.displayName ?? "" })),
members: g.members?.map((m) => ({ ...m, displayName: m.displayName ?? "" })) ?? [],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/azure/functions/groups.ts` at line 21, Update the members mapping in the
group transformation to handle g.members being absent by using optional chaining
with an empty-array fallback before calling map, while preserving the existing
displayName normalization.

Comment on lines +31 to +35
const res = withRetry(() =>
client.api(`/groups/${groupId}/members/$ref`).post({
"@odata.id": `https://graph.microsoft.com/v1.0/directoryObjects/${userId}`,
})
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Await the asynchronous withRetry wrapper.

The withRetry utility returns a Promise, but it is being called synchronously. This execution bypasses the local catch block (resulting in Unhandled Promise Rejections on failure), breaks the intended backoff behavior, and returns true prematurely before the Graph API operation finishes.

  • src/azure/functions/groups.ts#L31-L35: Add await before the withRetry call inside addGroupMember.
  • src/azure/functions/groups.ts#L45-L45: Add await before the withRetry call inside removeGroupMember.
📍 Affects 1 file
  • src/azure/functions/groups.ts#L31-L35 (this comment)
  • src/azure/functions/groups.ts#L45-L45
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/azure/functions/groups.ts` around lines 31 - 35, Await the asynchronous
withRetry calls in both addGroupMember (src/azure/functions/groups.ts lines
31-35) and removeGroupMember (src/azure/functions/groups.ts line 45), so retries
complete before returning and failures reach each method’s local catch block.

Comment on lines +6 to +18
getAll: publicProcedure.query(async () => {
return await getAllGroups()
}),
addMember: publicProcedure
.input(z.object({ groupId: z.string(), userId: z.string() }))
.mutation(async ({ input }) => {
return await addGroupMember(input.groupId, input.userId)
}),
removeMember: publicProcedure
.input(z.object({ groupId: z.string(), userId: z.string() }))
.mutation(async ({ input }) => {
return await removeGroupMember(input.groupId, input.userId)
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find available secured TRPC procedures.
rg -o 'export const [a-zA-Z0-9_]+Procedure' src/trpc.ts

Repository: PoliNetworkOrg/backend

Length of output: 221


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/routers/azure/groups.ts ==\n'
cat -n src/routers/azure/groups.ts

printf '\n== src/trpc.ts ==\n'
cat -n src/trpc.ts

printf '\n== group helper references ==\n'
rg -n "getAllGroups|addGroupMember|removeGroupMember|protectedProcedure|publicProcedure" src -S

Repository: PoliNetworkOrg/backend

Length of output: 10947


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/routers/azure/groups.ts
printf '\n---\n'
sed -n '1,220p' src/trpc.ts
printf '\n---\n'
rg -n "function (getAllGroups|addGroupMember|removeGroupMember)|const (getAllGroups|addGroupMember|removeGroupMember)|export .* (getAllGroups|addGroupMember|removeGroupMember)" src -S

Repository: PoliNetworkOrg/backend

Length of output: 3108


Restrict Azure group routes to authenticated access. publicProcedure leaves getAll, addMember, and removeMember callable without a session, so anyone can read group data and mutate Azure AD memberships. Switch these to protectedProcedure or an admin-only wrapper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routers/azure/groups.ts` around lines 6 - 18, Replace publicProcedure
with protectedProcedure or the existing admin-only wrapper for getAll,
addMember, and removeMember in the Azure group router, ensuring all group reads
and membership mutations require an authenticated session.

getLatestGuide: publicProcedure.output(guideSchema.nullable()).query(async () => {
const [latestGuide] = await DB.select().from(GUIDES_MATRICOLE).orderBy(desc(GUIDES_MATRICOLE.date)).limit(1)
const res = await DB.select().from(GUIDES_MATRICOLE).orderBy(desc(GUIDES_MATRICOLE.date)).limit(1)
console.log(res, res[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove raw database-row logging from the public procedure.

console.log(res, res[0]) logs the complete database row, including fields such as createdBy, modifiedBy, and timestamps that are not exposed by guideSchema. Remove it or use the existing logger at an appropriate debug level with an explicit, non-sensitive field allowlist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routers/web/guides_matricole.ts` at line 34, Remove the raw console.log
call in the public procedure; do not log complete database rows or sensitive
fields. If diagnostic logging is required, use the existing logger at debug
level and include only an explicit allowlist of non-sensitive fields.

Comment thread src/server.ts
})

app.get("/test/azure-groups", async (c) => {
if (env.NODE_ENV === "production") return c.status(500)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Return 404 instead of 500 for disabled dev routes.

Returning a 500 (Internal Server Error) for routes that are intentionally disabled in production is semantically incorrect and will falsely trigger error monitors. Use a 403 or 404 response instead.

  • src/server.ts#L92-L92: Replace return c.status(500) with return c.notFound().
  • src/server.ts#L102-L102: Replace return c.status(500) with return c.notFound().
📍 Affects 1 file
  • src/server.ts#L92-L92 (this comment)
  • src/server.ts#L102-L102
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.ts` at line 92, Update both production-disabled route guards in
src/server.ts at lines 92-92 and 102-102 to return c.notFound() instead of
c.status(500), preserving the existing NODE_ENV checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant