diff options
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/api/badges/+server.ts | 9 | ||||
| -rw-r--r-- | src/routes/api/oauth/refresh/+server.ts | 29 | ||||
| -rw-r--r-- | src/routes/feeds/activity-notifications/+server.ts | 64 | ||||
| -rw-r--r-- | src/routes/settings/+page.server.ts | 14 | ||||
| -rw-r--r-- | src/routes/settings/+page.svelte | 2 |
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> |