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/auth/logout
and LastLogin will clear the session and redirectX-LastLogin-Email
header for authenticated requestsAuthentication Flow:
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:
/backend/routes/views/dashboard.tsx
import dashboardRoute from "./backend/routes/views/dashboard.tsx";
app.get("/", dashboardRoute)
/routes/views/
because it's a user-facing view, not an API endpointThis approach:
main.tsx
focused on app setup and middleware/routes/views/
for better organizationThe dashboard route should:
c.get("userEmail")
)/api/health
endpoint results as raw JSON in a tag; e.g., JSON.stringify(data, null, 2)
/auth/logout
Authentication Middleware:
const email = c.req.header("X-LastLogin-Email")
/auth/logout
- LastLogin does this automaticallyLogin Page Requirements:
import { LoginWithGoogleButton } from "https://esm.town/v/stevekrouse/LoginWithGoogleButton"
<LoginWithGoogleButton />
React Component, optionally supplying the text
attributeFor authenticated users:
c.set('userEmail', email)
<a href="/auth/logout">Logout</a>
Public Routes Whitelist:
PUBLIC_ROUTES
array at the top for routes that should be accessible without authentication/auth/logout
in the whitelist (LastLogin handles it before your middleware runs)/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:
/auth/logout
route in your Hono app/auth/logout
- LastLogin handles it automatically/auth/logout
at the wrapper level before it reaches your middlewarePublic Routes:
/auth/logout
doesn't need to be whitelisted (LastLogin handles it before your middleware runs)Authentication Headers:
X-LastLogin-Email
headerError Handling:
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:
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