FluidFrameDev/tutorials/05-auth-roles-ui.md

5.1 KiB
Raw Blame History

05 — Auth: Users Service + Role-Gated UI

Goal:

  • Require login for certain actions.
  • Show/hide UI components based on user role.
  • Gate sensitive server functions so only specific roles can run them.

1) What Youll Build

  • A login/logout button set.
  • UI elements that only appear for specific roles (editor, admin).
  • A server callable (dangerous_action) that only certain roles can call.
  • Friendly messages when a user tries to overstep their privileges.

2) Enable the Users Service

In your Anvil app:

  1. App → Settings → Users Service → Enable.
  2. Add a few test users in the Users table:
    • Columns: email, password, roles (Text, Comma-separated, e.g., editor, admin).
  3. Assign roles for each user. Example:
    • alice@example.com → roles: editor
    • bob@example.com → roles: viewer
    • root@example.com → roles: admin

3) Server-Side Role Helper

Add to ServerModule1 (or create auth_helpers.py if you prefer):

import anvil.server
import anvil.users

def require_role(*roles):
    """
    Ensure the logged-in user has at least one of the given roles.
    """
    user = anvil.users.get_user()
    if not user:
        raise PermissionError("You must be logged in.")
    user_roles = set((user.get('roles') or "").replace(" ", "").split(","))
    if not set(roles).intersection(user_roles):
        raise PermissionError(f"You need one of these roles: {', '.join(roles)}")

@anvil.server.callable
def dangerous_action():
    """
    Only 'editor' or 'admin' can run this.
    """
    require_role('editor', 'admin')
    return "Sensitive operation completed successfully."

4) Client UI Setup

On Form1:

  • Add two Buttons:

    • button_login → text: “Login”
    • button_logout → text: “Logout”
  • Add a Label: label_user_info (for current user/roles).

  • Add a Button: button_danger → text: “Run Dangerous Action” (editor/admin only).

  • Arrange them neatly in a ColumnPanel or RowPanel.


5) Client Code (Form1)

from anvil import *
import anvil.server
import anvil.users

class Form1(Form1Template):

    def __init__(self, **properties):
        self.init_components(**properties)
        self.refresh_ui()

    def refresh_ui(self):
        """Refreshes UI based on login status and roles."""
        user = anvil.users.get_user()
        if user:
            roles = (user.get('roles') or "").replace(" ", "")
            self.label_user_info.text = f"Logged in as: {user['email']} ({roles})"
            self.button_login.visible = False
            self.button_logout.visible = True
            # Show dangerous button only if editor/admin
            role_list = roles.split(",")
            self.button_danger.visible = any(r in ('editor', 'admin') for r in role_list)
        else:
            self.label_user_info.text = "Not logged in"
            self.button_login.visible = True
            self.button_logout.visible = False
            self.button_danger.visible = False

    def button_login_click(self, **event_args):
        """Log the user in."""
        anvil.users.login_with_form()
        self.refresh_ui()

    def button_logout_click(self, **event_args):
        """Log the user out."""
        anvil.users.logout()
        self.refresh_ui()

    def button_danger_click(self, **event_args):
        """Attempt to run the dangerous action."""
        try:
            res = anvil.server.call('dangerous_action')
            Notification(res, style='success', timeout=3).show()
        except PermissionError as e:
            Notification(str(e), style='danger', timeout=3).show()
        except Exception as e:
            Notification(f"Error: {e}", style='danger').show()

6) Test Scenarios

  1. Not logged in → No dangerous button visible; clicking login prompts login form.
  2. Logged in as viewer → Sees dangerous button hidden.
  3. Logged in as editor/admin → Sees dangerous button; clicking it runs dangerous_action and shows success message.
  4. Force test: Temporarily show dangerous button for all roles, click it as a viewer, verify server rejects with PermissionError.

7) Bonus: Role Badge

  • Add a Label next to label_user_info styled with a background color based on role.
  • Example: editor → blue badge, admin → red badge.

8) Demo Asset

Record a short GIF showing:

  1. Logging in as viewer → no dangerous button.
  2. Logging in as editor → dangerous button appears and works.
  3. Logging in as admin → same, but maybe with admin-only UI section.

Save as: /assets/auth-roles-demo.gif

Reference in this tutorial:

![Auth Roles Demo](../assets/auth-roles-demo.gif)

9) Takeaways

  • Roles allow fine-grained control without changing client code.
  • Always check roles server-side — the client can be modified by anyone.
  • Hide UI for UX; enforce on server for security.

Labels & Project

When creating the issue for this tutorial:

  • Labels: type: tutorial, priority: P2
  • Project: Roadmap → Backlog
  • Milestone: v0.3 Auth + Polish