Skip to main content

CASL Dynamic Privacy Settings Example

Overview

CASL now supports guild-specific privacy settings that are enforced dynamically. Each guild can configure its own privacy rules, and CASL enforces them automatically on both backend and frontend.

How It Works

Backend: Guild Settings in Database

Guild privacy is stored in the Guild.settings JSON field:

{
"rosterPrivacy": "members", // "public" | "members" | "officers"
"showCharacterDetails": true,
"allowApplications": false
}

Backend: Ability Factory Reads Settings

When creating abilities, the AbilityFactory reads the guild's settings and applies them:

// Officers can always see roster
if (wowRank !== null && wowRank <= 2) {
can(Action.Read, 'Membership', { guildId: membership.guildId });
}
// Regular members - depends on guild privacy setting
else {
const rosterPrivacy = guildSettings.rosterPrivacy || 'members';
if (rosterPrivacy === 'members' || rosterPrivacy === 'public') {
can(Action.Read, 'Membership', { guildId: membership.guildId });
} else {
// Officers-only roster
cannot(Action.Read, 'Membership', { guildId: membership.guildId });
}
}

Frontend: Abilities Automatically Enforced

The frontend receives packed ability rules and uses them:

// This button only shows if user can read membership based on guild settings
<Can I={Action.Read} a="Membership">
<Button onClick={showRoster}>View Guild Roster</Button>
</Can>;

// Or programmatically
const ability = useContext(AbilityContext);
const canSeeRoster = ability.can(Action.Read, 'Membership');

Privacy Levels

1. Public Roster (rosterPrivacy: "public")

  • Anyone (including guests) can view the guild roster
  • Good for recruiting guilds

2. Members-Only Roster (rosterPrivacy: "members")

  • Default setting
  • Only guild members can view the roster
  • Officers and GM can always see it

3. Officers-Only Roster (rosterPrivacy: "officers")

  • Only officers (rank 0-2) and GM can view roster
  • Regular members cannot see full roster
  • Most restrictive

API Endpoints

Update Guild Settings (Guild Master or Officers with canManageGuild)

PATCH /api/v1/guilds/:guildId/settings
Authorization: Bearer <token>
Content-Type: application/json

{
"rosterPrivacy": "officers",
"showCharacterDetails": false,
"allowApplications": true
}

Get Guild (includes settings in response)

GET /api/v1/guilds/:guildId
Authorization: Bearer <token>

# Response includes:
{
"id": "...",
"name": "...",
"settings": {
"rosterPrivacy": "officers",
...
},
"userPermissions": {
"abilityRules": [...] // Rules respect guild settings
}
}

Frontend Example: Settings Page

function GuildSettingsPage({ guild }) {
const [settings, setSettings] = useState({
rosterPrivacy: guild.settings?.rosterPrivacy || 'members',
});

const updateSettingsMutation = useMutation({
mutationFn: (newSettings) =>
api.patch(`/api/v1/guilds/${guild.id}/settings`, newSettings),
});

return (
<Can I={Action.Update} a="Guild">
<Card>
<CardHeader>Privacy Settings</CardHeader>
<CardBody>
<Select
label="Roster Privacy"
value={settings.rosterPrivacy}
onChange={(e) => {
const newSettings = { rosterPrivacy: e.target.value };
setSettings(newSettings);
updateSettingsMutation.mutate(newSettings);
}}
>
<option value="public">Public (Anyone)</option>
<option value="members">Members Only</option>
<option value="officers">Officers Only</option>
</Select>
</CardBody>
</Card>
</Can>
);
}

Benefits

  1. Dynamic: Each guild has different privacy rules
  2. Centralized: Privacy logic lives in one place (AbilityFactory)
  3. Type-safe: Settings are validated with DTOs
  4. Automatic: Frontend abilities update when settings change
  5. Backend-enforced: Even if frontend is bypassed, backend enforces rules via CASL guard

Adding More Privacy Settings

To add new privacy settings:

  1. Add field to DTO: update-guild-settings.dto.ts
  2. Add logic to AbilityFactory: Define when the setting allows/denies access
  3. Update frontend: Use <Can> components to conditionally show/hide features

Example: Hide event details from non-members

// In AbilityFactory
if (guildSettings.eventsPrivacy === 'members-only' && !membership) {
cannot(Action.Read, 'Event', { guildId: guild.id });
}

// In frontend
<Can I={Action.Read} a="Event">
<EventsList events={guild.events} />
</Can>

Migration Strategy

If you want to set default privacy for existing guilds:

UPDATE guilds
SET settings = jsonb_set(
COALESCE(settings, '{}'::jsonb),
'{rosterPrivacy}',
'"members"'
)
WHERE settings->>'rosterPrivacy' IS NULL;