Conversation
📝 WalkthroughWalkthroughAdds a complete invite subsystem: public invite acceptance page and layout, unauthenticated invite APIs, invite-related types, UI for creating/managing invites (modal + table), activity log entries for invite events, auth bypass for /invite routes, and version display + CI versioning updates. Changes
Sequence Diagram(s)sequenceDiagram
actor InvitedUser as Invited User
participant InvitePage as Invite Page (client)
participant APIClient as Unauthenticated API Helpers
participant Backend as Backend
InvitedUser->>InvitePage: Open /invite?token=XYZ
InvitePage->>APIClient: fetchInviteInfo(token)
APIClient->>Backend: GET /users/invites/{token}
Backend-->>APIClient: UserInviteInfo (email,name,expires_at,valid,invited_by)
APIClient-->>InvitePage: Invite info
InvitePage->>InvitedUser: Render form + password rules
InvitedUser->>InvitePage: Enter password & confirm
InvitePage->>InvitePage: Validate rules & match
InvitedUser->>InvitePage: Click "Accept Invite"
InvitePage->>APIClient: acceptInvite(token, password)
APIClient->>Backend: POST /users/invites/{token}/accept {password}
alt Success
Backend-->>APIClient: { success: true }
APIClient-->>InvitePage: Success
InvitePage->>InvitedUser: Show success and redirect to login
else Error
Backend-->>APIClient: Error
APIClient-->>InvitePage: Error
InvitePage->>InvitedUser: Show error message
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/modules/users/UserInviteModal.tsx (1)
206-277: ValidateexpiresInbefore converting to seconds.Line 249 uses
parseInton user input without JS-side validation. While the Input component has HTML5min={1}, this is not reliable across browsers. Empty, invalid, or NaN values can reach the backend asexpires_in, potentially causing immediate expiry or rejection. Additionally, theisDisabledcheck does not account for invalid expiry values, allowing submission in invite mode when expiresIn is invalid.Clamp the value to ≥1 and add validation to
isDisabledto prevent submit when the expiry is invalid in invite mode.🐛 Suggested fix
const createInvite = async () => { const groups = await saveGroups(); const groupIds = groups.map((group) => group.id) as string[]; + const expiresDays = Math.max( + 1, + Number.parseInt(expiresIn, 10) || 1, + ); notify({ title: "Create Invite", description: `Creating invite link for ${name}...`, promise: inviteRequest .post({ name, email, role, auto_groups: groupIds, - expires_in: parseInt(expiresIn || "3") * 24 * 60 * 60, // Days to seconds + expires_in: expiresDays * 24 * 60 * 60, // Days to seconds }) .then((invite) => { mutate("/users?service_user=false"); onInviteCreated && onInviteCreated(invite); }), loadingMessage: "Creating invite...", }); }; const isDisabled = useMemo(() => { - return name.length === 0 || !isValidEmail; -}, [name, isValidEmail]); + const expiresDays = Number.parseInt(expiresIn, 10); + const invalidExpires = + !isCloud && + mode === "invite" && + (!Number.isFinite(expiresDays) || expiresDays < 1); + return name.length === 0 || !isValidEmail || invalidExpires; +}, [name, isValidEmail, expiresIn, isCloud, mode]);
🤖 Fix all issues with AI agents
In `@src/app/invite/page.tsx`:
- Line 66: The displayed special-character hint in the invite page is
inconsistent with the validator: the regex referenced by hasSpecialChar
(/[!@#$%^&*(),.?":{}|<>]/.test(password)) accepts many more characters than the
UI hint describes; fix by either updating the regex used for validation
(hasSpecialChar) to only include the documented set (!@#$%^&*) or updating the
UI hint text to list all allowed characters (e.g., include , . " : { } | < > and
others present in the regex) so the password validation and the visible
requirement string in the page component remain identical.
In `@src/modules/users/UserInvitesTable.tsx`:
- Around line 410-422: The default sorting state uses a non-existent column id
"is_current" which prevents initial sort from applying; update the
useLocalStorage initial value in the sorting state (the call to
useLocalStorage<SortingState> that sets "netbird-table-sort-invites" + path) to
reference a valid column id from InvitesTableColumns (e.g., replace "is_current"
with an existing id such as "name" or the actual status column id defined in
InvitesTableColumns) so the sorting and persisted state are consistent with the
table's column definitions.
🧹 Nitpick comments (3)
src/app/invite/page.tsx (1)
80-80: Consider using a typed error instead ofany.Using
anybypasses TypeScript's type checking. TheApiErrorinterface from the unauthenticated API could be used here.♻️ Suggested improvement
+import { ApiError } from "@/interfaces/Instance"; + // ... - } catch (err: any) { - setError(err.message || "Failed to accept invite"); + } catch (err) { + const apiError = err as ApiError; + setError(apiError.message || "Failed to accept invite"); } finally {src/utils/unauthenticatedApi.ts (1)
57-69: Consider URL-encoding the token parameter for defensive programming.While the invite tokens currently work unencoded in both path and query parameters throughout the codebase, URL-encoding is a best practice if tokens could contain special characters. The codebase already uses
encodeURIComponent()in similar contexts elsewhere (e.g.,SSHCredentialsModal.tsx,api.tsx), so applying it here would align with existing patterns.♻️ Suggested refactoring
export async function fetchInviteInfo(token: string): Promise<UserInviteInfo> { - return unauthenticatedRequest<UserInviteInfo>("GET", `/users/invites/${token}`); + return unauthenticatedRequest<UserInviteInfo>("GET", `/users/invites/${encodeURIComponent(token)}`); } export async function acceptInvite( token: string, password: string, ): Promise<UserInviteAcceptResponse> { return unauthenticatedRequest<UserInviteAcceptResponse>( "POST", - `/users/invites/${token}/accept`, + `/users/invites/${encodeURIComponent(token)}/accept`, { password }, ); }src/interfaces/User.ts (1)
20-63: PreferRoleenum for invite role fields.Using
stringhere bypasses the existingRoleenum and forces downstream casts. Aligning these toRoleimproves type safety without changing runtime behavior.♻️ Suggested type alignment
export interface UserInviteCreateRequest { email: string; name: string; - role: string; + role: Role; auto_groups: string[]; expires_in?: number; } export interface UserInviteCreateResponse { id: string; email: string; name: string; - role: string; + role: Role; auto_groups: string[]; status: string; invite_link: string; invite_expires_at: string; } export interface UserInviteListItem { id: string; email: string; name: string; - role: string; + role: Role; auto_groups: string[]; expires_at: string; created_at: string; expired: boolean; }
| const hasUppercase = /[A-Z]/.test(password); | ||
| const hasLowercase = /[a-z]/.test(password); | ||
| const hasNumber = /[0-9]/.test(password); | ||
| const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, read the file to verify the exact content at lines 66 and 224
echo "=== Reading src/app/invite/page.tsx ==="
wc -l src/app/invite/page.tsxRepository: netbirdio/dashboard
Length of output: 130
🏁 Script executed:
#!/bin/bash
# Read lines around line 66
echo "=== Lines 60-75 (around line 66) ==="
sed -n '60,75p' src/app/invite/page.tsx | cat -nRepository: netbirdio/dashboard
Length of output: 849
🏁 Script executed:
#!/bin/bash
# Read lines around line 224
echo "=== Lines 218-230 (around line 224) ==="
sed -n '218,230p' src/app/invite/page.tsx | cat -nRepository: netbirdio/dashboard
Length of output: 782
🏁 Script executed:
#!/bin/bash
# Search for password validation in backend
echo "=== Searching for password validation patterns ==="
rg -n "password.*valid|hasSpecialChar|special.*char|minLength" --type=ts --type=tsx -C2Repository: netbirdio/dashboard
Length of output: 158
🏁 Script executed:
#!/bin/bash
# Search more broadly for validation rules
echo "=== Searching for validation schemas or requirements ==="
rg -n "schema|zod|validation|password" --type=ts --type=tsx | grep -i "password\|special\|schema" | head -40Repository: netbirdio/dashboard
Length of output: 164
🏁 Script executed:
#!/bin/bash
# Search for API calls related to password validation
echo "=== Searching for password API endpoints ==="
rg -n "password" --type=ts -l | head -20Repository: netbirdio/dashboard
Length of output: 727
🏁 Script executed:
#!/bin/bash
# Search in common backend directories
echo "=== Searching backend directory structure ==="
fd -t f -e ts -e js "password" --max-depth 3 2>/dev/null | grep -E "api|server|backend" | head -20Repository: netbirdio/dashboard
Length of output: 108
🏁 Script executed:
#!/bin/bash
# Look for API calls in the invite page itself
echo "=== API call in invite page ==="
sed -n '70,120p' src/app/invite/page.tsx | cat -nRepository: netbirdio/dashboard
Length of output: 1873
🏁 Script executed:
#!/bin/bash
# Search for password-related API or validation constants
echo "=== Searching for password requirements or API ==="
rg -n "password\|validation\|requirements" src/ -i | grep -i "password\|special" | head -30Repository: netbirdio/dashboard
Length of output: 113
🏁 Script executed:
#!/bin/bash
# Find where acceptInvite is imported from
echo "=== Searching for acceptInvite import ==="
rg -n "acceptInvite|import.*acceptInvite" src/app/invite/page.tsx -B2 -A2Repository: netbirdio/dashboard
Length of output: 447
🏁 Script executed:
#!/bin/bash
# Find acceptInvite function definition
echo "=== Finding acceptInvite function ==="
rg -n "acceptInvite|function acceptInvite|const acceptInvite" src/ -A10 | head -50Repository: netbirdio/dashboard
Length of output: 1890
🏁 Script executed:
#!/bin/bash
# Check the unauthenticatedApi file since it was mentioned
echo "=== Checking unauthenticatedApi.ts ==="
cat -n src/utils/unauthenticatedApi.tsRepository: netbirdio/dashboard
Length of output: 2374
Align special character validation hint with regex.
The regex at line 66 accepts 16 special characters (!@#$%^&*(),.?":{}|<>), but the UI hint at line 224 only documents 8 of them (!@#$%^&*). Users can successfully use . or , but won't know these are valid, creating confusion.
Either expand the hint to show all accepted characters, or reduce the regex to match the documented set:
Suggested fix
Option 1 - Simplify regex to match hint:
- const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
+ const hasSpecialChar = /[!@#$%^&*()]/.test(password);Option 2 - Expand hint to show all accepted characters:
- <PasswordRule met={hasSpecialChar} text="One special character (!@#$%^&*)" />
+ <PasswordRule met={hasSpecialChar} text="One special character (!@#$%^&*(),.?\":{}|<>)" />📝 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.
| const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); | |
| const hasSpecialChar = /[!@#$%^&*()]/.test(password); |
🤖 Prompt for AI Agents
In `@src/app/invite/page.tsx` at line 66, The displayed special-character hint in
the invite page is inconsistent with the validator: the regex referenced by
hasSpecialChar (/[!@#$%^&*(),.?":{}|<>]/.test(password)) accepts many more
characters than the UI hint describes; fix by either updating the regex used for
validation (hasSpecialChar) to only include the documented set (!@#$%^&*) or
updating the UI hint text to list all allowed characters (e.g., include , . " :
{ } | < > and others present in the regex) so the password validation and the
visible requirement string in the page component remain identical.
| // Default sorting state of the table | ||
| const [sorting, setSorting] = useLocalStorage<SortingState>( | ||
| "netbird-table-sort-invites" + path, | ||
| [ | ||
| { | ||
| id: "is_current", | ||
| desc: true, | ||
| }, | ||
| { | ||
| id: "name", | ||
| desc: true, | ||
| }, | ||
| ], |
There was a problem hiding this comment.
Default sorting references a non-existent column.
Line 415 uses is_current, which isn’t defined in InvitesTableColumns, so the initial sort will be ignored and stored state may be inconsistent. Use an existing column id instead.
🐛 Suggested fix
const [sorting, setSorting] = useLocalStorage<SortingState>(
"netbird-table-sort-invites" + path,
[
- {
- id: "is_current",
- desc: true,
- },
{
+ id: "expires_at",
+ desc: true,
+ },
+ {
id: "name",
desc: true,
},
],
);🤖 Prompt for AI Agents
In `@src/modules/users/UserInvitesTable.tsx` around lines 410 - 422, The default
sorting state uses a non-existent column id "is_current" which prevents initial
sort from applying; update the useLocalStorage initial value in the sorting
state (the call to useLocalStorage<SortingState> that sets
"netbird-table-sort-invites" + path) to reference a valid column id from
InvitesTableColumns (e.g., replace "is_current" with an existing id such as
"name" or the actual status column id defined in InvitesTableColumns) so the
sorting and persisted state are consistent with the table's column definitions.
* Add version component * Add version update * Add version update * Show version only for self-hosted * Add version to dashboard on CI/CD
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/components/VersionInfo.tsx`:
- Around line 20-35: The compareVersions function fails to handle version
strings with a leading prefix like "v1.2.3"; normalize both inputs at the start
of compareVersions by trimming whitespace and stripping any leading non-numeric
characters (e.g., remove a leading "v" or other prefix via a regex) before
splitting and parsing so the major/minor/patch components parse correctly;
update compareVersions to sanitize current and latest (e.g., current =
current.trim().replace(/^[^\d]*/, "") and same for latest) then proceed with the
existing split/parse/compare logic.
♻️ Duplicate comments (1)
src/app/invite/page.tsx (1)
74-74: Special character validation mismatch between regex and UI hint.The regex accepts 16 special characters (
!@#$%^&*(),.?":{}|<>), but the UI hint at line 263 only shows 8 (!@#$%^&*). This creates user confusion as valid characters aren't documented.
| function compareVersions(current: string, latest: string): boolean { | ||
| // Returns true if latest is newer than current | ||
| if (!current || !latest) return false; | ||
| if (current === "development") return false; | ||
|
|
||
| const currentParts = current.split(".").map((p) => parseInt(p, 10) || 0); | ||
| const latestParts = latest.split(".").map((p) => parseInt(p, 10) || 0); | ||
|
|
||
| for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { | ||
| const c = currentParts[i] || 0; | ||
| const l = latestParts[i] || 0; | ||
| if (l > c) return true; | ||
| if (l < c) return false; | ||
| } | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Normalize version strings before comparison.
If versions include a leading prefix (e.g., “v1.2.3”), the major component parses to 0 and a major bump (v1 → v2) can be missed.
🛠️ Suggested fix
- const currentParts = current.split(".").map((p) => parseInt(p, 10) || 0);
- const latestParts = latest.split(".").map((p) => parseInt(p, 10) || 0);
+ const normalize = (v: string) => v.trim().replace(/^[^\d]*/, "");
+ const currentParts = normalize(current).split(".").map((p) => parseInt(p, 10) || 0);
+ const latestParts = normalize(latest).split(".").map((p) => parseInt(p, 10) || 0);📝 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.
| function compareVersions(current: string, latest: string): boolean { | |
| // Returns true if latest is newer than current | |
| if (!current || !latest) return false; | |
| if (current === "development") return false; | |
| const currentParts = current.split(".").map((p) => parseInt(p, 10) || 0); | |
| const latestParts = latest.split(".").map((p) => parseInt(p, 10) || 0); | |
| for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { | |
| const c = currentParts[i] || 0; | |
| const l = latestParts[i] || 0; | |
| if (l > c) return true; | |
| if (l < c) return false; | |
| } | |
| return false; | |
| } | |
| function compareVersions(current: string, latest: string): boolean { | |
| // Returns true if latest is newer than current | |
| if (!current || !latest) return false; | |
| if (current === "development") return false; | |
| const normalize = (v: string) => v.trim().replace(/^[^\d]*/, ""); | |
| const currentParts = normalize(current).split(".").map((p) => parseInt(p, 10) || 0); | |
| const latestParts = normalize(latest).split(".").map((p) => parseInt(p, 10) || 0); | |
| for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { | |
| const c = currentParts[i] || 0; | |
| const l = latestParts[i] || 0; | |
| if (l > c) return true; | |
| if (l < c) return false; | |
| } | |
| return false; | |
| } |
🤖 Prompt for AI Agents
In `@src/components/VersionInfo.tsx` around lines 20 - 35, The compareVersions
function fails to handle version strings with a leading prefix like "v1.2.3";
normalize both inputs at the start of compareVersions by trimming whitespace and
stripping any leading non-numeric characters (e.g., remove a leading "v" or
other prefix via a regex) before splitting and parsing so the major/minor/patch
components parse correctly; update compareVersions to sanitize current and
latest (e.g., current = current.trim().replace(/^[^\d]*/, "") and same for
latest) then proceed with the existing split/parse/compare logic.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.