fix(auth): request moderator OAuth scopes only on opt-in
Stop asking for mod scopes at normal login so existing core-only tokens never hit a missing-permissions dialog. Gate moderated-channels Helix, offer an Enable flow for mods/broadcasters, and upgrade atomically. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2d65aeec29
commit
cc772a7c90
15 changed files with 690 additions and 128 deletions
|
|
@ -1,4 +1,7 @@
|
|||
/// OAuth scopes Frosty requests for a logged-in Twitch user session.
|
||||
/// Core OAuth scopes Frosty requests on a normal Twitch login.
|
||||
///
|
||||
/// Matches the pre-moderator-tools set so existing tokens remain valid without
|
||||
/// a re-consent nag for non-moderators.
|
||||
const twitchUserScopes = <String>[
|
||||
'chat:read',
|
||||
'chat:edit',
|
||||
|
|
@ -6,15 +9,28 @@ const twitchUserScopes = <String>[
|
|||
'user:read:blocked_users',
|
||||
'user:manage:blocked_users',
|
||||
'user:manage:chat_color',
|
||||
];
|
||||
|
||||
/// Moderator-only scopes requested via an explicit opt-in upgrade.
|
||||
const twitchModeratorScopes = <String>[
|
||||
'user:read:moderated_channels',
|
||||
'moderator:manage:chat_messages',
|
||||
'moderator:manage:banned_users',
|
||||
];
|
||||
|
||||
/// Space-separated scope string for Twitch's authorize URL.
|
||||
/// Core + moderator scopes for an opted-in moderator session.
|
||||
const twitchAllUserScopes = <String>[
|
||||
...twitchUserScopes,
|
||||
...twitchModeratorScopes,
|
||||
];
|
||||
|
||||
/// Space-separated scope string for Twitch's authorize URL (core only).
|
||||
String get twitchUserScopeQuery => twitchUserScopes.join(' ');
|
||||
|
||||
/// Human-readable labels for [twitchUserScopes].
|
||||
/// Space-separated scope string for Twitch's authorize URL (core + mod).
|
||||
String get twitchAllUserScopeQuery => twitchAllUserScopes.join(' ');
|
||||
|
||||
/// Human-readable labels for Frosty-requested scopes.
|
||||
const twitchScopeLabels = <String, String>{
|
||||
'chat:read': 'Read chat messages',
|
||||
'chat:edit': 'Send chat messages',
|
||||
|
|
@ -27,6 +43,13 @@ const twitchScopeLabels = <String, String>{
|
|||
'moderator:manage:banned_users': 'Timeout and ban users as a moderator',
|
||||
};
|
||||
|
||||
/// Whether [granted] includes every moderator scope Frosty needs.
|
||||
bool hasModeratorScopes(Iterable<String> granted) =>
|
||||
missingTwitchScopes(
|
||||
granted: granted,
|
||||
required: twitchModeratorScopes,
|
||||
).isEmpty;
|
||||
|
||||
/// Scopes in [required] that are not present in [granted].
|
||||
List<String> missingTwitchScopes({
|
||||
required Iterable<String> granted,
|
||||
|
|
@ -36,7 +59,15 @@ List<String> missingTwitchScopes({
|
|||
return required.where((scope) => !grantedSet.contains(scope)).toList();
|
||||
}
|
||||
|
||||
/// Dialog body for a Twitch Helix/OAuth 401.
|
||||
String _scopeBullets(Iterable<String> scopes) => scopes
|
||||
.map((scope) => '• ${twitchScopeLabels[scope] ?? scope}')
|
||||
.join('\n');
|
||||
|
||||
/// Dialog body for a Twitch Helix/OAuth 401 on a core (non-mod) request.
|
||||
///
|
||||
/// Missing-permissions copy only lists **core** scopes. A token that already
|
||||
/// has core scopes is treated as expired/invalid rather than "missing mod
|
||||
/// permissions" so non-mods are never nagged about moderator consent.
|
||||
String unauthorizedDialogMessage({
|
||||
required bool isLoggedIn,
|
||||
required Iterable<String> grantedScopes,
|
||||
|
|
@ -47,14 +78,17 @@ String unauthorizedDialogMessage({
|
|||
|
||||
final missing = missingTwitchScopes(granted: grantedScopes);
|
||||
if (missing.isEmpty) {
|
||||
return 'Your session is missing permissions. Please log in again to continue.';
|
||||
return 'Your session has expired. Please log in again to continue.';
|
||||
}
|
||||
|
||||
final bullets = missing
|
||||
.map((scope) => '• ${twitchScopeLabels[scope] ?? scope}')
|
||||
.join('\n');
|
||||
|
||||
return 'Frosty needs additional Twitch permissions:\n\n'
|
||||
'$bullets\n\n'
|
||||
'${_scopeBullets(missing)}\n\n'
|
||||
'Log in again to grant them.';
|
||||
}
|
||||
|
||||
/// Copy for the explicit moderator-tools opt-in dialog.
|
||||
String enableModeratorToolsDialogMessage() {
|
||||
return 'Enable moderator tools to manage chat in channels you moderate:\n\n'
|
||||
'${_scopeBullets(twitchModeratorScopes)}\n\n'
|
||||
'You will be asked to authorize these permissions with Twitch.';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue