Skip to content

Commit 3b5ee73

Browse files
committed
feat: admin_remove_user RPC fully removes from auth.users (schema v17)
1 parent 5b19ab6 commit 3b5ee73

File tree

7 files changed

+253
-135
lines changed

7 files changed

+253
-135
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
All notable changes to BluePLM will be documented in this file.
44

5+
## [2.19.1] - 2025-12-31
6+
7+
### Changed
8+
- **Admin remove user fully deletes**: Removing a user from the organization now fully deletes them from `auth.users`, allowing clean re-invites without "user already registered" errors
9+
- **Schema version**: Bumped to v17
10+
11+
---
12+
13+
## [2.19.0] - 2025-12-31
14+
15+
### Fixed
16+
- **Multi-vault display bug on sign-in**: Fixed issue where the second vault would show the first vault's files until manually selecting each vault. Root cause was the working directory not updating when `activeVaultId` changed, and stale closure issues in the auto-connect flow
17+
- **Invited users can't see teams/roles**: Fixed critical bug where `handle_new_user` didn't include `org_id` in the `ON CONFLICT UPDATE` clause, so returning users with pending invites never had their org assigned
18+
19+
### Changed
20+
- **Schema version**: Bumped to v13
21+
22+
---
23+
524
## [2.18.3] - 2025-12-31
625

726
### Added

api/server.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,7 +1514,20 @@ export async function buildServer(): Promise<FastifyInstance> {
15141514
const orgCodeChunks = orgCodeBase64.match(/.{1,4}/g) || []
15151515
const orgCode = 'PDM-' + orgCodeChunks.join('-')
15161516

1517-
// Send invite email using Supabase Auth
1517+
// If user already has an auth account (e.g., was previously in org and removed),
1518+
// we can't send an invite email (Supabase doesn't allow it for existing users)
1519+
// Return the org code so admin can share it directly
1520+
if (existingAuthUser) {
1521+
return {
1522+
success: true,
1523+
message: `${normalizedEmail} already has an account. Share this org code with them to rejoin:`,
1524+
pending_member_id: pendingMemberId,
1525+
org_code: orgCode,
1526+
existing_user: true
1527+
}
1528+
}
1529+
1530+
// Send invite email using Supabase Auth (only for NEW users)
15181531
// Include org code in email data so it can be displayed in the email template
15191532
// Redirect to downloads page after confirmation
15201533
const { error: inviteError } = await adminClient.auth.admin.inviteUserByEmail(normalizedEmail, {
@@ -1531,9 +1544,7 @@ export async function buildServer(): Promise<FastifyInstance> {
15311544
fastify.log.warn({ email: normalizedEmail, error: inviteError }, 'Failed to send invite email, but pending member created')
15321545
return {
15331546
success: true,
1534-
message: resend
1535-
? `Failed to resend invite: ${inviteError.message}`
1536-
: `User added but invite email failed: ${inviteError.message}. They can still sign up manually.`,
1547+
message: `Invite created for ${normalizedEmail}. Email delivery failed but they can sign in manually.`,
15371548
pending_member_id: pendingMemberId
15381549
}
15391550
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "blue-plm",
3-
"version": "2.18.3",
3+
"version": "2.19.1",
44
"description": "Product Lifecycle Management for everyone who builds",
55
"main": "dist-electron/main.js",
66
"scripts": {

src/components/settings/TeamMembersSettings.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
Check,
1616
Search,
1717
Copy,
18-
Crown,
18+
Star,
1919
Lock,
2020
UserMinus,
2121
RefreshCw,
@@ -1626,7 +1626,7 @@ export function TeamMembersSettings() {
16261626
</span>
16271627
)}
16281628
{team.is_system && (
1629-
<Crown size={12} className="text-yellow-500" />
1629+
<Star size={12} className="text-yellow-500 fill-yellow-500" />
16301630
)}
16311631
</div>
16321632
<div className="text-xs text-plm-fg-muted flex items-center gap-3">
@@ -1717,8 +1717,8 @@ export function TeamMembersSettings() {
17171717
)}
17181718
{team.is_system && (
17191719
<span className="text-xs text-plm-fg-muted flex items-center gap-1 ml-auto">
1720-
<Crown size={12} className="text-yellow-500" />
1721-
System team (cannot be deleted)
1720+
<Star size={12} className="text-yellow-500 fill-yellow-500" />
1721+
Required
17221722
</span>
17231723
)}
17241724
</div>
@@ -5146,7 +5146,13 @@ function CreateUserDialog({
51465146
return
51475147
}
51485148

5149-
addToast('success', result.message || `Invite sent to ${email}`)
5149+
// If user already has an account, copy org code to clipboard
5150+
if (result.existing_user && result.org_code) {
5151+
await copyToClipboard(result.org_code)
5152+
addToast('success', `${result.message} (copied to clipboard)`)
5153+
} else {
5154+
addToast('success', result.message || `Invite sent to ${email}`)
5155+
}
51505156
onCreated()
51515157
onClose()
51525158
return

src/lib/schemaVersion.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { supabase } from './supabase'
2121

2222
// The schema version this app version expects
2323
// Increment this when releasing app updates that require schema changes
24-
export const EXPECTED_SCHEMA_VERSION = 12
24+
export const EXPECTED_SCHEMA_VERSION = 17
2525

2626
// Minimum schema version that will still work (for soft warnings vs hard errors)
2727
// Set this to allow some backwards compatibility
@@ -41,6 +41,11 @@ export const VERSION_DESCRIPTIONS: Record<number, string> = {
4141
10: 'join_org_by_slug creates user record if trigger hasn\'t fired (fixes org code race condition)',
4242
11: 'Case-insensitive email matching for pending_org_members (fixes invite flow with different email case)',
4343
12: 'Block user feature and regenerate org code (security features)',
44+
13: 'Fixed invite org assignment - handle_new_user includes org_id in UPDATE',
45+
14: 'Robust enum creation using pg_type check',
46+
15: 'Fixed workflow role assignment table name',
47+
16: 'Simplified default teams: Administrators (mandatory) + New Users (deletable)',
48+
17: 'admin_remove_user RPC fully removes user from org and auth.users',
4449
}
4550

4651
export interface SchemaVersionInfo {

src/lib/supabase.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2197,43 +2197,37 @@ export async function updateUserRole(
21972197
*/
21982198
export async function removeUserFromOrg(
21992199
targetUserId: string,
2200-
adminOrgId: string
2200+
_adminOrgId: string
22012201
): Promise<{ success: boolean; error?: string }> {
22022202
const client = getSupabaseClient()
22032203

2204-
// Verify target user is in same org
2204+
// Get target user's email for the RPC call
22052205
const { data: targetUser, error: fetchError } = await client
22062206
.from('users')
2207-
.select('id, org_id, email')
2207+
.select('email')
22082208
.eq('id', targetUserId)
22092209
.single()
22102210

22112211
if (fetchError || !targetUser) {
22122212
return { success: false, error: 'User not found' }
22132213
}
22142214

2215-
if (targetUser.org_id !== adminOrgId) {
2216-
return { success: false, error: 'User is not in your organization' }
2217-
}
2218-
2219-
// Delete any pending_org_members entries for this user's email in this org
2220-
// This allows the user to be re-invited later without constraint violations
2221-
await client
2222-
.from('pending_org_members')
2223-
.delete()
2224-
.eq('org_id', adminOrgId)
2225-
.ilike('email', targetUser.email)
2226-
2227-
// Remove from org by setting org_id to null
2228-
const { error } = await client
2229-
.from('users')
2230-
.update({ org_id: null, role: 'engineer' }) // Reset to default role
2231-
.eq('id', targetUserId)
2215+
// Call admin_remove_user RPC which fully removes the user from org AND auth.users
2216+
// This allows them to be cleanly re-invited later
2217+
const { data, error } = await client.rpc('admin_remove_user', {
2218+
p_user_email: targetUser.email
2219+
})
22322220

22332221
if (error) {
22342222
return { success: false, error: error.message }
22352223
}
22362224

2225+
const result = data as { success: boolean; error?: string; message?: string }
2226+
2227+
if (!result.success) {
2228+
return { success: false, error: result.error || 'Failed to remove user' }
2229+
}
2230+
22372231
return { success: true }
22382232
}
22392233

0 commit comments

Comments
 (0)