aboutsummaryrefslogtreecommitdiff
path: root/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes')
-rw-r--r--src/routes/api/badges/+server.ts9
-rw-r--r--src/routes/api/oauth/refresh/+server.ts29
-rw-r--r--src/routes/feeds/activity-notifications/+server.ts64
-rw-r--r--src/routes/settings/+page.server.ts14
-rw-r--r--src/routes/settings/+page.svelte2
5 files changed, 64 insertions, 54 deletions
diff --git a/src/routes/api/badges/+server.ts b/src/routes/api/badges/+server.ts
index 10b63125..a4212f40 100644
--- a/src/routes/api/badges/+server.ts
+++ b/src/routes/api/badges/+server.ts
@@ -15,6 +15,7 @@ import {
} from "$lib/Database/SB/User/badges";
import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie";
import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody";
+import { checkClickCounterLimit } from "$lib/Error/rateLimit";
import { appOrigin, appOriginHeaders } from "$lib/Utility/appOrigin";
import { isOwnerOrPrivileged } from "$lib/Utility/authorisation";
import privilegedUser from "$lib/Utility/privilegedUser";
@@ -53,8 +54,14 @@ export const DELETE = async ({ url, cookies }) => {
return await badges(identity.id);
};
-export const PUT = async ({ cookies, url, request }) => {
+export const PUT = async (event) => {
+ const { cookies, url, request } = event;
+
if (url.searchParams.get("incrementClickCount") || undefined) {
+ const limited = await checkClickCounterLimit(event);
+
+ if (limited) return limited;
+
if (request.headers.get("origin") !== appOrigin()) return unauthorised();
await incrementClickCount(
diff --git a/src/routes/api/oauth/refresh/+server.ts b/src/routes/api/oauth/refresh/+server.ts
deleted file mode 100644
index 49306076..00000000
--- a/src/routes/api/oauth/refresh/+server.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { redirect } from "@sveltejs/kit";
-import { env } from "$env/dynamic/private";
-import { env as env2 } from "$env/dynamic/public";
-
-export const GET = async ({ url, cookies }) => {
- const formData = new FormData();
-
- formData.append("grant_type", "refresh_token");
- formData.append("client_id", env2.PUBLIC_ANILIST_CLIENT_ID as string);
- formData.append("client_secret", env.ANILIST_CLIENT_SECRET as string);
- formData.append("refresh_token", url.searchParams.get("token") || "");
-
- const newUser = await (
- await fetch("https://anilist.co/api/v2/oauth/token", {
- method: "POST",
- body: formData,
- })
- ).json();
-
- cookies.set("user", JSON.stringify(newUser), {
- path: "/",
- maxAge: 60 * 60 * 24 * 7,
- httpOnly: false,
- sameSite: "lax",
- });
-
- if (url.searchParams.get("redirect")) redirect(303, "/");
- else return Response.json(newUser);
-};
diff --git a/src/routes/feeds/activity-notifications/+server.ts b/src/routes/feeds/activity-notifications/+server.ts
index 64ba3fdc..145e236e 100644
--- a/src/routes/feeds/activity-notifications/+server.ts
+++ b/src/routes/feeds/activity-notifications/+server.ts
@@ -2,16 +2,15 @@ import {
type Notification,
notifications,
} from "$lib/Data/AniList/notifications";
+import { refreshAniListToken } from "$lib/Utility/anilistOauth";
import { siteUrl } from "$lib/Utility/appOrigin";
-import root from "$lib/Utility/root";
+import { decryptFeedToken } from "$lib/Utility/feedToken";
const htmlEncode = (input: string) => {
return input.replace(/[\u00A0-\u9999<>&]/g, (i) => `&#${i.charCodeAt(0)};`);
};
-const render = (
- posts: Notification[] = [],
-) => `<?xml version="1.0" encoding="UTF-8" ?>
+const channel = (items: string) => `<?xml version="1.0" encoding="UTF-8" ?>
<rss
version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
@@ -27,7 +26,14 @@ xmlns:media="http://search.yahoo.com/mrss/">
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<language>en-US</language>
<snf:logo><url>${siteUrl("/favicon-196x196.png")}</url></snf:logo>
- ${posts
+ ${items}
+ </channel>
+</rss>
+`;
+
+const render = (posts: Notification[] = []) =>
+ channel(
+ posts
.filter((notification: Notification) => notification.type !== undefined)
.map((notification: Notification) => {
let title = `${notification.user.name}${notification.context}`;
@@ -66,28 +72,40 @@ xmlns:media="http://search.yahoo.com/mrss/">
<pubDate>${new Date(notification.createdAt * 1000).toUTCString()}</pubDate>
</item>`;
})
- .join("")}
- </channel>
-</rss>
-`;
-
-export const GET = async ({ url }) => {
- let token = url.searchParams.get("token");
- const refresh = url.searchParams.get("refresh");
- let notification = await notifications(token || "");
+ .join(""),
+ );
- if (notification === null) {
- token = (
- await (await fetch(root(`/api/oauth/refresh?token=${refresh}`))).json()
- ).access_token;
+// Old feed URLs carried the tokens in clear (?token=&refresh=); they no longer
+// resolve, so degrade to a single item nudging the user to re-copy the link
+// instead of silently going empty.
+const staleNotice = () =>
+ channel(`<item>
+<guid isPermaLink="false">due-moe-feed-url-outdated</guid>
+<title>${htmlEncode("Your due.moe RSS feed URL is outdated — re-copy it from Settings")}</title>
+<link>${siteUrl("/settings#feeds")}</link>
+<description>${htmlEncode("This feed link no longer works. Open due.moe → Settings → RSS Feeds and copy the new URL into your reader.")}</description>
+<pubDate>${new Date().toUTCString()}</pubDate>
+</item>`);
- notification = await notifications(token as string);
- }
-
- return new Response(token ? render(notification || []) : render(), {
+const feed = (body: string) =>
+ new Response(body, {
headers: {
- "Cache-Control": `max-age=0`,
+ "Cache-Control": "max-age=0",
"Content-Type": "application/xml",
},
});
+
+export const GET = async ({ url }) => {
+ const sealed = url.searchParams.get("feed");
+ const refreshToken = sealed ? await decryptFeedToken(sealed) : null;
+
+ if (!refreshToken) return feed(staleNotice());
+
+ const accessToken = await refreshAniListToken(refreshToken);
+
+ if (!accessToken) return feed(render());
+
+ const notification = await notifications(accessToken);
+
+ return feed(render(notification || []));
};
diff --git a/src/routes/settings/+page.server.ts b/src/routes/settings/+page.server.ts
new file mode 100644
index 00000000..321e5cfd
--- /dev/null
+++ b/src/routes/settings/+page.server.ts
@@ -0,0 +1,14 @@
+import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie";
+import { encryptFeedToken } from "$lib/Utility/feedToken";
+
+// Mint the RSS feed token server-side: the encryption key never reaches the
+// client, so the URL is built here from the refresh token already in the cookie
+// rather than from tokens handed to the browser.
+export const load = async ({ cookies }) => {
+ const cookie = cookies.get("user");
+ const user = cookie ? decodeAuthCookieOrNull(cookie) : null;
+
+ return {
+ feedToken: user ? await encryptFeedToken(user.refreshToken) : undefined,
+ };
+};
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte
index 79642944..9a3bf990 100644
--- a/src/routes/settings/+page.svelte
+++ b/src/routes/settings/+page.svelte
@@ -55,7 +55,7 @@ export let data: PageData;
<SettingSync />
</Category>
<Category title={$locale().settings.rssFeeds.title} id="feeds" newLine={false}>
- <RssFeeds user={data.user} />
+ <RssFeeds feedToken={data.feedToken} />
</Category>
</div>