Read through all of these instructions in order to understand the architecture of this Val, and implementation details important to this Val, and then once you have it all in your memory you should get to work.
If anything is unclear, you should stop and ask me for clarification before proceeding. Do not write code if you are unsure of what is expected.
Important: there are checkpoints in these instructions where you are directed to stop and ask permission before proceeding. Follow those instructions and do not proceed without permission.
Important: LastLogin automatically handles certain routes and authentication flows:
/auth/logout
- LastLogin intercepts this route and handles logout automatically- No custom logout route needed in your Hono app
- Simply link to
/auth/logout
and LastLogin will clear the session and redirect - Authentication headers - LastLogin adds
X-LastLogin-Email
header for authenticated requests
Authentication Flow:
- Unauthenticated requests → LastLogin checks for session
- No session → Returns 401 or redirects to login
- Valid session → Adds
X-LastLogin-Email
header and passes to your app /auth/logout
requests → LastLogin clears session automatically (never reaches your app)
Add global authentication middleware to main.tsx
:
import { lastlogin } from "https://esm.town/v/stevekrouse/lastlogin_safe";
// Import route modules
import authCheck from "./backend/routes/authCheck.ts";
// other route modules
const app = new Hono();
// Apply global authentication middleware to ALL routes
app.use("*", authCheck);
// other routes
// Export with LastLogin wrapper
async function handler(request: Request): Promise<Response> {
return app.fetch(request);
}
// Wrap and export the main HTTP handler with lastlogin
export default lastlogin(handler);
Best Practice: Separate the root dashboard route from main.tsx
Instead of defining the root route directly in main.tsx
, create a dedicated dashboard route file:
- Create:
/backend/routes/views/dashboard.tsx
- Purpose: Handle the authenticated user's main dashboard/landing page
import dashboardRoute from "./backend/routes/views/dashboard.tsx";
- Mount in main.tsx:
app.get("/", dashboardRoute)
- Note: Dashboard is placed in
/routes/views/
because it's a user-facing view, not an API endpoint
This approach:
- Keeps
main.tsx
focused on app setup and middleware - Makes the dashboard logic easier to maintain and test
- Follows the modular architecture pattern with views separated from API routes
- Places user-facing interfaces in
/routes/views/
for better organization - Allows for easier dashboard enhancements without touching core routing
The dashboard route should:
- Display user information (email from
c.get("userEmail")
) - Display the
/api/health
endpoint results as raw JSON in atag; e.g., JSON.stringify(data, null, 2)
- Include logout functionality via
/auth/logout
- Show a simple and professional welcome interface with minimal styling
Authentication Middleware:
- Retrieve the user's email:
const email = c.req.header("X-LastLogin-Email")
- If email exists, the user is authenticated - store in context and continue to your app
- If no email, show login page with LoginWithGoogleButton
- Note: You don't need to handle
/auth/logout
- LastLogin does this automatically
Login Page Requirements:
import { LoginWithGoogleButton } from "https://esm.town/v/stevekrouse/LoginWithGoogleButton"
- Use the
<LoginWithGoogleButton />
React Component, optionally supplying thetext
attribute - Add "via LastLogin" underneath LoginWithGoogleButton, centered, secondary text, linking to https://lastlogin.io/
- Pass the email from the server to the client-side code if using React hydration or similar techniques
For authenticated users:
- Store email in context:
c.set('userEmail', email)
- In your authenticated views, add logout functionality:
<a href="/auth/logout">Logout</a>
Public Routes Whitelist:
- Add a
PUBLIC_ROUTES
array at the top for routes that should be accessible without authentication - Don't include
/auth/logout
in the whitelist (LastLogin handles it before your middleware runs) - Use for truly public routes like:
/demo/:id
,/api/health
, static assets, etc.
If you encounter problems loading the React button, try this simple approach for authCheck.ts:
// Public routes that don't require authentication
const PUBLIC_ROUTES = [
// Add public routes here as needed
// '/demo/:id', // Example: public demo viewing
// '/api/public', // Example: public API endpoints
];
// Helper function to check if a route is public
function isPublicRoute(path: string): boolean {
return PUBLIC_ROUTES.some((route) => {
// Handle exact matches
if (route === path) return true;
// Handle wildcard patterns (basic implementation)
if (route.includes("*")) {
const pattern = route.replace("*", ".*");
return new RegExp(`^${pattern}$`).test(path);
}
// Handle route parameters (basic implementation)
if (route.includes(":")) {
const pattern = route.replace(/:[^/]+/g, "[^/]+");
return new RegExp(`^${pattern}$`).test(path);
}
return false;
});
}
export default async (c, next) => {
// Allow public routes to bypass authentication
if (isPublicRoute(c.req.path)) {
await next();
return;
}
const email = c.req.header("X-LastLogin-Email");
if (!email) {
// Show login page if not authenticated
return c.html(`
<!DOCTYPE html>
<html>
<head>
<title>Your App - Login Required</title>
</head>
<body>
<div style="border: 1px solid #ccc; padding: 20px 40px; float: left;">
<h1>Sign in</h1>
<p>Sign in to access your app.</p>
<div id="login-button-container">
<p>Loading login button...</p>
</div>
<p style="text-align: center; margin-top: 20px;">
<a href="https://lastlogin.io/" target="_blank" style="color: #888; font-size: 14px;">via LastLogin</a>
</p>
</div>
<script type="module">
import { LoginWithGoogleButton } from "https://esm.town/v/stevekrouse/LoginWithGoogleButton";
import React from "https://esm.sh/react@18.2.0?deps=react@18.2.0";
import { createRoot } from "https://esm.sh/react-dom@18.2.0/client?deps=react@18.2.0,react-dom@18.2.0";
const loginContainer = document.getElementById('login-button-container');
if (loginContainer) {
const root = createRoot(loginContainer);
root.render(React.createElement(LoginWithGoogleButton, { text: "Sign in with Google" }));
}
</script>
</body>
</html>
`);
}
// If authenticated, store email in context for use by routes
c.set("userEmail", email);
// Continue to the next middleware/route
await next();
};
Logout Route:
- ❌ Don't create a custom
/auth/logout
route in your Hono app - ✅ Just link to
/auth/logout
- LastLogin handles it automatically - The logout link will work for authenticated users without any custom code
- LastLogin intercepts
/auth/logout
at the wrapper level before it reaches your middleware
Public Routes:
- Use the whitelist in authCheck.ts for routes that should be accessible without authentication
/auth/logout
doesn't need to be whitelisted (LastLogin handles it before your middleware runs)- Examples of routes that should be whitelisted: public demos, health checks, static assets
Authentication Headers:
- Authenticated requests will have
X-LastLogin-Email
header - Unauthenticated requests will not have this header
- LastLogin wrapper handles session validation before your code runs
Error Handling:
- If LastLogin can't validate a session, it returns 401 before reaching your app
- Your authCheck middleware only runs for requests that pass LastLogin's validation
- No need to handle session expiration - LastLogin manages this
Townie, stop here! Before proceeding to additional steps, confirm that this step is working correctly.
If everything is working as intended: conclude this step with these messages:
- Briefly summarize the work that was just completed and why it's important
- Provide the URL to the dashboard endpoint so the user can see that the dashbaord is live. Tell the user that they can copy and paste that URL into a new tab to see it live.
- Tell the user that they can log into the dashboard using their Google email address.
Tell the user that the next step is to add webhook authentication to this Val. Explain what webhooks are why it's important to authenticate them.
Lastly, tell the user to copy this line and paste it into Townie:
Add webhook auth to this Val with what you see in
/_townie/03-webhooks.md