Good rundown, especially the RBAC role-explosion section. One thing worth adding to the "return one identical message" habit: matching the error text is necessary but not sufficient if the two code paths take different amounts of time. Looking up a user and then running bcrypt costs real milliseconds, so a login endpoint that only calls bcrypt when the email exists leaks that difference through response timing even with identical error strings. The usual fix is to always run a bcrypt compare against a fixed dummy hash when the email isn't found, so both paths cost roughly the same and timing can't be used to enumerate valid emails.