fix: handle legacy JSON array permissions and improve admin toggle UX
- Add migration to convert legacy JSON array permissions to decimal strings - Add runtime fallback in stringToPermissions for legacy format - Show all permission toggles as enabled (dimmed) when Administrator is on
This commit is contained in:
@@ -154,6 +154,9 @@ export function runMigrations(db: Database.Database): void {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// ─── Legacy permissions: convert JSON arrays to decimal strings ───────────
|
||||||
|
migrateLegacyPermissions(db);
|
||||||
|
|
||||||
// ─── RBAC Migration: Ensure @everyone roles exist for all spaces ─────────
|
// ─── RBAC Migration: Ensure @everyone roles exist for all spaces ─────────
|
||||||
migrateEveryoneRoles(db);
|
migrateEveryoneRoles(db);
|
||||||
|
|
||||||
@@ -175,6 +178,36 @@ export function runMigrations(db: Database.Database): void {
|
|||||||
console.log('Migrations complete.');
|
console.log('Migrations complete.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Convert legacy JSON array permissions (e.g. '["VIEW_CHANNEL"]') to decimal strings */
|
||||||
|
function migrateLegacyPermissions(db: Database.Database): void {
|
||||||
|
const roles = db.prepare('SELECT id, permissions FROM roles WHERE permissions IS NOT NULL').all() as { id: string; permissions: string }[];
|
||||||
|
const update = db.prepare('UPDATE roles SET permissions = ? WHERE id = ?');
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
// Skip if already a valid decimal string
|
||||||
|
try { BigInt(role.permissions); continue; } catch {}
|
||||||
|
|
||||||
|
// Try legacy JSON array
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(role.permissions);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
let result = 0n;
|
||||||
|
for (const key of parsed) {
|
||||||
|
const bit = PermissionBits[key as keyof typeof PermissionBits];
|
||||||
|
if (bit !== undefined) result |= bit;
|
||||||
|
}
|
||||||
|
update.run(result.toString(), role.id);
|
||||||
|
console.log(`Migrating: Converted legacy permissions for role ${role.id}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch { /* not JSON either */ }
|
||||||
|
|
||||||
|
// Unrecognized format — set to 0
|
||||||
|
update.run('0', role.id);
|
||||||
|
console.log(`Migrating: Reset unrecognized permissions for role ${role.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Ensure the single-row instance_settings row exists */
|
/** Ensure the single-row instance_settings row exists */
|
||||||
function migrateInstanceSettings(db: Database.Database): void {
|
function migrateInstanceSettings(db: Database.Database): void {
|
||||||
const row = db.prepare('SELECT id FROM instance_settings WHERE id = 1').get();
|
const row = db.prepare('SELECT id FROM instance_settings WHERE id = 1').get();
|
||||||
|
|||||||
@@ -63,6 +63,18 @@ export function stringToPermissions(str: string | undefined | null): bigint {
|
|||||||
try {
|
try {
|
||||||
return BigInt(str);
|
return BigInt(str);
|
||||||
} catch {
|
} catch {
|
||||||
|
// Fallback: legacy JSON array format (e.g. '["VIEW_CHANNEL","SEND_MESSAGES"]')
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(str);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
let result = 0n;
|
||||||
|
for (const key of parsed) {
|
||||||
|
const bit = PermissionBits[key as keyof typeof PermissionBits];
|
||||||
|
if (bit !== undefined) result |= bit;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch { /* not JSON either */ }
|
||||||
return 0n;
|
return 0n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,24 +330,28 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) {
|
|||||||
<div className="rounded-lg bg-white/[0.02] p-3.5">
|
<div className="rounded-lg bg-white/[0.02] p-3.5">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{group.perms.map((perm) => {
|
{group.perms.map((perm) => {
|
||||||
const isOn = (draftPermissions & perm.bit) !== 0n;
|
const isAdminBit = perm.bit === PermissionBits.ADMINISTRATOR;
|
||||||
const isAdmin = perm.bit === PermissionBits.ADMINISTRATOR;
|
const hasAdmin = (draftPermissions & PermissionBits.ADMINISTRATOR) !== 0n;
|
||||||
|
const isOn = isAdminBit ? hasAdmin : hasAdmin || (draftPermissions & perm.bit) !== 0n;
|
||||||
|
const isInherited = !isAdminBit && hasAdmin;
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={perm.label}
|
key={perm.label}
|
||||||
className="flex items-center justify-between py-1.5 px-2 rounded hover:bg-interactive-hover cursor-pointer group/perm"
|
className={`flex items-center justify-between py-1.5 px-2 rounded cursor-pointer group/perm ${
|
||||||
|
isInherited ? 'opacity-50 cursor-default' : 'hover:bg-interactive-hover'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<span className={`text-sm ${isAdmin ? 'text-txt-danger font-medium' : 'text-txt-primary'}`}>
|
<span className={`text-sm ${isAdminBit ? 'text-txt-danger font-medium' : 'text-txt-primary'}`}>
|
||||||
{perm.label}
|
{perm.label}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
togglePermission(perm.bit);
|
if (!isInherited) togglePermission(perm.bit);
|
||||||
}}
|
}}
|
||||||
className={`relative w-9 h-5 rounded-full transition-colors cursor-pointer ${
|
className={`relative w-9 h-5 rounded-full transition-colors ${
|
||||||
isOn ? 'bg-accent-primary' : 'bg-interactive-muted'
|
isInherited ? 'cursor-default' : 'cursor-pointer'
|
||||||
}`}
|
} ${isOn ? 'bg-accent-primary' : 'bg-interactive-muted'}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow-sm transition-transform ${
|
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow-sm transition-transform ${
|
||||||
|
|||||||
Reference in New Issue
Block a user