diff --git a/tutorials/05-auth-roles-ui.md b/tutorials/05-auth-roles-ui.md new file mode 100644 index 0000000..03cede0 --- /dev/null +++ b/tutorials/05-auth-roles-ui.md @@ -0,0 +1,176 @@ +# 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 You’ll 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): + +```python +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) + +```python +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: + +```markdown +![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` + +---