177 lines
5.1 KiB
Markdown
177 lines
5.1 KiB
Markdown
|
# 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
|
|||
|

|
|||
|
```
|
|||
|
|
|||
|
---
|
|||
|
|
|||
|
## 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`
|
|||
|
|
|||
|
---
|