@auth_required and @login_only, roles, the scaffolded
admin panel, GCS file storage, and WeasyPrint for PDF export.
Credentials you need first
Google OAuth setup
Google OAuth setup
- Open APIs and Credentials.
- Create an OAuth 2.0 Client ID of type Web application.
- Add
http://localhost:5173/auth/google/callbackas an authorized redirect URI. - Copy the client ID and secret.
GCS setup
GCS setup
- Create a bucket in Cloud Storage.
- Create a service account in IAM.
- Grant it the Storage Object Admin role.
- Create a JSON key and copy the entire contents.
Create the project
This part needs a different app type, so start fresh..env
Step 0: Clear out the demo files
tests/test_home.py goes with the route it tests. Leave it behind and feather test fails
on a 404 the moment you delete routes/pages/home.py.
Leave models/__init__.py alone for now. The Log, Account and AccountUser imports
already in it have to stay.
Step 1: The models
A board now belongs to a user, and columns belong to a board.models/kanban.py
models/column.py
models/card.py
models/__init__.py
Step 2: The migration
Step 3: The services
A word about roles before the code. Feather ships four of them,admin, editor,
moderator and user, and those are the only ones the scaffolded admin panel can assign
from its dropdown. So this tutorial uses them as they are: editors and admins can change
boards, everyone else gets a read-only view. New users arrive as user, and you promote
someone from /admin/users.
Adding a role of your own
Adding a role of your own
Add it to
ROLE_INHERITS in feather/auth/roles.py, then to the valid_roles list in
services/admin_service.py, and to the <select> in
templates/pages/admin/user_detail.html so the panel can assign it. The role column is
a plain string, so no migration is needed.services/kanban_service.py
services/column_service.py
services/card_service.py
services/__init__.py
card.column.kanban.user_id, and that chain is what makes the
authorization hold.
Step 4: The routes
routes/pages/dashboard.py
Why the pending page uses
@login_only. @auth_required blocks a suspended user
with a 403, which would make the pending page unreachable by exactly the people who need
it. @login_only checks only that there’s an authenticated session, without looking at
active or role.routes/pages/board.py
The API routes
Move, export and attachments all live here.routes/api/board.py
get_storage() returns whichever backend
STORAGE_BACKEND names, so the same code works against the local filesystem if you switch
it.
Steps 5 and 6: Templates and CSS
This part adds seven templates and a sizeable stylesheet: login, pending, the dashboard grid, the board, and partials for the board card, the column, the card, the PDF viewer modal and the upload modal. They’re mostly presentation, and reproducing four hundred lines of CSS here wouldn’t teach you anything the earlier parts didn’t. Copy them from Steps 5 and 6 of the source tutorial. The one pattern worth calling out is how the templates gate on role:Step 7: The island
Unchanged from part 3, other than living in a new project.static/islands/kanban-board.js
Step 8: The export button
The route already exists, so this is a link in the board header.templates/pages/board.html
Steps 9 and 10: OAuth and testing
Confirm your Google credentials listhttp://localhost:5173/auth/google/callback as a
redirect URI, then:
- Sign in with Google.
- Create a board from the dashboard.
- Add columns and cards.
- Go back and create a second board.
- Open
/admin/as an admin. - Attach a PDF to a card, then open it in the viewer.
- Export the board.
/admin/, an
editor can create and edit, and a plain user sees the boards with no create, edit
or delete controls.
Prompt Claude
Run this in two passes. The first gets you a working app behind sign-in, the second adds files and export.- Pass 1: auth and boards
- Pass 2: attachments and export
Checkpoint
- Google sign-in works, and a new account lands on the pending page until approved
- The dashboard shows your boards in a grid
- A board opens with columns, cards and working drag-and-drop
/admin/is reachable as an admin and refused otherwise- A PDF attaches to a card and opens in the viewer modal
- Exporting a board produces a PDF
- A plain
usersees no create or delete controls feather checkpasses
What you learned
- Google OAuth end to end, including the approval gate
@auth_requiredversus@login_only, and why the pending page needs the second- Reading
current_userand its role - Checking ownership through relationships rather than request parameters
- The scaffolded admin panel, and who can reach it
- File storage through
get_storage()rather than a cloud SDK - PDF generation with WeasyPrint
- Gating UI controls on role so they match what the services allow
Next: turn it into a SaaS
Tenant isolation, platform admin, and queries that can’t leak across organizations.