diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..b4214c8
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,6 @@
+# Auth0 SPA configuration.
+# These are PUBLIC, non-secret identifiers (they ship to the browser in the
+# Authorization Code + PKCE flow). Copy this file to `.env` for local dev, and
+# set the same variables in your Cloudflare build environment for deploys.
+PUBLIC_AUTH0_DOMAIN=id.letsbuilda.dev
+PUBLIC_AUTH0_CLIENT_ID=THoRnVJxMOfLxMAW0kaNW8nuP1q8q3qQ
diff --git a/.gitignore b/.gitignore
index 16d54bb..28f22ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
# build output
dist/
+.wrangler/
# generated types
.astro/
+worker-configuration.d.ts
# dependencies
node_modules/
diff --git a/README.md b/README.md
index 87b813a..5991171 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,26 @@ All commands are run from the root of the project, from a terminal:
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
+## ๐ Auth0 configuration
+
+Authentication uses the [`@auth0/auth0-spa-js`](https://github.com/auth0/auth0-spa-js)
+SDK (Authorization Code + PKCE). Configuration comes from two **public,
+non-secret** environment variables:
+
+| Variable | Description |
+| :----------------------- | :--------------------------- |
+| `PUBLIC_AUTH0_DOMAIN` | Auth0 tenant domain |
+| `PUBLIC_AUTH0_CLIENT_ID` | Auth0 SPA application client ID |
+
+Copy `.env.example` to `.env` for local development, and set the same variables
+in the Cloudflare build environment for deploys (Astro inlines `PUBLIC_*` values
+at build time).
+
+In the Auth0 application (type: **Single Page Application**), add both your local
+(`http://localhost:4321`) and production (`https://letsbuilda.dev`) origins to
+**Allowed Callback URLs**, **Allowed Logout URLs**, and **Allowed Web Origins**,
+and enable **Refresh Token Rotation**.
+
## ๐ Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/package-lock.json b/package-lock.json
index 480fd01..522c7ed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
},
"devDependencies": {
"@biomejs/biome": "2.4.15",
+ "@types/node": "^24.13.2",
"wrangler": "^4.95.0"
},
"engines": {
@@ -2296,6 +2297,16 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/node": {
+ "version": "24.13.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
+ "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
"node_modules/@types/react": {
"version": "19.2.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
@@ -5443,6 +5454,13 @@
"node": ">=20.18.1"
}
},
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/unenv": {
"version": "2.0.0-rc.24",
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
diff --git a/package.json b/package.json
index f47b461..41e7bdf 100644
--- a/package.json
+++ b/package.json
@@ -21,8 +21,9 @@
"react-dom": "^19.2.6"
},
"devDependencies": {
- "wrangler": "^4.95.0",
- "@biomejs/biome": "2.4.15"
+ "@biomejs/biome": "2.4.15",
+ "@types/node": "^24.13.2",
+ "wrangler": "^4.95.0"
},
"engines": {
"node": ">=24.12.0"
diff --git a/public/auth_config.json b/public/auth_config.json
deleted file mode 100644
index 841be81..0000000
--- a/public/auth_config.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "domain": "id.letsbuilda.dev",
- "clientId": "THoRnVJxMOfLxMAW0kaNW8nuP1q8q3qQ"
-}
diff --git a/public/css/main.css b/public/css/main.css
index ae61671..4cff3b7 100644
--- a/public/css/main.css
+++ b/public/css/main.css
@@ -1,8 +1,87 @@
-.hidden {
- display: none;
+:root {
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+ line-height: 1.5;
+ color: #1a1a1a;
}
-label {
- margin-bottom: 10px;
- display: block;
+body {
+ margin: 0;
+}
+
+nav {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid #e2e2e2;
+}
+
+nav a {
+ color: #2563eb;
+ text-decoration: none;
+}
+
+nav a:hover {
+ text-decoration: underline;
+}
+
+.nav-spacer {
+ flex: 1 1 auto;
+}
+
+nav button {
+ cursor: pointer;
+ padding: 0.4rem 0.9rem;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ background: #f9fafb;
+ font: inherit;
+}
+
+nav button:hover {
+ background: #f3f4f6;
+}
+
+main {
+ max-width: 40rem;
+ margin: 2rem auto;
+ padding: 0 1.5rem;
+}
+
+.profile-card {
+ display: grid;
+ gap: 1rem;
+ padding: 1.5rem;
+ border: 1px solid #e2e2e2;
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgb(0 0 0 / 8%);
+}
+
+.profile-card__avatar {
+ width: 96px;
+ height: 96px;
+ border-radius: 50%;
+ object-fit: cover;
+ background: #f3f4f6;
+}
+
+.profile-card__name {
+ margin: 0;
+}
+
+.profile-card__fields {
+ display: grid;
+ grid-template-columns: max-content 1fr;
+ gap: 0.5rem 1.5rem;
+ margin: 0;
+}
+
+.profile-card__fields dt {
+ font-weight: 600;
+ color: #4b5563;
+}
+
+.profile-card__fields dd {
+ margin: 0;
+ word-break: break-word;
}
diff --git a/public/js/app.js b/public/js/app.js
deleted file mode 100644
index b35841a..0000000
--- a/public/js/app.js
+++ /dev/null
@@ -1,68 +0,0 @@
-let auth0Client = null;
-
-const fetchAuthConfig = () => fetch("/auth_config.json");
-
-const configureClient = async () => {
- const response = await fetchAuthConfig();
- const config = await response.json();
-
- auth0Client = await auth0.createAuth0Client({
- domain: config.domain,
- clientId: config.clientId,
- });
-};
-
-window.onload = async () => {
- await configureClient();
-
- updateUI();
-
- const isAuthenticated = await auth0Client.isAuthenticated();
-
- if (isAuthenticated) {
- return;
- }
-
- const query = window.location.search;
- if (query.includes("code=") && query.includes("state=")) {
- await auth0Client.handleRedirectCallback();
- updateUI();
- window.history.replaceState({}, document.title, "/");
- }
-};
-
-const updateUI = async () => {
- const isAuthenticated = await auth0Client.isAuthenticated();
-
- document.getElementById("btn-logout").disabled = !isAuthenticated;
- document.getElementById("btn-login").disabled = isAuthenticated;
-
- if (isAuthenticated) {
- document.getElementById("gated-content").classList.remove("hidden");
-
- document.getElementById("ipt-access-token").innerHTML =
- await auth0Client.getTokenSilently();
-
- document.getElementById("ipt-user-profile").textContent = JSON.stringify(
- await auth0Client.getUser(),
- );
- } else {
- document.getElementById("gated-content").classList.add("hidden");
- }
-};
-
-const login = async () => {
- await auth0Client.loginWithRedirect({
- authorizationParams: {
- redirect_uri: window.location.origin,
- },
- });
-};
-
-const logout = () => {
- auth0Client.logout({
- logoutParams: {
- returnTo: window.location.origin,
- },
- });
-};
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..765ab4b
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1,12 @@
+///
Loading your account…
+ +Let's build a... something. Glad you're here.
- -Welcome to our page!
- - + +Log in to view your account.
-- You're seeing this content because you're currently - logged in. -
- - -