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 + + + + + + + +
+ + +
+

¹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 + + + + + + + +
+ + +
+

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 @@