diff --git a/api/src/index.js b/api/src/index.js
index 5320b45..7e5107a 100644
--- a/api/src/index.js
+++ b/api/src/index.js
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
var __name = (target, value) =>
__defProp(target, "name", { value, configurable: true });
-const SESSION_TTL = 86400 * 365; // 1 year
+const SESSION_TTL = 86400 * 365 * 2; // 2 years
var index_default = {
async fetch(request, env) {
@@ -40,6 +40,8 @@ var index_default = {
res = await handleMe(request, env);
} else if (path === "/api/refresh" && method === "POST") {
res = await handleRefresh(request, env);
+ } else if (path === "/api/account/refresh" && method === "POST") {
+ res = await handleAccountRefresh(request, env);
} else if (path === "/api/logout" && method === "POST") {
res = await handleLogout(request, env);
} else if (path === "/api/user" && method === "DELETE") {
@@ -281,7 +283,7 @@ async function handleOAuthCallback(request, env) {
JSON.stringify(userRecord)
);
- // One-year persistent session
+ // Two-year persistent session
const sessionId = crypto.randomUUID();
await env.SESSIONS_KV.put(
@@ -308,6 +310,47 @@ __name(
"handleOAuthCallback"
);
+async function fetchBufferAccountProfile(accessToken) {
+ const response = await fetch("https://api.buffer.com", {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ query: `{
+ account {
+ email
+ name
+ }
+ }`
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`Buffer account HTTP error: ${response.status}`);
+ }
+
+ const data = await response.json();
+ if (data.errors?.length) {
+ console.error(
+ "Buffer account GraphQL errors:",
+ JSON.stringify(data.errors)
+ );
+ throw new Error(
+ data.errors[0]?.message || "Buffer account query failed"
+ );
+ }
+
+ if (!data.data?.account) {
+ throw new Error("Buffer account data was not returned");
+ }
+
+ return data.data.account;
+}
+
+__name(fetchBufferAccountProfile, "fetchBufferAccountProfile");
+
// =========================================================
// PKCE
// =========================================================
@@ -407,7 +450,7 @@ async function handleMe(request, env) {
env
);
- // Renew session for another year
+ // Renew session for another two years
const sessionId =
getSessionId(request);
@@ -447,6 +490,58 @@ async function handleMe(request, env) {
__name(handleMe, "handleMe");
+async function handleAccountRefresh(request, env) {
+ if (!originOk(request, env)) {
+ return json({ error: "Forbidden" }, 403);
+ }
+
+ const userId = await getUserIdFromSession(request, env);
+ if (!userId) {
+ return json({ error: "Unauthorized" }, 401);
+ }
+
+ const user = await getUser(userId, env);
+ if (!user) {
+ return json({ error: "Unauthorized" }, 401);
+ }
+
+ try {
+ const account = await fetchBufferAccountProfile(user.accessToken);
+ const updatedUser = {
+ ...user,
+ email: account.email ?? user.email,
+ name:
+ account.name ||
+ account.email?.split("@")[0] ||
+ user.name
+ };
+
+ await env.USERS_KV.put(
+ `user:${userId}`,
+ JSON.stringify(updatedUser)
+ );
+
+ return json({
+ success: true,
+ user: {
+ name: updatedUser.name,
+ email: updatedUser.email
+ }
+ });
+ } catch (e) {
+ console.error("Buffer account refresh failed:", e);
+ return json(
+ {
+ success: false,
+ error: e.message || "Unable to refresh account"
+ },
+ 502
+ );
+ }
+}
+
+__name(handleAccountRefresh, "handleAccountRefresh");
+
async function handleRefresh(
request,
env
diff --git a/auth/index.html b/auth/index.html
new file mode 100644
index 0000000..29420f7
--- /dev/null
+++ b/auth/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+Buffer2iCal – Please Login
+
+
+
+
+
+
+
+
+
+ Buffer2iCal
+ Please Login
+ Please Login into Buffer2iCal using one of the methods below to access your dashboardBack to home page
+
+
+
+
Less Secure & Doesn't Expire²
+
API Key
+
+
+ By clicking one of the buttons above you agree to the risks displayed to your right
+
+
+
+ ¹Unlike API Keys, OAuth sessions expire after set amount of time. When your OAuth Session is about to expire (within 6 hours) you will receive email notice to your Buffer Account Email⁴.
+ ²Unlike OAuth API keys never expire (unless set by the user in the Buffer Dashboard). API keys are less secure, anyone with your API key will have access to your Buffer Account (access controls are set by the user). API Keys don't use 2/MFA and don't require the user to enter their password or email. Do not share your key.
+ ³We do not recommended using Email Sign in. You must have already connected your Buffer Account to this app. Using Email Sign in will send a 6 digit code to your email. Your email must be the same as your Buffer Account
+ ⁴ OAuth Session timeout detection is in beta testing, you may receive unwanted emails form sessiontimeout@alerts.buffer2ical.madebydanny.uk
+
+
+
+
\ No newline at end of file
diff --git a/auth/oauth.html b/auth/oauth.html
new file mode 100644
index 0000000..856f88b
--- /dev/null
+++ b/auth/oauth.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+Buffer2iCal – Sign In Using OAuth
+
+
+
+
+
+
+
+
+
+ Buffer2iCal
+ Please Login
+ Please Login into Buffer2iCal using one of the methods below to access your dashboardChoose a different method
+
+
+
+
Less Secure & Doesn't Expire²
+
API Key
+
+
+
+
+
+ Sign In Using OAuth
+ Sign In Using Buffer OAuth. This will open the Buffer Login Page in a new tab.
+
+ Login in new tab¹
+ By clicking this button you agree to the risks listed below and our privacy policy
+
+
+
¹Unlike API Keys, OAuth sessions expire after set amount of time. When your OAuth Session is about to expire (within 6 hours) you will receive email notice to your Buffer Account Email⁴.
+
⁴OAuth Session timeout detection is in beta testing, you may receive unwanted emails form sessiontimeout@alerts.buffer2ical.madebydanny.uk
+
+
+
+
+
\ No newline at end of file
diff --git a/css/auth.css b/css/auth.css
new file mode 100644
index 0000000..756acd2
--- /dev/null
+++ b/css/auth.css
@@ -0,0 +1,81 @@
+* { margin: 0; padding: 0; box-sizing: border-box; }
+
+:root {
+ --bg: #231F20;
+ --yellow: #FFE22B;
+ --salmon: #FF9B76;
+ --text: #CCCCCC;
+ --muted: #8a8687;
+}
+
+body {
+ font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ background: var(--bg);
+ color: #fff;
+ min-height: 100vh;
+ padding: 2rem 1.5rem;
+ line-height: 1.35;
+}
+
+.layout {
+ display: grid;
+ grid-template-columns: minmax(240px, 300px) 1fr;
+ gap: 4rem;
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+h1 { color: var(--yellow); font-size: 2rem; font-weight: 500; line-height: 1.1; }
+h2.sub { color: var(--yellow); font-size: 1.6rem; font-weight: 400; margin-bottom: 0.5rem; }
+.intro { color: var(--text); font-size: 1.05rem; margin-bottom: 1.25rem; }
+a { color: var(--salmon); text-decoration: none; border-bottom: 1px dotted var(--salmon); }
+.nav-label { display: block; font-size: 0.7rem; color: var(--text); margin-bottom: 0.2rem; }
+.nav-item { margin-bottom: 0.9rem; }
+.nav-item--disabled { opacity: 0.5; pointer-events: none; }
+.nav-btn, .action {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ background: var(--salmon);
+ color: #fff;
+ text-decoration: none;
+ border: none;
+ font-size: 1.1rem;
+ padding: 0.5rem 0.6rem;
+ width: 150px;
+ cursor: pointer;
+ transition: filter 0.15s;
+}
+.nav-btn:hover { filter: brightness(1.08); }
+.nav-btn.active { background: var(--yellow); }
+.nav-btn svg, .action svg { width: 1.2em; height: 1.2em; fill: currentColor; flex: none; }
+
+/* right-hand panel */
+.panel { text-align: center; padding-top: 1.5rem; }
+.panel h2 { color: var(--yellow); font-size: 1.6rem; font-weight: 400; margin-bottom: 0.2rem; }
+.panel .lead { color: var(--text); font-size: 1.05rem; max-width: 30rem; margin: 0 auto; }
+.panel .lead.caps { text-transform: uppercase; }
+
+.action {
+ background: var(--yellow);
+ color: #fff;
+ justify-content: center;
+ width: 219px;
+ font-weight: 700;
+ margin: 3.5rem auto 0.5rem;
+}
+.action.wide { width: 219px; }
+
+.fine { color: var(--muted); font-size: 0.7rem; text-align: left; max-width: 219px; margin: 0 auto 0.5rem; }
+.notes { color: var(--muted); font-size: 0.7rem; text-align: left; max-width: 219px; margin: 1.25rem auto 0; }
+.notes p { margin-bottom: 0.9rem; }
+
+/* index variant: all notes visible */
+.layout.all .notes-all { color: var(--muted); font-size: 0.8rem; padding-top: 2.2rem; max-width: 540px; }
+.layout.all .notes-all p { margin-bottom: 1.1rem; }
+.agree { color: var(--muted); font-size: 0.7rem; max-width: 150px; margin-top: 0.4rem; }
+
+@media (max-width: 700px) {
+ .layout { grid-template-columns: 1fr; gap: 2rem; }
+ .panel { padding-top: 0; }
+}
\ No newline at end of file
diff --git a/dashboard.html b/dashboard.html
index 26d5cca..c5bb478 100644
--- a/dashboard.html
+++ b/dashboard.html
@@ -4,6 +4,7 @@
Dashboard - Buffer2iCal
+
@@ -15,6 +16,10 @@
Demo mode — example data only. No account changes can be made.
Logged in as: ( )
+
+ Refresh account
+
+
Sign out
@@ -53,14 +58,14 @@
@@ -14,7 +27,7 @@
Your Buffer schedule, right in your calendar.
See what's coming up and stay on top of every scheduled post. Automatically sync your Buffer Queue with Google Calendar, Proton Calendar, and more.
-
Connect Buffer
+
Connect Buffer
How it works:
@@ -26,7 +39,6 @@