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
- Dynamic: Each guild has different privacy rules
- Centralized: Privacy logic lives in one place (AbilityFactory)
- Type-safe: Settings are validated with DTOs
- Automatic: Frontend abilities update when settings change
- Backend-enforced: Even if frontend is bypassed, backend enforces rules via CASL guard
Adding More Privacy Settings
To add new privacy settings:
- Add field to DTO:
update-guild-settings.dto.ts - Add logic to AbilityFactory: Define when the setting allows/denies access
- 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;